create-assistant-ui 0.0.75 → 0.0.76
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 +0 -2
- package/dist/index.js.map +1 -1
- package/dist/run.d.ts +2 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +51 -3
- package/dist/run.js.map +1 -1
- package/package.json +7 -5
- package/src/index.ts +0 -3
- package/src/run.test.ts +155 -0
- package/src/run.ts +56 -3
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { main } from \"./run\";\n\
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { main } from \"./run\";\n\nvoid main();\n"],"mappings":";;;AAGK,KAAK"}
|
package/dist/run.d.ts
CHANGED
package/dist/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","names":[],"sources":["../src/run.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"run.d.ts","names":[],"sources":["../src/run.ts"],"mappings":";iBA4BgB,SAAS,iBAAiB,iBAAiB;iBAqErC,QAAQ"}
|
package/dist/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "cross-spawn";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { constants } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
//#region src/run.ts
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
@@ -11,11 +12,51 @@ var SpawnExitError = class extends Error {
|
|
|
11
12
|
this.code = code;
|
|
12
13
|
}
|
|
13
14
|
};
|
|
15
|
+
var SpawnSignalError = class extends Error {
|
|
16
|
+
signal;
|
|
17
|
+
forwarded;
|
|
18
|
+
constructor(signal, forwarded) {
|
|
19
|
+
super(`Process terminated by ${signal}`);
|
|
20
|
+
this.signal = signal;
|
|
21
|
+
this.forwarded = forwarded;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
14
24
|
function runSpawn(command, args) {
|
|
15
25
|
return new Promise((resolve, reject) => {
|
|
16
26
|
const child = spawn(command, args, { stdio: "inherit" });
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
let forwardedSignal = null;
|
|
28
|
+
const forwardSignal = (signal) => {
|
|
29
|
+
if (forwardedSignal !== null) {
|
|
30
|
+
child.kill("SIGKILL");
|
|
31
|
+
cleanup();
|
|
32
|
+
reject(new SpawnSignalError(signal, true));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
forwardedSignal = signal;
|
|
36
|
+
child.kill(signal);
|
|
37
|
+
};
|
|
38
|
+
const onSigint = () => forwardSignal("SIGINT");
|
|
39
|
+
const onSigterm = () => forwardSignal("SIGTERM");
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
process.off("SIGINT", onSigint);
|
|
42
|
+
process.off("SIGTERM", onSigterm);
|
|
43
|
+
};
|
|
44
|
+
process.on("SIGINT", onSigint);
|
|
45
|
+
process.on("SIGTERM", onSigterm);
|
|
46
|
+
child.on("error", (error) => {
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
child.on("close", (code, signal) => {
|
|
51
|
+
cleanup();
|
|
52
|
+
if (forwardedSignal !== null) {
|
|
53
|
+
reject(new SpawnSignalError(forwardedSignal, true));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (signal !== null) {
|
|
57
|
+
reject(new SpawnSignalError(signal, false));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
19
60
|
if (code !== 0) reject(new SpawnExitError(code || 1));
|
|
20
61
|
else resolve();
|
|
21
62
|
});
|
|
@@ -36,6 +77,13 @@ async function main() {
|
|
|
36
77
|
if (args[0] !== "create") args.unshift("create");
|
|
37
78
|
await runSpawn(process.execPath, [assistantUiBinPath, ...args]);
|
|
38
79
|
} catch (error) {
|
|
80
|
+
if (error instanceof SpawnSignalError) {
|
|
81
|
+
if (error.forwarded) {
|
|
82
|
+
process.removeAllListeners(error.signal);
|
|
83
|
+
process.kill(process.pid, error.signal);
|
|
84
|
+
}
|
|
85
|
+
process.exit(128 + (constants.signals[error.signal] ?? 0));
|
|
86
|
+
}
|
|
39
87
|
if (error instanceof SpawnExitError) process.exit(error.code);
|
|
40
88
|
const message = error instanceof Error ? error.message : String(error);
|
|
41
89
|
console.error(`Error: ${message}`);
|
|
@@ -43,6 +91,6 @@ async function main() {
|
|
|
43
91
|
}
|
|
44
92
|
}
|
|
45
93
|
//#endregion
|
|
46
|
-
export { main };
|
|
94
|
+
export { main, runSpawn };
|
|
47
95
|
|
|
48
96
|
//# sourceMappingURL=run.js.map
|
package/dist/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","names":[],"sources":["../src/run.ts"],"sourcesContent":["import { spawn } from \"cross-spawn\";\nimport { readFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nconst require = createRequire(import.meta.url);\n\nclass SpawnExitError extends Error {\n code: number;\n\n constructor(code: number) {\n super(`Process exited with code ${code}`);\n this.code = code;\n }\n}\n\
|
|
1
|
+
{"version":3,"file":"run.js","names":[],"sources":["../src/run.ts"],"sourcesContent":["import { spawn } from \"cross-spawn\";\nimport { readFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { constants } from \"node:os\";\nimport path from \"node:path\";\n\nconst require = createRequire(import.meta.url);\n\nclass SpawnExitError extends Error {\n code: number;\n\n constructor(code: number) {\n super(`Process exited with code ${code}`);\n this.code = code;\n }\n}\n\nclass SpawnSignalError extends Error {\n signal: NodeJS.Signals;\n forwarded: boolean;\n\n constructor(signal: NodeJS.Signals, forwarded: boolean) {\n super(`Process terminated by ${signal}`);\n this.signal = signal;\n this.forwarded = forwarded;\n }\n}\n\nexport function runSpawn(command: string, args: string[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, {\n stdio: \"inherit\",\n });\n let forwardedSignal: NodeJS.Signals | null = null;\n\n const forwardSignal = (signal: NodeJS.Signals) => {\n if (forwardedSignal !== null) {\n child.kill(\"SIGKILL\");\n cleanup();\n reject(new SpawnSignalError(signal, true));\n return;\n }\n forwardedSignal = signal;\n child.kill(signal);\n };\n const onSigint = () => forwardSignal(\"SIGINT\");\n const onSigterm = () => forwardSignal(\"SIGTERM\");\n const cleanup = () => {\n process.off(\"SIGINT\", onSigint);\n process.off(\"SIGTERM\", onSigterm);\n };\n\n process.on(\"SIGINT\", onSigint);\n process.on(\"SIGTERM\", onSigterm);\n\n child.on(\"error\", (error) => {\n cleanup();\n reject(error);\n });\n child.on(\"close\", (code, signal) => {\n cleanup();\n if (forwardedSignal !== null) {\n reject(new SpawnSignalError(forwardedSignal, true));\n return;\n }\n if (signal !== null) {\n reject(new SpawnSignalError(signal, false));\n return;\n }\n if (code !== 0) {\n reject(new SpawnExitError(code || 1));\n } else {\n resolve();\n }\n });\n });\n}\n\nasync function resolveAssistantUiBinPath(): Promise<string> {\n const packageJsonPath = require.resolve(\"assistant-ui/package.json\");\n const packageJsonRaw = await readFile(packageJsonPath, \"utf8\");\n const packageJson = JSON.parse(packageJsonRaw) as {\n bin?: string | Record<string, string>;\n };\n\n const bin =\n typeof packageJson.bin === \"string\"\n ? packageJson.bin\n : packageJson.bin?.[\"assistant-ui\"];\n\n if (!bin) {\n throw new Error(\"assistant-ui package does not expose a binary.\");\n }\n\n return path.resolve(path.dirname(packageJsonPath), bin);\n}\n\nexport async function main(): Promise<void> {\n try {\n const assistantUiBinPath = await resolveAssistantUiBinPath();\n\n const args = process.argv.slice(2);\n if (args[0] !== \"create\") {\n args.unshift(\"create\");\n }\n\n await runSpawn(process.execPath, [assistantUiBinPath, ...args]);\n } catch (error) {\n if (error instanceof SpawnSignalError) {\n if (error.forwarded) {\n process.removeAllListeners(error.signal);\n process.kill(process.pid, error.signal);\n }\n process.exit(128 + (constants.signals[error.signal] ?? 0));\n }\n\n if (error instanceof SpawnExitError) {\n process.exit(error.code);\n }\n\n const message = error instanceof Error ? error.message : String(error);\n console.error(`Error: ${message}`);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;AAMA,MAAM,UAAU,cAAc,YAAY,GAAG;AAE7C,IAAM,iBAAN,cAA6B,MAAM;CACjC;CAEA,YAAY,MAAc;EACxB,MAAM,4BAA4B,MAAM;EACxC,KAAK,OAAO;CACd;AACF;AAEA,IAAM,mBAAN,cAA+B,MAAM;CACnC;CACA;CAEA,YAAY,QAAwB,WAAoB;EACtD,MAAM,yBAAyB,QAAQ;EACvC,KAAK,SAAS;EACd,KAAK,YAAY;CACnB;AACF;AAEA,SAAgB,SAAS,SAAiB,MAA+B;CACvE,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,MAAM,SAAS,MAAM,EACjC,OAAO,UACT,CAAC;EACD,IAAI,kBAAyC;EAE7C,MAAM,iBAAiB,WAA2B;GAChD,IAAI,oBAAoB,MAAM;IAC5B,MAAM,KAAK,SAAS;IACpB,QAAQ;IACR,OAAO,IAAI,iBAAiB,QAAQ,IAAI,CAAC;IACzC;GACF;GACA,kBAAkB;GAClB,MAAM,KAAK,MAAM;EACnB;EACA,MAAM,iBAAiB,cAAc,QAAQ;EAC7C,MAAM,kBAAkB,cAAc,SAAS;EAC/C,MAAM,gBAAgB;GACpB,QAAQ,IAAI,UAAU,QAAQ;GAC9B,QAAQ,IAAI,WAAW,SAAS;EAClC;EAEA,QAAQ,GAAG,UAAU,QAAQ;EAC7B,QAAQ,GAAG,WAAW,SAAS;EAE/B,MAAM,GAAG,UAAU,UAAU;GAC3B,QAAQ;GACR,OAAO,KAAK;EACd,CAAC;EACD,MAAM,GAAG,UAAU,MAAM,WAAW;GAClC,QAAQ;GACR,IAAI,oBAAoB,MAAM;IAC5B,OAAO,IAAI,iBAAiB,iBAAiB,IAAI,CAAC;IAClD;GACF;GACA,IAAI,WAAW,MAAM;IACnB,OAAO,IAAI,iBAAiB,QAAQ,KAAK,CAAC;IAC1C;GACF;GACA,IAAI,SAAS,GACX,OAAO,IAAI,eAAe,QAAQ,CAAC,CAAC;QAEpC,QAAQ;EAEZ,CAAC;CACH,CAAC;AACH;AAEA,eAAe,4BAA6C;CAC1D,MAAM,kBAAkB,QAAQ,QAAQ,2BAA2B;CACnE,MAAM,iBAAiB,MAAM,SAAS,iBAAiB,MAAM;CAC7D,MAAM,cAAc,KAAK,MAAM,cAAc;CAI7C,MAAM,MACJ,OAAO,YAAY,QAAQ,WACvB,YAAY,MACZ,YAAY,MAAM;CAExB,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,gDAAgD;CAGlE,OAAO,KAAK,QAAQ,KAAK,QAAQ,eAAe,GAAG,GAAG;AACxD;AAEA,eAAsB,OAAsB;CAC1C,IAAI;EACF,MAAM,qBAAqB,MAAM,0BAA0B;EAE3D,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;EACjC,IAAI,KAAK,OAAO,UACd,KAAK,QAAQ,QAAQ;EAGvB,MAAM,SAAS,QAAQ,UAAU,CAAC,oBAAoB,GAAG,IAAI,CAAC;CAChE,SAAS,OAAO;EACd,IAAI,iBAAiB,kBAAkB;GACrC,IAAI,MAAM,WAAW;IACnB,QAAQ,mBAAmB,MAAM,MAAM;IACvC,QAAQ,KAAK,QAAQ,KAAK,MAAM,MAAM;GACxC;GACA,QAAQ,KAAK,OAAO,UAAU,QAAQ,MAAM,WAAW,EAAE;EAC3D;EAEA,IAAI,iBAAiB,gBACnB,QAAQ,KAAK,MAAM,IAAI;EAGzB,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,QAAQ,MAAM,UAAU,SAAS;EACjC,QAAQ,KAAK,CAAC;CAChB;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-assistant-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.76",
|
|
4
4
|
"description": "Create assistant-ui apps with one command",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
],
|
|
26
26
|
"sideEffects": false,
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"assistant-ui": "0.0.113",
|
|
29
|
+
"cross-spawn": "^7.0.6"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
+
"@assistant-ui/x-buildutils": "0.0.24",
|
|
32
33
|
"@types/cross-spawn": "^6.0.6",
|
|
33
|
-
"
|
|
34
|
+
"vitest": "^4.1.11"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
|
-
"build": "aui-build"
|
|
50
|
+
"build": "aui-build",
|
|
51
|
+
"test": "vitest run"
|
|
50
52
|
}
|
|
51
53
|
}
|
package/src/index.ts
CHANGED
package/src/run.test.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
spawn: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("cross-spawn", async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal()),
|
|
10
|
+
spawn: mocks.spawn,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { main, runSpawn } from "./run";
|
|
14
|
+
|
|
15
|
+
const createChild = () =>
|
|
16
|
+
Object.assign(new EventEmitter(), {
|
|
17
|
+
kill: vi.fn(() => true),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("runSpawn", () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("forwards termination signals to the child", async () => {
|
|
27
|
+
const child = createChild();
|
|
28
|
+
mocks.spawn.mockReturnValue(child);
|
|
29
|
+
const existingListeners = new Set(process.listeners("SIGTERM"));
|
|
30
|
+
const initialListeners = process.listenerCount("SIGTERM");
|
|
31
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
32
|
+
const signalHandler = process
|
|
33
|
+
.listeners("SIGTERM")
|
|
34
|
+
.find((listener) => !existingListeners.has(listener));
|
|
35
|
+
|
|
36
|
+
expect(signalHandler).toBeDefined();
|
|
37
|
+
signalHandler?.();
|
|
38
|
+
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
39
|
+
|
|
40
|
+
child.emit("close", 0, null);
|
|
41
|
+
await expect(result).rejects.toMatchObject({
|
|
42
|
+
signal: "SIGTERM",
|
|
43
|
+
forwarded: true,
|
|
44
|
+
});
|
|
45
|
+
expect(process.listenerCount("SIGTERM")).toBe(initialListeners);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("force-stops the child after a repeated termination signal", async () => {
|
|
49
|
+
const child = createChild();
|
|
50
|
+
mocks.spawn.mockReturnValue(child);
|
|
51
|
+
const existingListeners = new Set(process.listeners("SIGTERM"));
|
|
52
|
+
const initialListeners = process.listenerCount("SIGTERM");
|
|
53
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
54
|
+
const signalHandler = process
|
|
55
|
+
.listeners("SIGTERM")
|
|
56
|
+
.find((listener) => !existingListeners.has(listener));
|
|
57
|
+
|
|
58
|
+
expect(signalHandler).toBeDefined();
|
|
59
|
+
signalHandler?.();
|
|
60
|
+
signalHandler?.();
|
|
61
|
+
|
|
62
|
+
expect(child.kill).toHaveBeenNthCalledWith(1, "SIGTERM");
|
|
63
|
+
expect(child.kill).toHaveBeenNthCalledWith(2, "SIGKILL");
|
|
64
|
+
await expect(result).rejects.toMatchObject({
|
|
65
|
+
signal: "SIGTERM",
|
|
66
|
+
forwarded: true,
|
|
67
|
+
});
|
|
68
|
+
expect(process.listenerCount("SIGTERM")).toBe(initialListeners);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("distinguishes child crash signals from forwarded signals", async () => {
|
|
72
|
+
const child = createChild();
|
|
73
|
+
mocks.spawn.mockReturnValue(child);
|
|
74
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
75
|
+
|
|
76
|
+
child.emit("close", null, "SIGSEGV");
|
|
77
|
+
|
|
78
|
+
await expect(result).rejects.toMatchObject({
|
|
79
|
+
signal: "SIGSEGV",
|
|
80
|
+
forwarded: false,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("removes signal forwarding after normal completion", async () => {
|
|
85
|
+
const child = createChild();
|
|
86
|
+
mocks.spawn.mockReturnValue(child);
|
|
87
|
+
const initialSigintListeners = process.listenerCount("SIGINT");
|
|
88
|
+
const initialSigtermListeners = process.listenerCount("SIGTERM");
|
|
89
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
90
|
+
|
|
91
|
+
child.emit("close", 0, null);
|
|
92
|
+
await expect(result).resolves.toBeUndefined();
|
|
93
|
+
|
|
94
|
+
expect(process.listenerCount("SIGINT")).toBe(initialSigintListeners);
|
|
95
|
+
expect(process.listenerCount("SIGTERM")).toBe(initialSigtermListeners);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("main", () => {
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vi.restoreAllMocks();
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("re-raises a signal forwarded to the child", async () => {
|
|
106
|
+
const child = createChild();
|
|
107
|
+
mocks.spawn.mockReturnValue(child);
|
|
108
|
+
const existingListeners = new Set(process.listeners("SIGTERM"));
|
|
109
|
+
const kill = vi.spyOn(process, "kill").mockReturnValue(true);
|
|
110
|
+
const removeAllListeners = vi
|
|
111
|
+
.spyOn(process, "removeAllListeners")
|
|
112
|
+
.mockReturnValue(process);
|
|
113
|
+
const exit = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
114
|
+
throw new Error("process.exit");
|
|
115
|
+
});
|
|
116
|
+
const result = main();
|
|
117
|
+
|
|
118
|
+
await vi.waitFor(() => expect(mocks.spawn).toHaveBeenCalledOnce());
|
|
119
|
+
const signalHandler = process
|
|
120
|
+
.listeners("SIGTERM")
|
|
121
|
+
.find((listener) => !existingListeners.has(listener));
|
|
122
|
+
expect(signalHandler).toBeDefined();
|
|
123
|
+
signalHandler?.();
|
|
124
|
+
child.emit("close", null, "SIGTERM");
|
|
125
|
+
|
|
126
|
+
await expect(result).rejects.toThrow("process.exit");
|
|
127
|
+
expect(removeAllListeners).toHaveBeenCalledWith("SIGTERM");
|
|
128
|
+
expect(kill).toHaveBeenCalledWith(process.pid, "SIGTERM");
|
|
129
|
+
expect(removeAllListeners.mock.invocationCallOrder[0]).toBeLessThan(
|
|
130
|
+
kill.mock.invocationCallOrder[0]!,
|
|
131
|
+
);
|
|
132
|
+
expect(exit).toHaveBeenCalledWith(143);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("does not re-raise a signal received only by the child", async () => {
|
|
136
|
+
const child = createChild();
|
|
137
|
+
mocks.spawn.mockReturnValue(child);
|
|
138
|
+
const kill = vi.spyOn(process, "kill").mockReturnValue(true);
|
|
139
|
+
const removeAllListeners = vi
|
|
140
|
+
.spyOn(process, "removeAllListeners")
|
|
141
|
+
.mockReturnValue(process);
|
|
142
|
+
const exit = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
143
|
+
throw new Error("process.exit");
|
|
144
|
+
});
|
|
145
|
+
const result = main();
|
|
146
|
+
|
|
147
|
+
await vi.waitFor(() => expect(mocks.spawn).toHaveBeenCalledOnce());
|
|
148
|
+
child.emit("close", null, "SIGSEGV");
|
|
149
|
+
|
|
150
|
+
await expect(result).rejects.toThrow("process.exit");
|
|
151
|
+
expect(removeAllListeners).not.toHaveBeenCalled();
|
|
152
|
+
expect(kill).not.toHaveBeenCalled();
|
|
153
|
+
expect(exit).toHaveBeenCalledWith(139);
|
|
154
|
+
});
|
|
155
|
+
});
|
package/src/run.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "cross-spawn";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { constants } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
@@ -14,14 +15,58 @@ class SpawnExitError extends Error {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
class SpawnSignalError extends Error {
|
|
19
|
+
signal: NodeJS.Signals;
|
|
20
|
+
forwarded: boolean;
|
|
21
|
+
|
|
22
|
+
constructor(signal: NodeJS.Signals, forwarded: boolean) {
|
|
23
|
+
super(`Process terminated by ${signal}`);
|
|
24
|
+
this.signal = signal;
|
|
25
|
+
this.forwarded = forwarded;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function runSpawn(command: string, args: string[]): Promise<void> {
|
|
18
30
|
return new Promise((resolve, reject) => {
|
|
19
31
|
const child = spawn(command, args, {
|
|
20
32
|
stdio: "inherit",
|
|
21
33
|
});
|
|
34
|
+
let forwardedSignal: NodeJS.Signals | null = null;
|
|
35
|
+
|
|
36
|
+
const forwardSignal = (signal: NodeJS.Signals) => {
|
|
37
|
+
if (forwardedSignal !== null) {
|
|
38
|
+
child.kill("SIGKILL");
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(new SpawnSignalError(signal, true));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
forwardedSignal = signal;
|
|
44
|
+
child.kill(signal);
|
|
45
|
+
};
|
|
46
|
+
const onSigint = () => forwardSignal("SIGINT");
|
|
47
|
+
const onSigterm = () => forwardSignal("SIGTERM");
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
process.off("SIGINT", onSigint);
|
|
50
|
+
process.off("SIGTERM", onSigterm);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
process.on("SIGINT", onSigint);
|
|
54
|
+
process.on("SIGTERM", onSigterm);
|
|
22
55
|
|
|
23
|
-
child.on("error", (error) =>
|
|
24
|
-
|
|
56
|
+
child.on("error", (error) => {
|
|
57
|
+
cleanup();
|
|
58
|
+
reject(error);
|
|
59
|
+
});
|
|
60
|
+
child.on("close", (code, signal) => {
|
|
61
|
+
cleanup();
|
|
62
|
+
if (forwardedSignal !== null) {
|
|
63
|
+
reject(new SpawnSignalError(forwardedSignal, true));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (signal !== null) {
|
|
67
|
+
reject(new SpawnSignalError(signal, false));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
25
70
|
if (code !== 0) {
|
|
26
71
|
reject(new SpawnExitError(code || 1));
|
|
27
72
|
} else {
|
|
@@ -61,6 +106,14 @@ export async function main(): Promise<void> {
|
|
|
61
106
|
|
|
62
107
|
await runSpawn(process.execPath, [assistantUiBinPath, ...args]);
|
|
63
108
|
} catch (error) {
|
|
109
|
+
if (error instanceof SpawnSignalError) {
|
|
110
|
+
if (error.forwarded) {
|
|
111
|
+
process.removeAllListeners(error.signal);
|
|
112
|
+
process.kill(process.pid, error.signal);
|
|
113
|
+
}
|
|
114
|
+
process.exit(128 + (constants.signals[error.signal] ?? 0));
|
|
115
|
+
}
|
|
116
|
+
|
|
64
117
|
if (error instanceof SpawnExitError) {
|
|
65
118
|
process.exit(error.code);
|
|
66
119
|
}
|