@heyamiko/amiko-cli 0.4.11 → 0.5.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/dist/index.js +75768 -0
- package/package.json +26 -19
- package/PLAN.md +0 -113
- package/bun.lock +0 -743
- package/dist/__tests__/cli-non-tty.test.js +0 -74
- package/dist/agent/piAgent.js +0 -239
- package/dist/agent/tools.js +0 -149
- package/dist/app.js +0 -355
- package/dist/cli.js +0 -94
- package/dist/commands.js +0 -22
- package/dist/components/ModelPicker.js +0 -63
- package/dist/config.js +0 -26
- package/dist/configFile.js +0 -23
- package/dist/headless.js +0 -218
- package/dist/memory/qmd.js +0 -87
- package/dist/prompt.js +0 -43
- package/dist/session.js +0 -26
- package/dist/types.js +0 -1
- package/dist/workspace.js +0 -42
- package/src/__tests__/cli-non-tty.test.ts +0 -86
- package/src/agent/piAgent.ts +0 -266
- package/src/agent/tools.ts +0 -153
- package/src/app.tsx +0 -448
- package/src/cli.tsx +0 -115
- package/src/commands.ts +0 -39
- package/src/components/ModelPicker.tsx +0 -107
- package/src/config.ts +0 -31
- package/src/configFile.ts +0 -33
- package/src/headless.ts +0 -222
- package/src/memory/qmd.ts +0 -111
- package/src/prompt.ts +0 -55
- package/src/session.ts +0 -35
- package/src/types.ts +0 -40
- package/src/workspace.ts +0 -58
- package/tsconfig.json +0 -16
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { join, dirname } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const CLI_PATH = join(__dirname, "..", "..", "dist", "cli.js");
|
|
7
|
-
function runCli(args = [], stdin) {
|
|
8
|
-
return new Promise((resolve) => {
|
|
9
|
-
const child = execFile("node", [CLI_PATH, ...args], {
|
|
10
|
-
timeout: 10000,
|
|
11
|
-
env: { ...process.env, HOME: "/tmp/amiko-cli-test" },
|
|
12
|
-
}, (error, stdout, stderr) => {
|
|
13
|
-
resolve({
|
|
14
|
-
stdout: stdout || "",
|
|
15
|
-
stderr: stderr || "",
|
|
16
|
-
code: error?.code === "ETIMEDOUT" ? 0 : error?.status ?? 0,
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
if (stdin !== undefined) {
|
|
20
|
-
child.stdin?.write(stdin);
|
|
21
|
-
}
|
|
22
|
-
child.stdin?.end();
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
describe("amiko-cli non-TTY behavior", () => {
|
|
26
|
-
it("does not crash with RangeError in piped mode", async () => {
|
|
27
|
-
const result = await runCli();
|
|
28
|
-
const combined = result.stdout + result.stderr;
|
|
29
|
-
expect(combined).not.toContain("RangeError");
|
|
30
|
-
expect(combined).not.toContain("Invalid count value");
|
|
31
|
-
});
|
|
32
|
-
it("enters headless mode when stdin is not a TTY", async () => {
|
|
33
|
-
const result = await runCli([], "/help\n");
|
|
34
|
-
const combined = result.stdout + result.stderr;
|
|
35
|
-
expect(combined).toContain("headless");
|
|
36
|
-
expect(combined).toContain("Commands:");
|
|
37
|
-
});
|
|
38
|
-
it("--headless flag is recognized", async () => {
|
|
39
|
-
const result = await runCli(["--headless"], "/help\n");
|
|
40
|
-
const combined = result.stdout + result.stderr;
|
|
41
|
-
expect(combined).toContain("headless");
|
|
42
|
-
expect(combined).toContain("Commands:");
|
|
43
|
-
});
|
|
44
|
-
it("headless /help lists available commands", async () => {
|
|
45
|
-
const result = await runCli([], "/help\n");
|
|
46
|
-
expect(result.stdout).toContain("/exit");
|
|
47
|
-
expect(result.stdout).toContain("/memory");
|
|
48
|
-
expect(result.stdout).toContain("/model");
|
|
49
|
-
expect(result.stdout).toContain("/search");
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
describe("String.repeat patch", () => {
|
|
53
|
-
it("clamps negative values to empty string", () => {
|
|
54
|
-
const origRepeat = String.prototype.repeat;
|
|
55
|
-
String.prototype.repeat = function (count) {
|
|
56
|
-
return origRepeat.call(this, Math.max(0, Math.floor(count)));
|
|
57
|
-
};
|
|
58
|
-
try {
|
|
59
|
-
expect("─".repeat(-2)).toBe("");
|
|
60
|
-
expect("─".repeat(-1)).toBe("");
|
|
61
|
-
expect("─".repeat(0)).toBe("");
|
|
62
|
-
expect("─".repeat(1)).toBe("─");
|
|
63
|
-
expect("─".repeat(3)).toBe("───");
|
|
64
|
-
expect("x".repeat(2.9)).toBe("xx");
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
String.prototype.repeat = origRepeat;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
it("original String.repeat throws on negative values", () => {
|
|
71
|
-
expect(() => "x".repeat(-2)).toThrow(RangeError);
|
|
72
|
-
expect(() => "x".repeat(-1)).toThrow(RangeError);
|
|
73
|
-
});
|
|
74
|
-
});
|
package/dist/agent/piAgent.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
|
-
import { getEnvApiKey, getModel, getModels, getProviders } from "@mariozechner/pi-ai";
|
|
3
|
-
import { createBashTool, createReadTool } from "./tools.js";
|
|
4
|
-
function createQueue() {
|
|
5
|
-
const queue = [];
|
|
6
|
-
let pending = null;
|
|
7
|
-
return {
|
|
8
|
-
push(value) {
|
|
9
|
-
if (pending) {
|
|
10
|
-
const resolve = pending;
|
|
11
|
-
pending = null;
|
|
12
|
-
resolve(value);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
queue.push(value);
|
|
16
|
-
},
|
|
17
|
-
end() {
|
|
18
|
-
if (pending) {
|
|
19
|
-
const resolve = pending;
|
|
20
|
-
pending = null;
|
|
21
|
-
resolve(null);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
queue.push(null);
|
|
25
|
-
},
|
|
26
|
-
async *iterate() {
|
|
27
|
-
while (true) {
|
|
28
|
-
if (queue.length > 0) {
|
|
29
|
-
const value = queue.shift();
|
|
30
|
-
if (value === null)
|
|
31
|
-
return;
|
|
32
|
-
yield value;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const value = await new Promise((resolve) => {
|
|
36
|
-
pending = resolve;
|
|
37
|
-
});
|
|
38
|
-
if (value === null)
|
|
39
|
-
return;
|
|
40
|
-
yield value;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function resolveModel(config) {
|
|
46
|
-
if (config.model) {
|
|
47
|
-
const [provider, ...rest] = config.model.split("/");
|
|
48
|
-
const modelId = rest.join("/");
|
|
49
|
-
if (provider && modelId) {
|
|
50
|
-
return { provider, modelId };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (config.modelProvider && config.modelId) {
|
|
54
|
-
return { provider: config.modelProvider, modelId: config.modelId };
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const PROVIDER_PRIORITY = [
|
|
59
|
-
"openai",
|
|
60
|
-
"anthropic",
|
|
61
|
-
"openrouter",
|
|
62
|
-
"google",
|
|
63
|
-
"groq",
|
|
64
|
-
"mistral",
|
|
65
|
-
"xai",
|
|
66
|
-
"cerebras",
|
|
67
|
-
"openai-codex",
|
|
68
|
-
"azure-openai-responses",
|
|
69
|
-
"amazon-bedrock",
|
|
70
|
-
"google-vertex",
|
|
71
|
-
"google-gemini-cli",
|
|
72
|
-
"vercel-ai-gateway",
|
|
73
|
-
"zai",
|
|
74
|
-
"minimax",
|
|
75
|
-
"minimax-cn",
|
|
76
|
-
"opencode",
|
|
77
|
-
"github-copilot"
|
|
78
|
-
];
|
|
79
|
-
const DEFAULT_MODEL_BY_PROVIDER = {
|
|
80
|
-
openai: ["gpt-4.1", "gpt-4.1-mini", "gpt-4o-mini", "gpt-4o"],
|
|
81
|
-
anthropic: ["claude-3-5-sonnet-latest", "claude-3-5-sonnet-20241022", "claude-3-5-sonnet-20240620"],
|
|
82
|
-
openrouter: ["openai/gpt-4.1", "openai/gpt-4.1-mini", "anthropic/claude-3.5-sonnet"],
|
|
83
|
-
google: ["gemini-2.5-flash-lite-preview-06-17", "gemini-2.5-pro-preview-06-17", "gemini-1.5-pro"],
|
|
84
|
-
groq: ["llama-3.1-70b-versatile", "llama-3.1-8b-instant"],
|
|
85
|
-
mistral: ["mistral-large-latest", "mistral-small-latest"],
|
|
86
|
-
xai: ["grok-2-latest", "grok-2"],
|
|
87
|
-
cerebras: ["llama3.1-70b", "llama3.1-8b"],
|
|
88
|
-
"openai-codex": ["gpt-5.1-codex-max", "gpt-5.2-codex", "gpt-5.2"]
|
|
89
|
-
};
|
|
90
|
-
function pickDefaultModelId(provider) {
|
|
91
|
-
const models = getModels(provider);
|
|
92
|
-
if (models.length === 0)
|
|
93
|
-
return null;
|
|
94
|
-
const preferred = DEFAULT_MODEL_BY_PROVIDER[provider] || [];
|
|
95
|
-
for (const id of preferred) {
|
|
96
|
-
if (models.some((m) => m.id === id))
|
|
97
|
-
return id;
|
|
98
|
-
}
|
|
99
|
-
return models.map((m) => m.id).sort()[0] || null;
|
|
100
|
-
}
|
|
101
|
-
function pickAutoModel() {
|
|
102
|
-
const providers = getProviders();
|
|
103
|
-
const providersWithKeys = providers.filter((provider) => Boolean(getEnvApiKey(provider)));
|
|
104
|
-
if (providersWithKeys.length === 0)
|
|
105
|
-
return null;
|
|
106
|
-
const ordered = PROVIDER_PRIORITY.filter((p) => providersWithKeys.includes(p));
|
|
107
|
-
const provider = ordered[0] || providersWithKeys.sort()[0];
|
|
108
|
-
if (!provider)
|
|
109
|
-
return null;
|
|
110
|
-
const modelId = pickDefaultModelId(provider);
|
|
111
|
-
if (!modelId)
|
|
112
|
-
return null;
|
|
113
|
-
return { provider, modelId };
|
|
114
|
-
}
|
|
115
|
-
export async function createPiAgent(config) {
|
|
116
|
-
const modelInfo = resolveModel(config) || pickAutoModel();
|
|
117
|
-
const model = modelInfo ? getModel(modelInfo.provider, modelInfo.modelId) : undefined;
|
|
118
|
-
if (modelInfo) {
|
|
119
|
-
if (!model) {
|
|
120
|
-
return fallbackAgent(`Unknown model ${modelInfo.provider}/${modelInfo.modelId}.`);
|
|
121
|
-
}
|
|
122
|
-
const apiKey = getEnvApiKey(modelInfo.provider);
|
|
123
|
-
if (!apiKey) {
|
|
124
|
-
return fallbackAgent(`No API key found for ${modelInfo.provider}. Set the provider env var or run /login ${modelInfo.provider}.`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const initialState = {
|
|
128
|
-
thinkingLevel: config.thinkingLevel ?? "off",
|
|
129
|
-
systemPrompt: "",
|
|
130
|
-
tools: [createReadTool({ root: process.cwd() }), createBashTool({ cwd: process.cwd() })],
|
|
131
|
-
messages: []
|
|
132
|
-
};
|
|
133
|
-
if (model)
|
|
134
|
-
initialState.model = model;
|
|
135
|
-
const agent = new Agent({
|
|
136
|
-
initialState,
|
|
137
|
-
getApiKey: (provider) => getEnvApiKey(provider)
|
|
138
|
-
});
|
|
139
|
-
const eventListeners = [];
|
|
140
|
-
agent.subscribe((event) => {
|
|
141
|
-
for (const listener of eventListeners) {
|
|
142
|
-
listener(event);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
return {
|
|
146
|
-
async *streamReply(input) {
|
|
147
|
-
const queue = createQueue();
|
|
148
|
-
let ended = false;
|
|
149
|
-
const endOnce = () => {
|
|
150
|
-
if (ended)
|
|
151
|
-
return;
|
|
152
|
-
ended = true;
|
|
153
|
-
queue.end();
|
|
154
|
-
};
|
|
155
|
-
agent.setSystemPrompt(input.prompt);
|
|
156
|
-
let toolErrors = 0;
|
|
157
|
-
const maxToolErrors = 3;
|
|
158
|
-
const provider = agent.state?.model?.provider;
|
|
159
|
-
if (provider && !getEnvApiKey(provider)) {
|
|
160
|
-
queue.push(`\n[amiko] No API key configured for ${provider}.\n` +
|
|
161
|
-
`Set the provider env var and retry, e.g. OPENAI_API_KEY or ANTHROPIC_API_KEY.\n` +
|
|
162
|
-
`Then run /model <provider>/<modelId>.\n`);
|
|
163
|
-
endOnce();
|
|
164
|
-
yield* queue.iterate();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const unsubscribe = agent.subscribe((event) => {
|
|
168
|
-
if (event?.type === "message_update") {
|
|
169
|
-
const msg = event.assistantMessageEvent;
|
|
170
|
-
if (msg?.type === "text_delta") {
|
|
171
|
-
const delta = typeof msg.delta === "string" ? msg.delta : msg.textDelta;
|
|
172
|
-
if (typeof delta === "string")
|
|
173
|
-
queue.push(delta);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (event?.type === "tool_execution_end" && event.isError) {
|
|
177
|
-
toolErrors += 1;
|
|
178
|
-
if (toolErrors >= maxToolErrors) {
|
|
179
|
-
queue.push("\n[amiko] Tool execution failed too many times. Aborting this response.\n");
|
|
180
|
-
agent.abort();
|
|
181
|
-
endOnce();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (event?.type === "agent_end") {
|
|
185
|
-
endOnce();
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
try {
|
|
189
|
-
await agent.prompt(input.userMessage);
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
queue.push(`\n[amiko] Agent error: ${error.message || "unknown"}\n`);
|
|
193
|
-
}
|
|
194
|
-
finally {
|
|
195
|
-
unsubscribe();
|
|
196
|
-
endOnce();
|
|
197
|
-
}
|
|
198
|
-
yield* queue.iterate();
|
|
199
|
-
},
|
|
200
|
-
setModel(provider, modelId) {
|
|
201
|
-
const resolved = getModel(provider, modelId);
|
|
202
|
-
if (!resolved) {
|
|
203
|
-
return { ok: false, message: `Unknown model ${provider}/${modelId}.` };
|
|
204
|
-
}
|
|
205
|
-
const apiKey = getEnvApiKey(provider);
|
|
206
|
-
if (!apiKey) {
|
|
207
|
-
return {
|
|
208
|
-
ok: false,
|
|
209
|
-
message: `No API key found for ${provider}. Set the provider env var or run /login ${provider}.`
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
agent.setModel(resolved);
|
|
213
|
-
return { ok: true, message: `Using model ${provider}/${modelId}.` };
|
|
214
|
-
},
|
|
215
|
-
getCurrentModel() {
|
|
216
|
-
const current = agent.state?.model;
|
|
217
|
-
if (!current)
|
|
218
|
-
return null;
|
|
219
|
-
return `${current.provider}/${current.id}`;
|
|
220
|
-
},
|
|
221
|
-
onEvent(handler) {
|
|
222
|
-
eventListeners.push(handler);
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
function fallbackAgent(reason) {
|
|
227
|
-
return {
|
|
228
|
-
async *streamReply(input) {
|
|
229
|
-
const message = [
|
|
230
|
-
"[amiko] Agent backend unavailable.",
|
|
231
|
-
`Reason: ${reason}`,
|
|
232
|
-
"Configure AMIKO_MODEL or AMIKO_MODEL_PROVIDER/AMIKO_MODEL_ID and API keys.",
|
|
233
|
-
"Echoing your input:",
|
|
234
|
-
input.userMessage
|
|
235
|
-
].join("\n");
|
|
236
|
-
yield message;
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
}
|
package/dist/agent/tools.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { Type } from "@sinclair/typebox";
|
|
7
|
-
const MAX_OUTPUT_BYTES = 200_000;
|
|
8
|
-
function textContent(text) {
|
|
9
|
-
return { type: "text", text };
|
|
10
|
-
}
|
|
11
|
-
function resolvePath(root, targetPath) {
|
|
12
|
-
if (path.isAbsolute(targetPath))
|
|
13
|
-
return targetPath;
|
|
14
|
-
return path.resolve(root, targetPath);
|
|
15
|
-
}
|
|
16
|
-
function expandTilde(value) {
|
|
17
|
-
if (!value)
|
|
18
|
-
return value;
|
|
19
|
-
if (value === "~")
|
|
20
|
-
return os.homedir();
|
|
21
|
-
if (value.startsWith("~/"))
|
|
22
|
-
return path.join(os.homedir(), value.slice(2));
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
function resolveShell() {
|
|
26
|
-
const candidates = [process.env.SHELL, "/bin/zsh", "/bin/bash", "/bin/sh"].filter(Boolean);
|
|
27
|
-
for (const candidate of candidates) {
|
|
28
|
-
try {
|
|
29
|
-
if (fsSync.existsSync(candidate))
|
|
30
|
-
return candidate;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
// ignore
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
export function createReadTool(options) {
|
|
39
|
-
return {
|
|
40
|
-
name: "read",
|
|
41
|
-
label: "read",
|
|
42
|
-
description: "Read a file from disk",
|
|
43
|
-
parameters: Type.Object({
|
|
44
|
-
path: Type.String({ description: "Path to the file" }),
|
|
45
|
-
maxBytes: Type.Optional(Type.Number({ description: "Max bytes to read" }))
|
|
46
|
-
}),
|
|
47
|
-
async execute(_toolCallId, params) {
|
|
48
|
-
const filePath = resolvePath(options.root, expandTilde(params.path));
|
|
49
|
-
const maxBytes = params.maxBytes ?? MAX_OUTPUT_BYTES;
|
|
50
|
-
try {
|
|
51
|
-
const data = await fs.readFile(filePath);
|
|
52
|
-
const sliced = data.subarray(0, Math.max(0, maxBytes));
|
|
53
|
-
const text = sliced.toString("utf8");
|
|
54
|
-
const suffix = data.length > sliced.length ? `\n[truncated ${data.length - sliced.length} bytes]` : "";
|
|
55
|
-
return { content: [textContent(text + suffix)], details: { path: filePath, bytes: sliced.length } };
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
const message = error.message || "read failed";
|
|
59
|
-
return {
|
|
60
|
-
content: [textContent(`Error reading ${filePath}: ${message}`)],
|
|
61
|
-
details: { path: filePath, error: message }
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
export function createBashTool(options) {
|
|
68
|
-
return {
|
|
69
|
-
name: "bash",
|
|
70
|
-
label: "bash",
|
|
71
|
-
description: "Execute a shell command",
|
|
72
|
-
parameters: Type.Object({
|
|
73
|
-
command: Type.String({ description: "Shell command to execute" }),
|
|
74
|
-
cwd: Type.Optional(Type.String({ description: "Working directory" })),
|
|
75
|
-
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in milliseconds" }))
|
|
76
|
-
}),
|
|
77
|
-
execute(_toolCallId, params, signal) {
|
|
78
|
-
const cwd = params.cwd ? resolvePath(options.cwd, expandTilde(params.cwd)) : options.cwd;
|
|
79
|
-
const timeoutMs = params.timeoutMs ?? 30_000;
|
|
80
|
-
const shell = resolveShell();
|
|
81
|
-
return new Promise((resolve) => {
|
|
82
|
-
if (!shell) {
|
|
83
|
-
resolve({
|
|
84
|
-
content: [textContent("Error running command: no shell available in environment")],
|
|
85
|
-
details: { cwd, code: null, timedOut: false, error: "no shell available" }
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const command = expandTilde(params.command);
|
|
90
|
-
const child = spawn(command, { cwd, shell });
|
|
91
|
-
let stdout = "";
|
|
92
|
-
let stderr = "";
|
|
93
|
-
let timedOut = false;
|
|
94
|
-
const onData = (chunk, target) => {
|
|
95
|
-
const text = chunk.toString("utf8");
|
|
96
|
-
if (target === "stdout")
|
|
97
|
-
stdout += text;
|
|
98
|
-
else
|
|
99
|
-
stderr += text;
|
|
100
|
-
if (stdout.length + stderr.length > MAX_OUTPUT_BYTES) {
|
|
101
|
-
stdout = stdout.slice(0, MAX_OUTPUT_BYTES);
|
|
102
|
-
stderr = stderr.slice(0, MAX_OUTPUT_BYTES - stdout.length);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
child.stdout?.on("data", (chunk) => onData(chunk, "stdout"));
|
|
106
|
-
child.stderr?.on("data", (chunk) => onData(chunk, "stderr"));
|
|
107
|
-
child.on("error", (error) => {
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
if (signal)
|
|
110
|
-
signal.removeEventListener("abort", abortHandler);
|
|
111
|
-
const message = error.message || "spawn error";
|
|
112
|
-
resolve({
|
|
113
|
-
content: [textContent(`Error running command: ${message}`)],
|
|
114
|
-
details: { cwd, code: null, timedOut: false, error: message }
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
const timer = setTimeout(() => {
|
|
118
|
-
timedOut = true;
|
|
119
|
-
child.kill("SIGKILL");
|
|
120
|
-
}, timeoutMs);
|
|
121
|
-
const abortHandler = () => {
|
|
122
|
-
child.kill("SIGKILL");
|
|
123
|
-
};
|
|
124
|
-
if (signal) {
|
|
125
|
-
if (signal.aborted)
|
|
126
|
-
abortHandler();
|
|
127
|
-
signal.addEventListener("abort", abortHandler, { once: true });
|
|
128
|
-
}
|
|
129
|
-
child.on("close", (code) => {
|
|
130
|
-
clearTimeout(timer);
|
|
131
|
-
if (signal)
|
|
132
|
-
signal.removeEventListener("abort", abortHandler);
|
|
133
|
-
const output = stdout.trimEnd();
|
|
134
|
-
const errorOut = stderr.trimEnd();
|
|
135
|
-
const lines = [
|
|
136
|
-
`cwd: ${cwd}`,
|
|
137
|
-
`exit: ${timedOut ? "timeout" : code}`,
|
|
138
|
-
output ? `stdout:\n${output}` : "stdout: (empty)",
|
|
139
|
-
errorOut ? `stderr:\n${errorOut}` : "stderr: (empty)"
|
|
140
|
-
];
|
|
141
|
-
resolve({
|
|
142
|
-
content: [textContent(lines.join("\n"))],
|
|
143
|
-
details: { cwd, code, timedOut }
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|