返回 CodeWhale
run.js
根目录 / npm / codewhale / scripts / run.js
1 const { spawn: spawnChild } = require("child_process");
2 const os = require("os");
3 const { getBinaryPath, installFailureHint } = require("./install");
4
5 const pkg = require("../package.json");
6
7 function isVersionFlag(args = process.argv.slice(2)) {
8 return args.includes("--version") || args.includes("-V");
9 }
10
11 // Print the install hint (mirror / release-base guidance) for a failure that
12 // looks like a download problem. Prints nothing for other failures.
13 function printInstallFailureHint(error, log = console.error) {
14 const hint = installFailureHint(error);
15 if (hint) {
16 log(hint);
17 }
18 }
19
20 // Shared by the `codewhale` and `codew` bin shims: the error and, when the
21 // binary could not be downloaded, the hint that says what to do about it.
22 function reportStartFailure(binaryName, error, log = console.error) {
23 log(`Failed to start ${binaryName}:`, error && error.message ? error.message : String(error));
24 printInstallFailureHint(error, log);
25 }
26
27 // `--version` must still answer when the native binary is missing, but it
28 // must not report a binary version that is not actually installed: the
29 // expected version goes to stdout labelled as such, the failure to stderr.
30 function printVersionFallback(binaryName, error) {
31 const binVersion =
32 process.env.CODEWHALE_VERSION ||
33 process.env.DEEPSEEK_TUI_VERSION ||
34 process.env.DEEPSEEK_VERSION ||
35 pkg.codewhaleBinaryVersion || pkg.deepseekBinaryVersion || pkg.version;
36 console.log(`${binaryName} (npm wrapper) v${pkg.version}`);
37 console.log(`binary: not installed (expected v${binVersion})`);
38 console.log(`repo: ${pkg.repository?.url || "N/A"}`);
39 if (error) {
40 console.error(`${binaryName}: native binary unavailable: ${error.message || String(error)}`);
41 printInstallFailureHint(error);
42 }
43 }
44
45 // Signals that end the wrapper must end the native binary too. With a
46 // blocking spawn the wrapper died on SIGTERM and left the child running (still
47 // executing tools, still holding its port); as PID 1 in a container it ignored
48 // SIGTERM entirely. So SIGTERM, which a terminal never sends, is forwarded.
49 //
50 // SIGINT (Ctrl-C) and SIGHUP (terminal hangup) come from the terminal, which
51 // delivers them to the whole foreground process group, native child included.
52 // Forwarding them would deliver each one twice, and the native binary treats a
53 // second signal as "skip the graceful drain". On Windows `child.kill()` is a
54 // hard TerminateProcess, which would race the child's own Ctrl-C cleanup. The
55 // wrapper only has to outlive them, then mirror how the child ended.
56 const FORWARDED_SIGNALS = process.platform === "win32" ? [] : ["SIGTERM"];
57 const OUTLIVED_SIGNALS = ["SIGINT", "SIGHUP"];
58
59 function ignoreSignal() {}
60
61 // Run the native binary and settle with how it ended. While it runs, the
62 // forwarded signals go to the child and the outlived ones do not kill the
63 // wrapper.
64 function runChild(spawn, binaryPath, args, proc) {
65 return new Promise((resolve) => {
66 let child;
67 try {
68 child = spawn(binaryPath, args, { stdio: "inherit" });
69 } catch (error) {
70 resolve({ error });
71 return;
72 }
73 const listeners = [
74 ...FORWARDED_SIGNALS.map((signal) => [
75 signal,
76 () => {
77 try {
78 child.kill(signal);
79 } catch {
80 // The child already exited; its exit event settles the run.
81 }
82 },
83 ]),
84 ...OUTLIVED_SIGNALS.map((signal) => [signal, ignoreSignal]),
85 ];
86 for (const [signal, listener] of listeners) {
87 proc.on(signal, listener);
88 }
89 let settled = false;
90 const settle = (result) => {
91 if (settled) return;
92 settled = true;
93 for (const [signal, listener] of listeners) {
94 proc.removeListener(signal, listener);
95 }
96 resolve(result);
97 };
98 child.once("error", (error) => settle({ error }));
99 child.once("exit", (status, signal) => settle({ status, signal }));
100 });
101 }
102
103 // Mirror a signal death to the parent shell: re-raise it on the wrapper once
104 // our listeners are gone, and fall back to the conventional 128 + n.
105 function exitLikeChild(result, exit, proc) {
106 if (result.signal) {
107 try {
108 proc.kill(proc.pid, result.signal);
109 } catch {
110 // fall through to the numeric status
111 }
112 const number = os.constants.signals[result.signal];
113 return exit(number ? 128 + number : 1);
114 }
115 return exit(result.status ?? 1);
116 }
117
118 async function run(binaryName, options = {}) {
119 const args = options.args || process.argv.slice(2);
120 const resolveBinaryPath = options.getBinaryPath || getBinaryPath;
121 const spawn = options.spawn || spawnChild;
122 const exit = options.exit || process.exit;
123 const proc = options.process || process;
124 const versionFlag = isVersionFlag(args);
125
126 let binaryPath;
127 try {
128 binaryPath = await resolveBinaryPath(binaryName);
129 } catch (error) {
130 if (versionFlag) {
131 printVersionFallback(binaryName, error);
132 return exit(0);
133 }
134 throw error;
135 }
136
137 const result = await runChild(spawn, binaryPath, args, proc);
138 if (result.error) {
139 if (versionFlag) {
140 printVersionFallback(binaryName, result.error);
141 return exit(0);
142 }
143 throw result.error;
144 }
145 return exitLikeChild(result, exit, proc);
146 }
147
148 async function runCodeWhale() {
149 await run("codewhale");
150 }
151
152 async function runCodeWhaleTui() {
153 // v0.9.5 single-binary: tui is now an alias to codewhale (kept for backwards compat, will warn)
154 if (!process.env.CODEWHALE_SUPPRESS_TUI_DEPRECATION) {
155 process.stderr.write("codewhale-tui: deprecated alias to `codewhale` (single binary since v0.9.5). Use `codewhale` instead.\n");
156 }
157 await run("codewhale");
158 }
159
160 module.exports = {
161 run,
162 runCodeWhale,
163 runCodeWhaleTui,
164 reportStartFailure,
165 _internal: { isVersionFlag, printVersionFallback, FORWARDED_SIGNALS, OUTLIVED_SIGNALS },
166 };
167
168 if (require.main === module) {
169 const command = process.argv[1] || "";
170 if (command.includes("tui")) {
171 runCodeWhaleTui().catch((error) => {
172 reportStartFailure("codewhale", error);
173 process.exit(1);
174 });
175 } else {
176 runCodeWhale().catch((error) => {
177 reportStartFailure("codewhale", error);
178 process.exit(1);
179 });
180 }
181 }
182
182 lines JAVASCRIPT