返回 CodeWhale
npm-wrapper-smoke.js
根目录 / scripts / release / npm-wrapper-smoke.js
1 #!/usr/bin/env node
2
3 const fs = require("fs");
4 const fsp = require("fs/promises");
5 const http = require("http");
6 const os = require("os");
7 const path = require("path");
8 const { spawn } = require("child_process");
9
10 const repoRoot = path.resolve(__dirname, "..", "..");
11 const packageDir = path.join(repoRoot, "npm", "codewhale");
12 const prepareAssetsScript = path.join(
13 repoRoot,
14 "scripts",
15 "release",
16 "prepare-local-release-assets.js",
17 );
18
19 function shellQuote(value) {
20 return /\s/.test(value) ? JSON.stringify(value) : value;
21 }
22
23 function usesWindowsCommandShim(command) {
24 return process.platform === "win32" && (command === "npm" || command === "npx");
25 }
26
27 function runCommand(command, args, options = {}) {
28 const cwd = options.cwd || repoRoot;
29 console.log(`$ ${[command, ...args].map(shellQuote).join(" ")}`);
30 const child = spawn(command, args, {
31 cwd,
32 env: {
33 ...process.env,
34 ...(options.env || {}),
35 },
36 encoding: "utf8",
37 shell: usesWindowsCommandShim(command),
38 stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
39 windowsHide: true,
40 });
41
42 if (!options.capture) {
43 return new Promise((resolve, reject) => {
44 child.once("error", reject);
45 child.once("close", (status) => {
46 if (status === 0) {
47 resolve({ stdout: "", stderr: "" });
48 } else {
49 reject(new Error(`${command} exited with status ${status}`));
50 }
51 });
52 });
53 }
54
55 let stdout = "";
56 let stderr = "";
57 child.stdout.setEncoding("utf8");
58 child.stderr.setEncoding("utf8");
59 child.stdout.on("data", (chunk) => {
60 stdout += chunk;
61 });
62 child.stderr.on("data", (chunk) => {
63 stderr += chunk;
64 });
65 return new Promise((resolve, reject) => {
66 child.once("error", reject);
67 child.once("close", (status) => {
68 if (status === 0) {
69 resolve({ stdout, stderr });
70 return;
71 }
72 process.stdout.write(stdout);
73 process.stderr.write(stderr);
74 reject(new Error(`${command} exited with status ${status}`));
75 });
76 });
77 }
78
79 function serveDirectory(root) {
80 const server = http.createServer(async (request, response) => {
81 try {
82 const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
83 const decodedPath = decodeURIComponent(requestUrl.pathname);
84 const filePath = path.resolve(root, `.${decodedPath}`);
85 const relative = path.relative(root, filePath);
86 if (relative.startsWith("..") || path.isAbsolute(relative)) {
87 response.writeHead(403);
88 response.end("forbidden");
89 return;
90 }
91
92 const fileStat = await fsp.stat(filePath);
93 if (!fileStat.isFile()) {
94 response.writeHead(404);
95 response.end("not found");
96 return;
97 }
98
99 response.writeHead(200, {
100 "Content-Length": fileStat.size,
101 "Content-Type": "application/octet-stream",
102 });
103 fs.createReadStream(filePath).pipe(response);
104 } catch (error) {
105 response.writeHead(error && error.code === "ENOENT" ? 404 : 500);
106 response.end(error && error.message ? error.message : "not found");
107 }
108 });
109
110 return new Promise((resolve, reject) => {
111 server.once("error", reject);
112 server.listen(0, "127.0.0.1", () => {
113 const address = server.address();
114 resolve({
115 baseUrl: `http://127.0.0.1:${address.port}/`,
116 server,
117 });
118 });
119 });
120 }
121
122 function parsePackJson(stdout) {
123 const trimmed = stdout.trim();
124 if (!trimmed) {
125 throw new Error("npm pack did not return package metadata");
126 }
127 const parsed = JSON.parse(trimmed);
128 const first = Array.isArray(parsed) ? parsed[0] : parsed;
129 if (!first || !first.filename) {
130 throw new Error(`npm pack metadata did not include a filename: ${trimmed}`);
131 }
132 return first.filename;
133 }
134
135 async function main() {
136 const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "codewhale-npm-smoke-"));
137 const suppliedAssetsDir = String(
138 process.env.CODEWHALE_SMOKE_ASSETS_DIR || "",
139 ).trim();
140 const releaseAssetsDir = suppliedAssetsDir
141 ? path.resolve(suppliedAssetsDir)
142 : path.join(tempRoot, "release-assets");
143 const packDir = path.join(tempRoot, "pack");
144 const installDir = path.join(tempRoot, "install");
145 let keepTemp = process.env.DEEPSEEK_TUI_KEEP_SMOKE_DIR === "1";
146 let server;
147
148 try {
149 await fsp.mkdir(packDir, { recursive: true });
150 await fsp.mkdir(installDir, { recursive: true });
151
152 if (suppliedAssetsDir) {
153 const assetDirectoryStat = await fsp.stat(releaseAssetsDir);
154 if (!assetDirectoryStat.isDirectory()) {
155 throw new Error(`CODEWHALE_SMOKE_ASSETS_DIR is not a directory: ${releaseAssetsDir}`);
156 }
157 console.log(`Using preassembled release assets from ${releaseAssetsDir}`);
158 } else {
159 const prepareArgs = [prepareAssetsScript, releaseAssetsDir];
160 const cargoTargetDir = String(process.env.CARGO_TARGET_DIR || "").trim();
161 if (cargoTargetDir) {
162 prepareArgs.push(
163 path.join(path.resolve(repoRoot, cargoTargetDir), "release"),
164 );
165 }
166 await runCommand(process.execPath, prepareArgs);
167 }
168 const served = await serveDirectory(releaseAssetsDir);
169 server = served.server;
170
171 const env = {
172 DEEPSEEK_TUI_FORCE_DOWNLOAD: "1",
173 DEEPSEEK_TUI_RELEASE_BASE_URL: served.baseUrl,
174 };
175 const pack = await runCommand(
176 "npm",
177 ["pack", "--json", "--pack-destination", packDir],
178 {
179 capture: true,
180 cwd: packageDir,
181 env,
182 },
183 );
184 const tarball = path.join(packDir, parsePackJson(pack.stdout));
185
186 await runCommand("npm", ["init", "-y"], { cwd: installDir });
187 await runCommand("npm", ["install", tarball], { cwd: installDir, env });
188 await runCommand("npx", ["--no-install", "codewhale", "doctor", "--help"], {
189 cwd: installDir,
190 env,
191 });
192 await runCommand("npx", ["--no-install", "codew", "--version"], {
193 cwd: installDir,
194 env,
195 });
196 await runCommand("npx", ["--no-install", "codewhale-tui", "--help"], {
197 cwd: installDir,
198 env,
199 });
200
201 console.log(`npm wrapper smoke passed with local assets from ${served.baseUrl}`);
202 } catch (error) {
203 keepTemp = true;
204 console.error(`npm wrapper smoke failed: ${error.message}`);
205 console.error(`Smoke workspace retained at ${tempRoot}`);
206 process.exitCode = 1;
207 } finally {
208 if (server) {
209 await new Promise((resolve) => server.close(resolve));
210 }
211 if (!keepTemp) {
212 await fsp.rm(tempRoot, { force: true, recursive: true });
213 }
214 }
215 }
216
217 main();
218
218 lines JAVASCRIPT