返回 DeepSeek-TUI-2026
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", "deepseek-tui");
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(), "deepseek-npm-smoke-"));
137 const releaseAssetsDir = path.join(tempRoot, "release-assets");
138 const packDir = path.join(tempRoot, "pack");
139 const installDir = path.join(tempRoot, "install");
140 let keepTemp = process.env.DEEPSEEK_TUI_KEEP_SMOKE_DIR === "1";
141 let server;
142
143 try {
144 await fsp.mkdir(packDir, { recursive: true });
145 await fsp.mkdir(installDir, { recursive: true });
146
147 await runCommand(process.execPath, [prepareAssetsScript, releaseAssetsDir]);
148 const served = await serveDirectory(releaseAssetsDir);
149 server = served.server;
150
151 const env = {
152 DEEPSEEK_TUI_FORCE_DOWNLOAD: "1",
153 DEEPSEEK_TUI_RELEASE_BASE_URL: served.baseUrl,
154 };
155 const pack = await runCommand(
156 "npm",
157 ["pack", "--json", "--pack-destination", packDir],
158 {
159 capture: true,
160 cwd: packageDir,
161 env,
162 },
163 );
164 const tarball = path.join(packDir, parsePackJson(pack.stdout));
165
166 await runCommand("npm", ["init", "-y"], { cwd: installDir });
167 await runCommand("npm", ["install", tarball], { cwd: installDir, env });
168 await runCommand("npx", ["--no-install", "deepseek", "doctor", "--help"], {
169 cwd: installDir,
170 env,
171 });
172 await runCommand("npx", ["--no-install", "deepseek-tui", "--help"], {
173 cwd: installDir,
174 env,
175 });
176
177 console.log(`npm wrapper smoke passed with local assets from ${served.baseUrl}`);
178 } catch (error) {
179 keepTemp = true;
180 console.error(`npm wrapper smoke failed: ${error.message}`);
181 console.error(`Smoke workspace retained at ${tempRoot}`);
182 process.exitCode = 1;
183 } finally {
184 if (server) {
185 await new Promise((resolve) => server.close(resolve));
186 }
187 if (!keepTemp) {
188 await fsp.rm(tempRoot, { force: true, recursive: true });
189 }
190 }
191 }
192
193 main();
194
194 lines JAVASCRIPT