@co0ontty/wand 1.41.2 → 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.
- package/dist/build-info.json +3 -3
- package/dist/cli.js +0 -0
- package/dist/npm-update-utils.js +103 -19
- package/dist/web-ui/scripts.js +17 -3
- package/package.json +2 -2
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-05-30T23:
|
|
4
|
-
"version": "1.41.
|
|
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
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -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
|
-
|
|
170
|
+
if (/全局 wand 安装不完整|无法解析 npm 全局安装目录/.test(msg)) {
|
|
171
|
+
note(`[wand] npm install 后安装目录不完整,尝试强制重装...`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
108
176
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
157
|
-
if (!
|
|
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 (!
|
|
247
|
+
if (!hitRecoverableInstallError(res))
|
|
164
248
|
return { ...res, attempts };
|
|
165
249
|
// 终极兜底(卸载用固定包名,兼容 git spec,见 async 版同样注释)
|
|
166
250
|
attempts.push(`npm uninstall -g ${PACKAGE_NAME}`);
|
package/dist/web-ui/scripts.js
CHANGED
|
@@ -10,10 +10,24 @@ function escapeHtml(value) {
|
|
|
10
10
|
.replace(/'/g, "'");
|
|
11
11
|
}
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|