@co0ontty/wand 1.41.1 → 1.41.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "7881848a909b65c2a17ee9d443514110e2e23f1f",
3
- "builtAt": "2026-05-30T15:40:31.007Z",
4
- "version": "1.41.1",
2
+ "commit": "1f4df3d175e7fca737cf75fa45388d55dc870531",
3
+ "builtAt": "2026-05-30T23:40:01.415Z",
4
+ "version": "1.41.3",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
File without changes
@@ -12,7 +12,7 @@
12
12
  * 如果第一次安装仍然撞上 ENOTEMPTY,清理后重试;再不行就 uninstall + force install。
13
13
  */
14
14
  import { exec, spawnSync } from "node:child_process";
15
- import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
15
+ import { chmodSync, existsSync, readdirSync, rmSync, statSync } from "node:fs";
16
16
  import path from "node:path";
17
17
  import process from "node:process";
18
18
  import { promisify } from "node:util";
@@ -77,6 +77,68 @@ export function cleanupNpmLeftovers() {
77
77
  }
78
78
  return { removed, errors };
79
79
  }
80
+ const REQUIRED_RUNTIME_FILES = [
81
+ "package.json",
82
+ path.join("dist", "cli.js"),
83
+ path.join("dist", "server.js"),
84
+ path.join("dist", "web-ui", "index.js"),
85
+ path.join("dist", "web-ui", "scripts.js"),
86
+ path.join("dist", "web-ui", "styles.js"),
87
+ path.join("dist", "web-ui", "content", "scripts.js"),
88
+ path.join("dist", "web-ui", "content", "styles.css"),
89
+ path.join("dist", "web-ui", "content", "vendor", "wterm", "wterm.bundle.js"),
90
+ path.join("dist", "web-ui", "content", "vendor", "qrcode", "qrcode.bundle.js"),
91
+ ];
92
+ function getGlobalPackageDir() {
93
+ const root = getNpmGlobalRoot();
94
+ return root ? path.join(root, PACKAGE_SCOPE, PACKAGE_BASENAME) : null;
95
+ }
96
+ function validateGlobalWandInstall() {
97
+ const packageDir = getGlobalPackageDir();
98
+ if (!packageDir) {
99
+ return { ok: false, message: "无法解析 npm 全局安装目录。" };
100
+ }
101
+ const missing = [];
102
+ for (const rel of REQUIRED_RUNTIME_FILES) {
103
+ const fullPath = path.join(packageDir, rel);
104
+ try {
105
+ if (!statSync(fullPath).isFile()) {
106
+ missing.push(rel);
107
+ }
108
+ }
109
+ catch {
110
+ missing.push(rel);
111
+ }
112
+ }
113
+ if (missing.length > 0) {
114
+ return {
115
+ ok: false,
116
+ message: `全局 wand 安装不完整: ${packageDir} 缺少 ${missing.join(", ")}`,
117
+ };
118
+ }
119
+ if (process.platform !== "win32") {
120
+ const cliPath = path.join(packageDir, "dist", "cli.js");
121
+ try {
122
+ const mode = statSync(cliPath).mode;
123
+ if ((mode & 0o111) === 0) {
124
+ chmodSync(cliPath, mode | 0o755);
125
+ }
126
+ }
127
+ catch (err) {
128
+ return {
129
+ ok: false,
130
+ message: `全局 wand CLI 无法设置执行权限: ${cliPath}: ${err instanceof Error ? err.message : String(err)}`,
131
+ };
132
+ }
133
+ }
134
+ return { ok: true, packageDir };
135
+ }
136
+ function assertGlobalWandInstallComplete() {
137
+ const result = validateGlobalWandInstall();
138
+ if (!result.ok) {
139
+ throw new Error(result.message);
140
+ }
141
+ }
80
142
  /**
81
143
  * 异步版本的全局安装:
82
144
  * 1. 清理残留
@@ -99,26 +161,35 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
99
161
  }
100
162
  try {
101
163
  await execAsync(`npm install -g ${pkg}`, { timeout: timeoutMs });
164
+ assertGlobalWandInstallComplete();
102
165
  return;
103
166
  }
104
167
  catch (error) {
105
168
  const msg = error instanceof Error ? error.message : String(error);
106
169
  if (!/ENOTEMPTY|EEXIST/.test(msg)) {
107
- throw error;
170
+ if (/全局 wand 安装不完整|无法解析 npm 全局安装目录/.test(msg)) {
171
+ note(`[wand] npm install 后安装目录不完整,尝试强制重装...`);
172
+ }
173
+ else {
174
+ throw error;
175
+ }
108
176
  }
109
- note(`[wand] npm install 遇到 ENOTEMPTY/EEXIST,清理后重试一次...`);
110
- }
111
- cleanupNpmLeftovers();
112
- try {
113
- await execAsync(`npm install -g ${pkg}`, { timeout: timeoutMs });
114
- return;
115
- }
116
- catch (error) {
117
- const msg = error instanceof Error ? error.message : String(error);
118
- if (!/ENOTEMPTY|EEXIST/.test(msg)) {
119
- throw error;
177
+ else {
178
+ note(`[wand] npm install 遇到 ENOTEMPTY/EEXIST,清理后重试一次...`);
179
+ cleanupNpmLeftovers();
180
+ try {
181
+ await execAsync(`npm install -g ${pkg}`, { timeout: timeoutMs });
182
+ assertGlobalWandInstallComplete();
183
+ return;
184
+ }
185
+ catch (retryError) {
186
+ const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
187
+ if (!/ENOTEMPTY|EEXIST|全局 wand 安装不完整|无法解析 npm 全局安装目录/.test(retryMsg)) {
188
+ throw retryError;
189
+ }
190
+ }
191
+ note(`[wand] 重试仍失败,尝试先卸载再强制安装...`);
120
192
  }
121
- note(`[wand] 重试仍失败,尝试先卸载再强制安装...`);
122
193
  }
123
194
  // 终极兜底:uninstall + force install
124
195
  // 卸载用固定包名 PACKAGE_NAME,而不是从 install spec 反推:spec 可能是 git
@@ -131,6 +202,7 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
131
202
  }
132
203
  cleanupNpmLeftovers();
133
204
  await execAsync(`npm install -g --force ${pkg}`, { timeout: timeoutMs });
205
+ assertGlobalWandInstallComplete();
134
206
  }
135
207
  /**
136
208
  * 同步版本,给 TUI installUpdate 用。
@@ -139,28 +211,40 @@ export async function installPackageGloballyAsync(pkg, timeoutMs, log) {
139
211
  */
140
212
  export function installPackageGloballySync(pkg, timeoutMs) {
141
213
  const attempts = [];
214
+ const withValidation = (res) => {
215
+ if (res.status !== 0)
216
+ return res;
217
+ const validation = validateGlobalWandInstall();
218
+ if (validation.ok)
219
+ return res;
220
+ return {
221
+ status: 1,
222
+ stdout: res.stdout,
223
+ stderr: `${res.stderr ? `${res.stderr}\n` : ""}${validation.message}`,
224
+ };
225
+ };
142
226
  const tryInstall = (extra) => {
143
227
  const args = ["install", "-g", ...extra, pkg];
144
228
  attempts.push(`npm ${args.join(" ")}`);
145
229
  const r = spawnSync("npm", args, { encoding: "utf8", timeout: timeoutMs });
146
- return {
230
+ return withValidation({
147
231
  status: r.status,
148
232
  stdout: r.stdout || "",
149
233
  stderr: r.stderr || "",
150
- };
234
+ });
151
235
  };
152
236
  cleanupNpmLeftovers();
153
237
  let res = tryInstall([]);
154
238
  if (res.status === 0)
155
239
  return { ...res, attempts };
156
- const hitENOTEMPTY = (r) => /ENOTEMPTY|EEXIST/.test(r.stdout + r.stderr);
157
- if (!hitENOTEMPTY(res))
240
+ const hitRecoverableInstallError = (r) => /ENOTEMPTY|EEXIST|全局 wand 安装不完整|无法解析 npm 全局安装目录/.test(r.stdout + r.stderr);
241
+ if (!hitRecoverableInstallError(res))
158
242
  return { ...res, attempts };
159
243
  cleanupNpmLeftovers();
160
244
  res = tryInstall([]);
161
245
  if (res.status === 0)
162
246
  return { ...res, attempts };
163
- if (!hitENOTEMPTY(res))
247
+ if (!hitRecoverableInstallError(res))
164
248
  return { ...res, attempts };
165
249
  // 终极兜底(卸载用固定包名,兼容 git spec,见 async 版同样注释)
166
250
  attempts.push(`npm uninstall -g ${PACKAGE_NAME}`);
@@ -10,10 +10,24 @@ function escapeHtml(value) {
10
10
  .replace(/'/g, "'");
11
11
  }
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
- // Cache the script content
13
+ let _scriptCache = null;
14
+ let _scriptCacheMtimeMs = 0;
14
15
  export function getScriptContent(configPath) {
15
16
  const scriptPath = path.join(__dirname, "content", "scripts.js");
16
- const scriptContent = fs.readFileSync(scriptPath, "utf-8");
17
+ try {
18
+ const stat = fs.statSync(scriptPath);
19
+ if (_scriptCache === null || stat.mtimeMs !== _scriptCacheMtimeMs) {
20
+ _scriptCache = fs.readFileSync(scriptPath, "utf-8");
21
+ _scriptCacheMtimeMs = stat.mtimeMs;
22
+ }
23
+ }
24
+ catch {
25
+ // During self-update npm can briefly replace the global package directory.
26
+ // Keep serving the already-loaded UI until /api/restart switches process.
27
+ if (_scriptCache === null) {
28
+ _scriptCache = fs.readFileSync(scriptPath, "utf-8");
29
+ }
30
+ }
17
31
  // Inject the config path
18
- return scriptContent.replace("${escapeHtml(configPath)}", escapeHtml(configPath));
32
+ return _scriptCache.replace("${escapeHtml(configPath)}", escapeHtml(configPath));
19
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.41.1",
3
+ "version": "1.41.3",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "preferGlobal": true,
18
18
  "scripts": {
19
- "build": "node scripts/bundle-wterm.js && node scripts/bundle-qrcode.js && tsc -p tsconfig.json && npm run build:copy-content && node scripts/stamp-build-info.js",
19
+ "build": "node scripts/bundle-wterm.js && node scripts/bundle-qrcode.js && tsc -p tsconfig.json && npm run build:copy-content && node scripts/stamp-build-info.js && node scripts/fix-dist-permissions.js",
20
20
  "build:copy-content": "cp -r src/web-ui/content dist/web-ui/",
21
21
  "dev": "tsx src/cli.ts web",
22
22
  "check": "tsc --noEmit -p tsconfig.json",