@co0ontty/wand 1.21.19 → 1.23.0

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * TUI 运维操作命令集合。
3
+ *
4
+ * 所有命令统一返回 CommandResult,UI 层负责把结果渲染到 toast / 弹窗 / 日志。
5
+ * 命令本身不直接写 stdout / stderr —— TUI 模式下 stderr 已经被 log-bus 劫持。
6
+ */
7
+ export interface CommandResult {
8
+ ok: boolean;
9
+ /** 给用户看的简短状态行(一行)。 */
10
+ message: string;
11
+ /** 可选的详细输出(多行),供"详情"折叠展示。 */
12
+ detail?: string;
13
+ }
14
+ /**
15
+ * 重启当前进程。
16
+ * 通过 spawn 一个 detached 子进程复用同一份 argv,然后退出父进程,
17
+ * 让 systemd / 用户终端把控制权交给新进程。
18
+ */
19
+ export declare function restartSelf(): CommandResult;
20
+ export interface UpdateInfo {
21
+ current: string;
22
+ latest: string | null;
23
+ hasUpdate: boolean;
24
+ }
25
+ /** 通过 npm view 拿到最新版本号。失败返回 latest=null。 */
26
+ export declare function checkUpdate(currentVersion: string): UpdateInfo;
27
+ /**
28
+ * 执行 `npm install -g @co0ontty/wand@latest`。
29
+ * 此调用同步阻塞(TUI 上层应在另一线程的 setImmediate 调度,或直接 await)。
30
+ * 返回 npm 输出供调试。
31
+ */
32
+ export declare function installUpdate(): CommandResult;
33
+ export declare function openInBrowser(url: string): CommandResult;
34
+ export declare function copyToClipboard(text: string): CommandResult;
35
+ export interface ServiceContext {
36
+ configPath: string;
37
+ /** wand 可执行文件路径。优先使用 process.argv[1],回退到 which wand。 */
38
+ wandBin?: string;
39
+ }
40
+ export declare function isServiceInstalled(): boolean;
41
+ export interface ServiceStatus {
42
+ /** 是否已安装服务文件。 */
43
+ installed: boolean;
44
+ /** active(running) / inactive / failed / unknown 等;解析自 systemctl/launchctl。 */
45
+ state: "active" | "inactive" | "failed" | "loaded" | "unknown" | "unsupported";
46
+ /** 给用户看的描述行(例如 "active (running) since Mon 2025-05-11 08:23:45 CST; 12min ago")。 */
47
+ description: string;
48
+ /** 原始命令输出,供 Detail 面板展示。 */
49
+ raw: string;
50
+ /** 平台。 */
51
+ platform: NodeJS.Platform;
52
+ }
53
+ export declare function serviceStatus(): ServiceStatus;
54
+ export declare function serviceStart(): CommandResult;
55
+ export declare function serviceStop(): CommandResult;
56
+ export declare function serviceRestart(): CommandResult;
57
+ /** 取最近 N 行服务日志。 */
58
+ export declare function serviceLogs(lines?: number): CommandResult;
59
+ export declare function installService(ctx: ServiceContext): CommandResult;
60
+ export declare function uninstallService(): CommandResult;
@@ -0,0 +1,505 @@
1
+ /**
2
+ * TUI 运维操作命令集合。
3
+ *
4
+ * 所有命令统一返回 CommandResult,UI 层负责把结果渲染到 toast / 弹窗 / 日志。
5
+ * 命令本身不直接写 stdout / stderr —— TUI 模式下 stderr 已经被 log-bus 劫持。
6
+ */
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+ const PACKAGE_NAME = "@co0ontty/wand";
13
+ // ─── 重启 ────────────────────────────────────────────────────────────────
14
+ /**
15
+ * 重启当前进程。
16
+ * 通过 spawn 一个 detached 子进程复用同一份 argv,然后退出父进程,
17
+ * 让 systemd / 用户终端把控制权交给新进程。
18
+ */
19
+ export function restartSelf() {
20
+ try {
21
+ const child = spawn(process.execPath, process.argv.slice(1), {
22
+ detached: true,
23
+ stdio: "inherit",
24
+ env: process.env,
25
+ });
26
+ child.unref();
27
+ // 给个短延时让用户能在屏幕上看到"重启中…"
28
+ setTimeout(() => {
29
+ process.exit(0);
30
+ }, 200);
31
+ return { ok: true, message: "重启中…新进程已派生" };
32
+ }
33
+ catch (err) {
34
+ return { ok: false, message: `重启失败: ${errMsg(err)}` };
35
+ }
36
+ }
37
+ /** 通过 npm view 拿到最新版本号。失败返回 latest=null。 */
38
+ export function checkUpdate(currentVersion) {
39
+ const res = spawnSync("npm", ["view", PACKAGE_NAME, "version"], {
40
+ encoding: "utf8",
41
+ timeout: 15_000,
42
+ });
43
+ if (res.status !== 0 || !res.stdout) {
44
+ return { current: currentVersion, latest: null, hasUpdate: false };
45
+ }
46
+ const latest = res.stdout.trim();
47
+ return {
48
+ current: currentVersion,
49
+ latest,
50
+ hasUpdate: compareSemver(latest, currentVersion) > 0,
51
+ };
52
+ }
53
+ /**
54
+ * 执行 `npm install -g @co0ontty/wand@latest`。
55
+ * 此调用同步阻塞(TUI 上层应在另一线程的 setImmediate 调度,或直接 await)。
56
+ * 返回 npm 输出供调试。
57
+ */
58
+ export function installUpdate() {
59
+ const res = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
60
+ encoding: "utf8",
61
+ timeout: 180_000,
62
+ });
63
+ const out = (res.stdout || "") + (res.stderr ? "\n" + res.stderr : "");
64
+ if (res.status === 0) {
65
+ return {
66
+ ok: true,
67
+ message: "更新已安装。按 [R] 重启以生效。",
68
+ detail: out.trim(),
69
+ };
70
+ }
71
+ return {
72
+ ok: false,
73
+ message: `npm install 失败 (exit ${res.status})`,
74
+ detail: out.trim(),
75
+ };
76
+ }
77
+ // ─── 打开浏览器 ─────────────────────────────────────────────────────────
78
+ export function openInBrowser(url) {
79
+ const cmd = process.platform === "darwin"
80
+ ? "open"
81
+ : process.platform === "win32"
82
+ ? "cmd"
83
+ : "xdg-open";
84
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
85
+ try {
86
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
87
+ child.unref();
88
+ return { ok: true, message: `已在浏览器打开: ${url}` };
89
+ }
90
+ catch (err) {
91
+ return { ok: false, message: `打开失败: ${errMsg(err)}` };
92
+ }
93
+ }
94
+ // ─── 复制到剪贴板 ───────────────────────────────────────────────────────
95
+ export function copyToClipboard(text) {
96
+ const candidates = clipboardCandidates();
97
+ for (const c of candidates) {
98
+ const res = spawnSync(c.cmd, c.args, { input: text, timeout: 5_000 });
99
+ if (res.status === 0) {
100
+ return { ok: true, message: `已复制到剪贴板 (${c.cmd})` };
101
+ }
102
+ }
103
+ return {
104
+ ok: false,
105
+ message: "未找到可用的剪贴板工具 (pbcopy / xclip / wl-copy / clip)",
106
+ };
107
+ }
108
+ function clipboardCandidates() {
109
+ if (process.platform === "darwin")
110
+ return [{ cmd: "pbcopy", args: [] }];
111
+ if (process.platform === "win32")
112
+ return [{ cmd: "clip", args: [] }];
113
+ // Linux:优先 wl-copy(Wayland),其次 xclip / xsel
114
+ return [
115
+ { cmd: "wl-copy", args: [] },
116
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
117
+ { cmd: "xsel", args: ["--clipboard", "--input"] },
118
+ ];
119
+ }
120
+ export function isServiceInstalled() {
121
+ return existsSync(servicePath());
122
+ }
123
+ export function serviceStatus() {
124
+ if (process.platform === "linux")
125
+ return systemdStatus();
126
+ if (process.platform === "darwin")
127
+ return launchdStatus();
128
+ return {
129
+ installed: false,
130
+ state: "unsupported",
131
+ description: `当前平台 ${process.platform} 不支持服务管理`,
132
+ raw: "",
133
+ platform: process.platform,
134
+ };
135
+ }
136
+ export function serviceStart() {
137
+ if (process.platform === "linux")
138
+ return runSystemctl(["start", "wand.service"], "已启动");
139
+ if (process.platform === "darwin")
140
+ return launchctlLoad();
141
+ return unsupported();
142
+ }
143
+ export function serviceStop() {
144
+ if (process.platform === "linux")
145
+ return runSystemctl(["stop", "wand.service"], "已停止");
146
+ if (process.platform === "darwin")
147
+ return launchctlUnload();
148
+ return unsupported();
149
+ }
150
+ export function serviceRestart() {
151
+ if (process.platform === "linux")
152
+ return runSystemctl(["restart", "wand.service"], "已重启");
153
+ if (process.platform === "darwin")
154
+ return launchdRestart();
155
+ return unsupported();
156
+ }
157
+ /** 取最近 N 行服务日志。 */
158
+ export function serviceLogs(lines = 80) {
159
+ if (process.platform === "linux") {
160
+ const r = spawnSync("journalctl", ["--user", "-u", "wand.service", "-n", String(lines), "--no-pager"], { encoding: "utf8", timeout: 10_000 });
161
+ if (r.status === 0)
162
+ return { ok: true, message: `journalctl 输出 ${lines} 行`, detail: r.stdout.trim() };
163
+ return { ok: false, message: "journalctl 调用失败", detail: r.stderr.trim() || `exit ${r.status}` };
164
+ }
165
+ if (process.platform === "darwin") {
166
+ return {
167
+ ok: false,
168
+ message: "launchd 不直接写日志,请用 Console.app 或在 plist 里配置 StandardOutPath",
169
+ };
170
+ }
171
+ return unsupported();
172
+ }
173
+ function systemdStatus() {
174
+ const installed = isServiceInstalled();
175
+ if (!installed) {
176
+ return {
177
+ installed: false,
178
+ state: "unknown",
179
+ description: "未安装 (按 i 注册)",
180
+ raw: "",
181
+ platform: "linux",
182
+ };
183
+ }
184
+ // 用 `systemctl --user is-active` + `show -p ActiveState,SubState,ActiveEnterTimestamp` 拿结构化数据
185
+ const active = spawnSync("systemctl", ["--user", "is-active", "wand.service"], { encoding: "utf8" });
186
+ const stateRaw = (active.stdout || "").trim();
187
+ const show = spawnSync("systemctl", [
188
+ "--user",
189
+ "show",
190
+ "wand.service",
191
+ "-p",
192
+ "ActiveState",
193
+ "-p",
194
+ "SubState",
195
+ "-p",
196
+ "ActiveEnterTimestamp",
197
+ "-p",
198
+ "MainPID",
199
+ ], { encoding: "utf8" });
200
+ const props = parseSystemctlShow(show.stdout || "");
201
+ const status = spawnSync("systemctl", ["--user", "status", "wand.service", "--no-pager", "-n", "5"], {
202
+ encoding: "utf8",
203
+ });
204
+ const sub = props.SubState || stateRaw;
205
+ const since = props.ActiveEnterTimestamp ? ` since ${props.ActiveEnterTimestamp}` : "";
206
+ const pid = props.MainPID && props.MainPID !== "0" ? ` · PID ${props.MainPID}` : "";
207
+ const desc = `${stateRaw}${sub ? ` (${sub})` : ""}${since}${pid}`;
208
+ let normalized = "unknown";
209
+ if (stateRaw === "active")
210
+ normalized = "active";
211
+ else if (stateRaw === "inactive")
212
+ normalized = "inactive";
213
+ else if (stateRaw === "failed")
214
+ normalized = "failed";
215
+ else if (stateRaw === "activating" || stateRaw === "reloading")
216
+ normalized = "active";
217
+ return {
218
+ installed: true,
219
+ state: normalized,
220
+ description: desc,
221
+ raw: status.stdout || status.stderr || "",
222
+ platform: "linux",
223
+ };
224
+ }
225
+ function launchdStatus() {
226
+ const installed = isServiceInstalled();
227
+ if (!installed) {
228
+ return {
229
+ installed: false,
230
+ state: "unknown",
231
+ description: "未安装 (按 i 注册)",
232
+ raw: "",
233
+ platform: "darwin",
234
+ };
235
+ }
236
+ // launchctl list 输出三列:PID Status Label
237
+ const list = spawnSync("launchctl", ["list", "com.wand.web"], { encoding: "utf8" });
238
+ if (list.status !== 0) {
239
+ return {
240
+ installed: true,
241
+ state: "inactive",
242
+ description: "loaded 但未在运行(launchctl list 找不到)",
243
+ raw: list.stderr || "",
244
+ platform: "darwin",
245
+ };
246
+ }
247
+ // launchctl list <label> 给出多行 plist 格式:包含 PID / LastExitStatus
248
+ const text = list.stdout;
249
+ const pidMatch = text.match(/"PID"\s*=\s*(\d+);/);
250
+ const exitMatch = text.match(/"LastExitStatus"\s*=\s*(-?\d+);/);
251
+ const pid = pidMatch ? Number(pidMatch[1]) : 0;
252
+ const lastExit = exitMatch ? Number(exitMatch[1]) : 0;
253
+ const desc = pid > 0 ? `running · PID ${pid}` : `stopped (last exit=${lastExit})`;
254
+ return {
255
+ installed: true,
256
+ state: pid > 0 ? "active" : "inactive",
257
+ description: desc,
258
+ raw: text,
259
+ platform: "darwin",
260
+ };
261
+ }
262
+ function runSystemctl(args, successWord) {
263
+ const r = spawnSync("systemctl", ["--user", ...args], { encoding: "utf8", timeout: 15_000 });
264
+ if (r.status === 0) {
265
+ return { ok: true, message: `systemctl --user ${args.join(" ")} ${successWord}` };
266
+ }
267
+ return {
268
+ ok: false,
269
+ message: `systemctl 失败 (exit ${r.status})`,
270
+ detail: ((r.stdout || "") + "\n" + (r.stderr || "")).trim(),
271
+ };
272
+ }
273
+ function launchctlLoad() {
274
+ const plist = servicePath();
275
+ if (!existsSync(plist))
276
+ return { ok: false, message: "未安装 (plist 不存在)" };
277
+ const r = spawnSync("launchctl", ["load", "-w", plist], { encoding: "utf8", timeout: 10_000 });
278
+ if (r.status === 0)
279
+ return { ok: true, message: "已 launchctl load" };
280
+ return {
281
+ ok: false,
282
+ message: `launchctl load 失败 (exit ${r.status})`,
283
+ detail: ((r.stdout || "") + "\n" + (r.stderr || "")).trim(),
284
+ };
285
+ }
286
+ function launchctlUnload() {
287
+ const plist = servicePath();
288
+ if (!existsSync(plist))
289
+ return { ok: false, message: "未安装 (plist 不存在)" };
290
+ const r = spawnSync("launchctl", ["unload", plist], { encoding: "utf8", timeout: 10_000 });
291
+ if (r.status === 0)
292
+ return { ok: true, message: "已 launchctl unload" };
293
+ return {
294
+ ok: false,
295
+ message: `launchctl unload 失败 (exit ${r.status})`,
296
+ detail: ((r.stdout || "") + "\n" + (r.stderr || "")).trim(),
297
+ };
298
+ }
299
+ function launchdRestart() {
300
+ const stop = launchctlUnload();
301
+ const start = launchctlLoad();
302
+ if (stop.ok && start.ok)
303
+ return { ok: true, message: "已 launchd 重启" };
304
+ return {
305
+ ok: false,
306
+ message: "launchd 重启失败",
307
+ detail: `unload: ${stop.message}\nload: ${start.message}`,
308
+ };
309
+ }
310
+ function unsupported() {
311
+ return { ok: false, message: `当前平台 ${process.platform} 不支持服务管理` };
312
+ }
313
+ function parseSystemctlShow(text) {
314
+ const out = {};
315
+ for (const line of text.split("\n")) {
316
+ const eq = line.indexOf("=");
317
+ if (eq <= 0)
318
+ continue;
319
+ out[line.slice(0, eq)] = line.slice(eq + 1).trim();
320
+ }
321
+ return out;
322
+ }
323
+ export function installService(ctx) {
324
+ if (process.platform === "linux")
325
+ return installSystemdUserService(ctx);
326
+ if (process.platform === "darwin")
327
+ return installLaunchdAgent(ctx);
328
+ return { ok: false, message: `当前平台 ${process.platform} 暂不支持服务注册` };
329
+ }
330
+ export function uninstallService() {
331
+ if (process.platform === "linux")
332
+ return uninstallSystemdUserService();
333
+ if (process.platform === "darwin")
334
+ return uninstallLaunchdAgent();
335
+ return { ok: false, message: `当前平台 ${process.platform} 暂不支持服务注册` };
336
+ }
337
+ function servicePath() {
338
+ if (process.platform === "linux") {
339
+ return path.join(os.homedir(), ".config/systemd/user/wand.service");
340
+ }
341
+ if (process.platform === "darwin") {
342
+ return path.join(os.homedir(), "Library/LaunchAgents/com.wand.web.plist");
343
+ }
344
+ return "";
345
+ }
346
+ function resolveWandBin(ctx) {
347
+ if (ctx.wandBin && existsSync(ctx.wandBin))
348
+ return ctx.wandBin;
349
+ const argv1 = process.argv[1];
350
+ if (argv1 && existsSync(argv1))
351
+ return argv1;
352
+ const which = spawnSync("which", ["wand"], { encoding: "utf8" });
353
+ if (which.status === 0 && which.stdout)
354
+ return which.stdout.trim();
355
+ return "wand";
356
+ }
357
+ function installSystemdUserService(ctx) {
358
+ const unitPath = servicePath();
359
+ const wandBin = resolveWandBin(ctx);
360
+ const nodeBin = process.execPath;
361
+ const unit = [
362
+ "[Unit]",
363
+ "Description=wand web console",
364
+ "After=network.target",
365
+ "",
366
+ "[Service]",
367
+ "Type=simple",
368
+ `ExecStart=${nodeBin} ${wandBin} web -c ${ctx.configPath}`,
369
+ `Environment=WAND_NO_TUI=1`,
370
+ "Restart=on-failure",
371
+ "RestartSec=3",
372
+ "",
373
+ "[Install]",
374
+ "WantedBy=default.target",
375
+ "",
376
+ ].join("\n");
377
+ try {
378
+ mkdirSync(path.dirname(unitPath), { recursive: true });
379
+ writeFileSync(unitPath, unit, "utf8");
380
+ }
381
+ catch (err) {
382
+ return { ok: false, message: `写入 unit 失败: ${errMsg(err)}` };
383
+ }
384
+ const reload = spawnSync("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
385
+ const enable = spawnSync("systemctl", ["--user", "enable", "--now", "wand.service"], { encoding: "utf8" });
386
+ const detail = [
387
+ `unit: ${unitPath}`,
388
+ `daemon-reload: ${reload.status === 0 ? "ok" : `failed (${reload.stderr.trim()})`}`,
389
+ `enable --now: ${enable.status === 0 ? "ok" : `failed (${enable.stderr.trim()})`}`,
390
+ "",
391
+ "提示: 若需开机自启请运行 `loginctl enable-linger $USER`",
392
+ ].join("\n");
393
+ if (enable.status !== 0) {
394
+ return {
395
+ ok: false,
396
+ message: "已写入 unit,但 systemctl 启用失败",
397
+ detail,
398
+ };
399
+ }
400
+ return {
401
+ ok: true,
402
+ message: `已注册 systemd 用户服务: ${unitPath}`,
403
+ detail,
404
+ };
405
+ }
406
+ function uninstallSystemdUserService() {
407
+ const unitPath = servicePath();
408
+ if (!existsSync(unitPath)) {
409
+ return { ok: false, message: "未检测到已安装的 systemd 用户服务" };
410
+ }
411
+ const stop = spawnSync("systemctl", ["--user", "disable", "--now", "wand.service"], { encoding: "utf8" });
412
+ try {
413
+ unlinkSync(unitPath);
414
+ }
415
+ catch (err) {
416
+ return { ok: false, message: `删除 unit 失败: ${errMsg(err)}` };
417
+ }
418
+ spawnSync("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
419
+ return {
420
+ ok: true,
421
+ message: "已卸载 systemd 用户服务",
422
+ detail: stop.status === 0 ? "disable --now: ok" : `disable --now: ${stop.stderr.trim()}`,
423
+ };
424
+ }
425
+ function installLaunchdAgent(ctx) {
426
+ const plistPath = servicePath();
427
+ const wandBin = resolveWandBin(ctx);
428
+ const nodeBin = process.execPath;
429
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
430
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
431
+ <plist version="1.0">
432
+ <dict>
433
+ <key>Label</key><string>com.wand.web</string>
434
+ <key>ProgramArguments</key>
435
+ <array>
436
+ <string>${nodeBin}</string>
437
+ <string>${wandBin}</string>
438
+ <string>web</string>
439
+ <string>-c</string>
440
+ <string>${ctx.configPath}</string>
441
+ </array>
442
+ <key>EnvironmentVariables</key>
443
+ <dict>
444
+ <key>WAND_NO_TUI</key><string>1</string>
445
+ </dict>
446
+ <key>RunAtLoad</key><true/>
447
+ <key>KeepAlive</key><true/>
448
+ </dict>
449
+ </plist>
450
+ `;
451
+ try {
452
+ mkdirSync(path.dirname(plistPath), { recursive: true });
453
+ writeFileSync(plistPath, plist, "utf8");
454
+ }
455
+ catch (err) {
456
+ return { ok: false, message: `写入 plist 失败: ${errMsg(err)}` };
457
+ }
458
+ const load = spawnSync("launchctl", ["load", "-w", plistPath], { encoding: "utf8" });
459
+ if (load.status !== 0) {
460
+ return {
461
+ ok: false,
462
+ message: "已写入 plist,但 launchctl load 失败",
463
+ detail: load.stderr.trim() || `exit ${load.status}`,
464
+ };
465
+ }
466
+ return {
467
+ ok: true,
468
+ message: `已注册 launchd 用户代理: ${plistPath}`,
469
+ };
470
+ }
471
+ function uninstallLaunchdAgent() {
472
+ const plistPath = servicePath();
473
+ if (!existsSync(plistPath)) {
474
+ return { ok: false, message: "未检测到已安装的 launchd 用户代理" };
475
+ }
476
+ const unload = spawnSync("launchctl", ["unload", "-w", plistPath], { encoding: "utf8" });
477
+ try {
478
+ unlinkSync(plistPath);
479
+ }
480
+ catch (err) {
481
+ return { ok: false, message: `删除 plist 失败: ${errMsg(err)}` };
482
+ }
483
+ return {
484
+ ok: true,
485
+ message: "已卸载 launchd 用户代理",
486
+ detail: unload.status === 0 ? "unload: ok" : `unload: ${unload.stderr.trim()}`,
487
+ };
488
+ }
489
+ // ─── 工具 ────────────────────────────────────────────────────────────────
490
+ function errMsg(err) {
491
+ return err instanceof Error ? err.message : String(err);
492
+ }
493
+ /** 简易语义化版本比较;返回正数 = a > b。 */
494
+ function compareSemver(a, b) {
495
+ const pa = a.replace(/^v/, "").split(/[.\-+]/).map((s) => Number.parseInt(s, 10));
496
+ const pb = b.replace(/^v/, "").split(/[.\-+]/).map((s) => Number.parseInt(s, 10));
497
+ const len = Math.max(pa.length, pb.length);
498
+ for (let i = 0; i < len; i++) {
499
+ const x = Number.isFinite(pa[i]) ? pa[i] : 0;
500
+ const y = Number.isFinite(pb[i]) ? pb[i] : 0;
501
+ if (x !== y)
502
+ return x - y;
503
+ }
504
+ return 0;
505
+ }