@co0ontty/wand 2.13.0 → 3.0.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.
- package/dist/build-info.json +3 -3
- package/dist/cert.js +0 -3
- package/dist/claude-pty-bridge.d.ts +1 -9
- package/dist/claude-pty-bridge.js +5 -22
- package/dist/middleware/path-safety.js +1 -4
- package/dist/npm-update-utils.d.ts +2 -1
- package/dist/npm-update-utils.js +8 -5
- package/dist/pidfile.d.ts +0 -2
- package/dist/pidfile.js +0 -18
- package/dist/process-manager.d.ts +1 -8
- package/dist/process-manager.js +3 -86
- package/dist/server-session-routes.d.ts +1 -3
- package/dist/server-session-routes.js +19 -32
- package/dist/server.js +5 -21
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +0 -13
- package/dist/structured-session-manager.d.ts +0 -1
- package/dist/structured-session-manager.js +0 -2
- package/dist/update-helper.js +31 -17
- package/dist/upload-routes.js +2 -4
- package/dist/version-utils.js +14 -10
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +0 -1
- package/dist/ws-broadcast.js +0 -2
- package/package.json +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import express from "express";
|
|
2
1
|
import { SessionInputError } from "./process-manager.js";
|
|
3
2
|
import { getDefaultModelForProvider, normalizeMode } from "./config.js";
|
|
4
3
|
import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
@@ -7,7 +6,6 @@ import { resolveSessionCwd } from "./session-cwd.js";
|
|
|
7
6
|
import { resolveCommitAiContext } from "./session-ai-context.js";
|
|
8
7
|
import { getGitStatus, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
|
|
9
8
|
import { getErrorMessage } from "./error-utils.js";
|
|
10
|
-
export { getErrorMessage };
|
|
11
9
|
export function parseSessionCreationOrigin(body) {
|
|
12
10
|
const rawSource = body?.sessionSource;
|
|
13
11
|
if (rawSource !== undefined && rawSource !== "interactive" && rawSource !== "automation" && rawSource !== "startup") {
|
|
@@ -97,10 +95,6 @@ function removeFromHiddenClaudeSessionIds(storage, ids) {
|
|
|
97
95
|
function getSessionById(processes, structured, id) {
|
|
98
96
|
return structured.get(id) ?? processes.get(id);
|
|
99
97
|
}
|
|
100
|
-
function listAllSessions(processes, structured) {
|
|
101
|
-
return [...structured.list(), ...processes.list()]
|
|
102
|
-
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
103
|
-
}
|
|
104
98
|
/** Lightweight session list — omits output and messages to reduce payload. */
|
|
105
99
|
function listAllSessionsSlim(processes, structured) {
|
|
106
100
|
return [...structured.listSlim(), ...processes.listSlim()]
|
|
@@ -115,7 +109,7 @@ function requireWorktreeSession(snapshot) {
|
|
|
115
109
|
}
|
|
116
110
|
return snapshot;
|
|
117
111
|
}
|
|
118
|
-
function buildWorktreeMergeInfo(current,
|
|
112
|
+
function buildWorktreeMergeInfo(current, info) {
|
|
119
113
|
return {
|
|
120
114
|
...(current.worktreeMergeInfo ?? null),
|
|
121
115
|
...(info ?? null),
|
|
@@ -124,7 +118,7 @@ function buildWorktreeMergeInfo(current, status, info) {
|
|
|
124
118
|
};
|
|
125
119
|
}
|
|
126
120
|
function saveWorktreeMergeState(storage, current, status, info) {
|
|
127
|
-
const mergedInfo = buildWorktreeMergeInfo(current,
|
|
121
|
+
const mergedInfo = buildWorktreeMergeInfo(current, info);
|
|
128
122
|
const updated = {
|
|
129
123
|
...current,
|
|
130
124
|
worktreeMergeStatus: status,
|
|
@@ -134,14 +128,7 @@ function saveWorktreeMergeState(storage, current, status, info) {
|
|
|
134
128
|
return updated;
|
|
135
129
|
}
|
|
136
130
|
function getWorktreeMergeResponseStatus(error) {
|
|
137
|
-
|
|
138
|
-
if (!code) {
|
|
139
|
-
return 400;
|
|
140
|
-
}
|
|
141
|
-
if (code === "WORKTREE_MERGE_CONFLICT") {
|
|
142
|
-
return 409;
|
|
143
|
-
}
|
|
144
|
-
return 400;
|
|
131
|
+
return getWorktreeMergeErrorCode(error) === "WORKTREE_MERGE_CONFLICT" ? 409 : 400;
|
|
145
132
|
}
|
|
146
133
|
function getWorktreeMergePayload(error, fallback) {
|
|
147
134
|
if (error instanceof WorktreeMergeError) {
|
|
@@ -244,7 +231,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
244
231
|
const all = listAllSessionsSlim(processes, structured);
|
|
245
232
|
res.json(all);
|
|
246
233
|
});
|
|
247
|
-
app.post("/api/structured-sessions",
|
|
234
|
+
app.post("/api/structured-sessions", async (req, res) => {
|
|
248
235
|
const body = req.body;
|
|
249
236
|
try {
|
|
250
237
|
if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode") {
|
|
@@ -279,7 +266,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
279
266
|
res.status(400).json({ error: getErrorMessage(error, "无法启动结构化会话。") });
|
|
280
267
|
}
|
|
281
268
|
});
|
|
282
|
-
app.post("/api/sessions/:id/model",
|
|
269
|
+
app.post("/api/sessions/:id/model", (req, res) => {
|
|
283
270
|
const body = req.body;
|
|
284
271
|
const rawModel = typeof body?.model === "string" ? body.model.trim() : null;
|
|
285
272
|
const id = req.params.id;
|
|
@@ -304,7 +291,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
304
291
|
});
|
|
305
292
|
// 思考深度切换:与 /model 路由对称。结构化会话立即影响下一条 prompt(CLI/SDK 各自接入
|
|
306
293
|
// --effort / thinking budget),PTY 会话通过 /effort 更新当前 Claude 进程。
|
|
307
|
-
app.post("/api/sessions/:id/thinking-effort",
|
|
294
|
+
app.post("/api/sessions/:id/thinking-effort", (req, res) => {
|
|
308
295
|
const body = req.body;
|
|
309
296
|
const raw = typeof body?.thinkingEffort === "string" ? body.thinkingEffort : null;
|
|
310
297
|
const id = req.params.id;
|
|
@@ -328,7 +315,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
328
315
|
// 执行模式切换:与 /model、/thinking-effort 路由对称。结构化会话立即影响下一条
|
|
329
316
|
// prompt(权限策略 / 系统提示 / CLI flag 都按 session.mode 逐轮重新派生);PTY 会话
|
|
330
317
|
// 仅更新 wand 自身的权限自动放行判定,已启动的 claude 进程命令行 flag 不变。codex 锁 full-access。
|
|
331
|
-
app.post("/api/sessions/:id/mode",
|
|
318
|
+
app.post("/api/sessions/:id/mode", (req, res) => {
|
|
332
319
|
const body = req.body;
|
|
333
320
|
const raw = typeof body?.mode === "string" ? body.mode.trim() : "";
|
|
334
321
|
if (!raw) {
|
|
@@ -365,7 +352,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
365
352
|
}
|
|
366
353
|
res.json({ id: snapshot.id, messages: snapshot.messages ?? [] });
|
|
367
354
|
});
|
|
368
|
-
app.post("/api/structured-sessions/:id/messages",
|
|
355
|
+
app.post("/api/structured-sessions/:id/messages", async (req, res) => {
|
|
369
356
|
const input = String(req.body?.input ?? "");
|
|
370
357
|
const interrupt = !!req.body?.interrupt;
|
|
371
358
|
// preserveQueue: 仅在 interrupt 路径有意义。排队条「立即」会带这个 flag,
|
|
@@ -388,7 +375,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
388
375
|
// ── Structured queued-messages management ──
|
|
389
376
|
// 这些端点构成"排队消息条"的后端操作面:reorder、立即发送、单条删除、全部清空。
|
|
390
377
|
// 全部走乐观更新模型,失败时前端会回滚到上一次 WS 推送的 queuedMessages。
|
|
391
|
-
app.patch("/api/structured-sessions/:id/queued",
|
|
378
|
+
app.patch("/api/structured-sessions/:id/queued", (req, res) => {
|
|
392
379
|
const rawOrder = req.body?.order;
|
|
393
380
|
if (!Array.isArray(rawOrder)) {
|
|
394
381
|
res.status(400).json({ error: "缺少 order 数组。" });
|
|
@@ -416,7 +403,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
416
403
|
res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
|
|
417
404
|
}
|
|
418
405
|
});
|
|
419
|
-
app.post("/api/structured-sessions/:id/queued/:index/promote",
|
|
406
|
+
app.post("/api/structured-sessions/:id/queued/:index/promote", async (req, res) => {
|
|
420
407
|
const index = Number(req.params.index);
|
|
421
408
|
if (!Number.isInteger(index)) {
|
|
422
409
|
res.status(400).json({ error: "下标无效。" });
|
|
@@ -498,7 +485,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
498
485
|
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法检查 worktree 合并状态。"));
|
|
499
486
|
}
|
|
500
487
|
});
|
|
501
|
-
app.post("/api/sessions/:id/worktree/merge",
|
|
488
|
+
app.post("/api/sessions/:id/worktree/merge", (req, res) => {
|
|
502
489
|
try {
|
|
503
490
|
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
504
491
|
if (!isMergeActionAllowed(current)) {
|
|
@@ -558,7 +545,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
558
545
|
res.json({ isGit: false, error: getErrorMessage(error, "无法读取 git 状态。") });
|
|
559
546
|
}
|
|
560
547
|
});
|
|
561
|
-
app.post("/api/sessions/:id/quick-commit",
|
|
548
|
+
app.post("/api/sessions/:id/quick-commit", async (req, res) => {
|
|
562
549
|
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
563
550
|
if (!snapshot) {
|
|
564
551
|
res.status(404).json({ error: "未找到该会话。" });
|
|
@@ -593,7 +580,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
593
580
|
res.status(400).json({ error: getErrorMessage(error, "快捷提交失败。") });
|
|
594
581
|
}
|
|
595
582
|
});
|
|
596
|
-
app.post("/api/sessions/:id/generate-commit-message",
|
|
583
|
+
app.post("/api/sessions/:id/generate-commit-message", async (req, res) => {
|
|
597
584
|
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
598
585
|
if (!snapshot) {
|
|
599
586
|
res.status(404).json({ error: "未找到该会话。" });
|
|
@@ -618,7 +605,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
618
605
|
res.status(400).json({ error: getErrorMessage(error, "生成 commit message 失败。") });
|
|
619
606
|
}
|
|
620
607
|
});
|
|
621
|
-
app.post("/api/sessions/:id/git/tag-head",
|
|
608
|
+
app.post("/api/sessions/:id/git/tag-head", async (req, res) => {
|
|
622
609
|
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
623
610
|
if (!snapshot) {
|
|
624
611
|
res.status(404).json({ error: "未找到该会话。" });
|
|
@@ -650,7 +637,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
650
637
|
res.status(400).json({ error: getErrorMessage(error, "打 tag 失败。") });
|
|
651
638
|
}
|
|
652
639
|
});
|
|
653
|
-
app.post("/api/sessions/:id/git/push",
|
|
640
|
+
app.post("/api/sessions/:id/git/push", async (req, res) => {
|
|
654
641
|
const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
655
642
|
if (!snapshot) {
|
|
656
643
|
res.status(404).json({ error: "未找到该会话。" });
|
|
@@ -699,7 +686,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
699
686
|
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法清理 worktree。"));
|
|
700
687
|
}
|
|
701
688
|
});
|
|
702
|
-
app.post("/api/sessions/batch-delete",
|
|
689
|
+
app.post("/api/sessions/batch-delete", (req, res) => {
|
|
703
690
|
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
704
691
|
? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
705
692
|
: [];
|
|
@@ -923,7 +910,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
923
910
|
res.status(response.statusCode).json(response.payload);
|
|
924
911
|
}
|
|
925
912
|
});
|
|
926
|
-
app.post("/api/codex-sessions/:threadId/resume",
|
|
913
|
+
app.post("/api/codex-sessions/:threadId/resume", async (req, res) => {
|
|
927
914
|
const threadId = String(req.params.threadId || "").trim();
|
|
928
915
|
const body = req.body;
|
|
929
916
|
try {
|
|
@@ -1121,7 +1108,7 @@ export function registerClaudeHistoryRoutes(app, processes, storage) {
|
|
|
1121
1108
|
res.status(500).json({ error: getErrorMessage(error, "无法删除该目录下的历史会话。") });
|
|
1122
1109
|
}
|
|
1123
1110
|
});
|
|
1124
|
-
app.post("/api/claude-history/batch-delete",
|
|
1111
|
+
app.post("/api/claude-history/batch-delete", (req, res) => {
|
|
1125
1112
|
const claudeSessionIds = Array.isArray(req.body?.claudeSessionIds)
|
|
1126
1113
|
? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1127
1114
|
: [];
|
|
@@ -1204,7 +1191,7 @@ export function registerClaudeHistoryRoutes(app, processes, storage) {
|
|
|
1204
1191
|
}
|
|
1205
1192
|
res.json({ ok: true });
|
|
1206
1193
|
});
|
|
1207
|
-
app.post("/api/codex-history/batch-delete",
|
|
1194
|
+
app.post("/api/codex-history/batch-delete", (req, res) => {
|
|
1208
1195
|
const threadIds = Array.isArray(req.body?.claudeSessionIds)
|
|
1209
1196
|
? req.body.claudeSessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1210
1197
|
: [];
|
package/dist/server.js
CHANGED
|
@@ -20,7 +20,8 @@ import { getCachedModels, refreshModels } from "./models.js";
|
|
|
20
20
|
import { ProcessManager } from "./process-manager.js";
|
|
21
21
|
import { SessionLogger } from "./session-logger.js";
|
|
22
22
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
23
|
-
import {
|
|
23
|
+
import { parseSessionCreationOrigin, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
24
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
24
25
|
import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
|
|
25
26
|
import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
|
|
26
27
|
import { computeRelaunch } from "./relaunch.js";
|
|
@@ -604,22 +605,6 @@ function resolveAppConnectOrigin(origin, config) {
|
|
|
604
605
|
return origin;
|
|
605
606
|
}
|
|
606
607
|
}
|
|
607
|
-
function decodeConnectCode(code) {
|
|
608
|
-
try {
|
|
609
|
-
const decoded = Buffer.from(code, "base64").toString("utf8");
|
|
610
|
-
const hashIdx = decoded.lastIndexOf("#");
|
|
611
|
-
if (hashIdx < 1)
|
|
612
|
-
return null;
|
|
613
|
-
const url = decoded.substring(0, hashIdx);
|
|
614
|
-
const token = decoded.substring(hashIdx + 1);
|
|
615
|
-
if (!url.startsWith("http") || token.length < 16)
|
|
616
|
-
return null;
|
|
617
|
-
return { url, token };
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
return null;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
608
|
/** Match a semver-looking token in a file name (with optional pre-release / build metadata). */
|
|
624
609
|
function resolveAndroidApkDir(configDir, config) {
|
|
625
610
|
const configuredDir = config.android?.apkDir?.trim();
|
|
@@ -1121,7 +1106,6 @@ export async function startServer(config, configPath) {
|
|
|
1121
1106
|
const useHttps = config.https === true;
|
|
1122
1107
|
const protocol = useHttps ? "https" : "http";
|
|
1123
1108
|
const requireAuth = buildRequireAuth(useHttps, storage, config);
|
|
1124
|
-
const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
|
|
1125
1109
|
app.use(express.json({ limit: "1mb" }));
|
|
1126
1110
|
app.use(compression({ threshold: 1024 }));
|
|
1127
1111
|
app.use((req, res, next) => {
|
|
@@ -1827,7 +1811,7 @@ export async function startServer(config, configPath) {
|
|
|
1827
1811
|
res.status(500).json({ error: getErrorMessage(error, "检查 CLI 更新失败。") });
|
|
1828
1812
|
}
|
|
1829
1813
|
});
|
|
1830
|
-
app.post("/api/provider-cli-updates",
|
|
1814
|
+
app.post("/api/provider-cli-updates", async (req, res) => {
|
|
1831
1815
|
if (providerCliUpdateInFlight || updateInFlight) {
|
|
1832
1816
|
res.status(409).json({ error: "CLI 更新正在进行中,请稍候。" });
|
|
1833
1817
|
return;
|
|
@@ -2616,7 +2600,7 @@ export async function startServer(config, configPath) {
|
|
|
2616
2600
|
const cli = storage.getConfigValue("autoUpdateProviderClis") === "true";
|
|
2617
2601
|
res.json({ web, apk, dmg, cli });
|
|
2618
2602
|
});
|
|
2619
|
-
app.post("/api/auto-update",
|
|
2603
|
+
app.post("/api/auto-update", (req, res) => {
|
|
2620
2604
|
const { web, apk, dmg, cli } = req.body;
|
|
2621
2605
|
if (typeof web === "boolean") {
|
|
2622
2606
|
storage.setConfigValue("autoUpdateWeb", String(web));
|
|
@@ -2649,7 +2633,7 @@ export async function startServer(config, configPath) {
|
|
|
2649
2633
|
},
|
|
2650
2634
|
});
|
|
2651
2635
|
});
|
|
2652
|
-
app.post("/api/update-channel",
|
|
2636
|
+
app.post("/api/update-channel", (req, res) => {
|
|
2653
2637
|
const body = (req.body ?? {});
|
|
2654
2638
|
const channel = body.channel === "beta" ? "beta" : "stable";
|
|
2655
2639
|
storage.setConfigValue("updateChannel", channel);
|
package/dist/storage.d.ts
CHANGED
|
@@ -58,6 +58,5 @@ export declare class WandStorage {
|
|
|
58
58
|
getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
|
|
59
59
|
loadSessions(): SessionSnapshot[];
|
|
60
60
|
private mapSessionRow;
|
|
61
|
-
updateSessionWorktreeMergeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
|
|
62
61
|
deleteSession(id: string): void;
|
|
63
62
|
}
|
package/dist/storage.js
CHANGED
|
@@ -577,19 +577,6 @@ export class WandStorage {
|
|
|
577
577
|
mapSessionRow(row) {
|
|
578
578
|
return mapSessionCore(row);
|
|
579
579
|
}
|
|
580
|
-
updateSessionWorktreeMergeState(id, status, info) {
|
|
581
|
-
const current = this.getSession(id);
|
|
582
|
-
if (!current) {
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
const updated = {
|
|
586
|
-
...current,
|
|
587
|
-
worktreeMergeStatus: status,
|
|
588
|
-
worktreeMergeInfo: info,
|
|
589
|
-
};
|
|
590
|
-
this.saveSessionMetadata(updated);
|
|
591
|
-
return updated;
|
|
592
|
-
}
|
|
593
580
|
deleteSession(id) {
|
|
594
581
|
this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
|
|
595
582
|
}
|
|
@@ -925,7 +925,6 @@ export class StructuredSessionManager {
|
|
|
925
925
|
createSession(options) {
|
|
926
926
|
const id = randomUUID();
|
|
927
927
|
const startedAt = new Date().toISOString();
|
|
928
|
-
const prompt = options.prompt?.trim();
|
|
929
928
|
const provider = options.provider === "codex" || options.provider === "opencode" ? options.provider : "claude";
|
|
930
929
|
const runner = options.runner ?? defaultStructuredRunner(provider);
|
|
931
930
|
const baseCwd = resolveSessionCwd(options.cwd, this.config.defaultCwd);
|
|
@@ -3179,7 +3178,6 @@ export class StructuredSessionManager {
|
|
|
3179
3178
|
killedForAskUserQuestion,
|
|
3180
3179
|
sessionId: turnState.sessionId,
|
|
3181
3180
|
});
|
|
3182
|
-
const interruptedByUser = this.interruptedWith.has(sessionId);
|
|
3183
3181
|
const msgs = this.buildCompletedAssistantMessages(current, turnState);
|
|
3184
3182
|
const interruptPrompt = this.interruptedWith.get(sessionId);
|
|
3185
3183
|
const keepRunning = killedForAskUserQuestion || !!interruptPrompt;
|
package/dist/update-helper.js
CHANGED
|
@@ -195,6 +195,7 @@ SERVICE_SCOPE=${shellQuote(serviceScope ?? "")}
|
|
|
195
195
|
TIMEOUT_MS=${timeoutMs}
|
|
196
196
|
NODE_BIN=${shellQuote(process.execPath)}
|
|
197
197
|
UPDATE_UTILS=${shellQuote(updateUtilsPath)}
|
|
198
|
+
GLOBAL_CLI_PATH_FILE=${shellQuote(`${logPath}.cli-path`)}
|
|
198
199
|
WORKING_DIRECTORY=${shellQuote(opts.cwd)}
|
|
199
200
|
CLI_ARGS=(${shellArray(opts.cliArgs)})
|
|
200
201
|
GLOBAL_CLI=""
|
|
@@ -207,6 +208,7 @@ run() {
|
|
|
207
208
|
|
|
208
209
|
on_exit() {
|
|
209
210
|
local status="$?"
|
|
211
|
+
rm -f "$GLOBAL_CLI_PATH_FILE" 2>/dev/null || true
|
|
210
212
|
if [ "$status" -ne 0 ]; then
|
|
211
213
|
if [ "$PARENT_EXITED" = "0" ]; then
|
|
212
214
|
echo "[wand-update] failed with exit code $status; parent service was left running"
|
|
@@ -230,22 +232,13 @@ wait_for_parent_exit() {
|
|
|
230
232
|
done
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
resolve_global_cli() {
|
|
234
|
-
local npm_root
|
|
235
|
-
npm_root="$(npm root -g)"
|
|
236
|
-
GLOBAL_CLI="$npm_root/@co0ontty/wand/dist/cli.js"
|
|
237
|
-
if [ ! -f "$GLOBAL_CLI" ]; then
|
|
238
|
-
echo "[wand-update] global CLI not found after install: $GLOBAL_CLI"
|
|
239
|
-
return 1
|
|
240
|
-
fi
|
|
241
|
-
}
|
|
242
|
-
|
|
243
235
|
main() {
|
|
244
236
|
echo "[wand-update] installing while parent service stays online"
|
|
245
|
-
if run "$NODE_BIN" ${nodeLoaderCommand}--input-type=module - "$UPDATE_UTILS" "$INSTALL_SPEC" "$TIMEOUT_MS" <<'WAND_INSTALL_NODE'
|
|
237
|
+
if run "$NODE_BIN" ${nodeLoaderCommand}--input-type=module - "$UPDATE_UTILS" "$INSTALL_SPEC" "$TIMEOUT_MS" "$GLOBAL_CLI_PATH_FILE" <<'WAND_INSTALL_NODE'
|
|
238
|
+
import { realpathSync, statSync, writeFileSync } from "node:fs";
|
|
246
239
|
import { pathToFileURL } from "node:url";
|
|
247
240
|
|
|
248
|
-
const [modulePath, installSpec, timeoutValue] = process.argv.slice(2);
|
|
241
|
+
const [modulePath, installSpec, timeoutValue, cliPathFile] = process.argv.slice(2);
|
|
249
242
|
const timeoutMs = Number(timeoutValue);
|
|
250
243
|
const updateUtils = await import(pathToFileURL(modulePath).href);
|
|
251
244
|
const installPackageGloballyAsync = updateUtils.installPackageGloballyAsync
|
|
@@ -253,19 +246,37 @@ const installPackageGloballyAsync = updateUtils.installPackageGloballyAsync
|
|
|
253
246
|
if (typeof installPackageGloballyAsync !== "function") {
|
|
254
247
|
throw new Error("installPackageGloballyAsync is unavailable in " + modulePath);
|
|
255
248
|
}
|
|
256
|
-
await installPackageGloballyAsync(installSpec, timeoutMs, (line) => {
|
|
249
|
+
const installedCli = await installPackageGloballyAsync(installSpec, timeoutMs, (line) => {
|
|
257
250
|
process.stdout.write(String(line) + "\\n");
|
|
258
251
|
});
|
|
252
|
+
if (typeof installedCli !== "string" || installedCli.trim().length === 0) {
|
|
253
|
+
throw new Error("installer did not return the installed Wand CLI path");
|
|
254
|
+
}
|
|
255
|
+
const resolvedCli = realpathSync(installedCli.trim());
|
|
256
|
+
if (!statSync(resolvedCli).isFile()) {
|
|
257
|
+
throw new Error("installed Wand CLI is not a file: " + resolvedCli);
|
|
258
|
+
}
|
|
259
|
+
writeFileSync(cliPathFile, resolvedCli, { encoding: "utf8", mode: 0o600 });
|
|
259
260
|
WAND_INSTALL_NODE
|
|
260
261
|
then
|
|
261
262
|
echo "[wand-update] install completed"
|
|
262
263
|
else
|
|
263
264
|
local install_status="$?"
|
|
264
|
-
echo "[wand-update] install failed; keeping parent service online"
|
|
265
|
+
echo "[wand-update] install or CLI resolution failed; keeping parent service online"
|
|
265
266
|
return "$install_status"
|
|
266
267
|
fi
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
if [ ! -f "$GLOBAL_CLI_PATH_FILE" ]; then
|
|
270
|
+
echo "[wand-update] installer did not persist the global CLI path"
|
|
271
|
+
return 1
|
|
272
|
+
fi
|
|
273
|
+
GLOBAL_CLI="$(<"$GLOBAL_CLI_PATH_FILE")"
|
|
274
|
+
rm -f "$GLOBAL_CLI_PATH_FILE"
|
|
275
|
+
if [ ! -f "$GLOBAL_CLI" ]; then
|
|
276
|
+
echo "[wand-update] global CLI not found after install: $GLOBAL_CLI"
|
|
277
|
+
return 1
|
|
278
|
+
fi
|
|
279
|
+
echo "[wand-update] resolved global CLI: $GLOBAL_CLI"
|
|
269
280
|
echo "[wand-update] initializing updated installation"
|
|
270
281
|
run "$NODE_BIN" "$GLOBAL_CLI" init -c "$CONFIG_PATH"
|
|
271
282
|
|
|
@@ -349,8 +360,11 @@ function spawnDetached(scriptPath, _logPath, env, serviceScope) {
|
|
|
349
360
|
}
|
|
350
361
|
}
|
|
351
362
|
if (process.platform === "darwin") {
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
// `detached: true` already creates an independent process group. Wrapping the
|
|
364
|
+
// helper in /usr/bin/nohup breaks when Wand itself is a launchd daemon:
|
|
365
|
+
// nohup exits immediately because it cannot adopt a Background session, while
|
|
366
|
+
// spawn() has already returned a PID and the API falsely reports success.
|
|
367
|
+
const result = trySpawn(resolveBashPath(), [scriptPath], "bash detached");
|
|
354
368
|
if (result)
|
|
355
369
|
return result;
|
|
356
370
|
}
|
package/dist/upload-routes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import multer from "multer";
|
|
@@ -16,9 +16,7 @@ export function registerUploadRoutes(app, processes) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const uploadDir = path.join(cwd, ".wand-uploads");
|
|
19
|
-
|
|
20
|
-
mkdirSync(uploadDir, { recursive: true });
|
|
21
|
-
}
|
|
19
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
22
20
|
cb(null, uploadDir);
|
|
23
21
|
},
|
|
24
22
|
filename(_req, file, cb) {
|
package/dist/version-utils.js
CHANGED
|
@@ -19,11 +19,9 @@ export function extractSemver(text) {
|
|
|
19
19
|
export function compareSemver(a, b) {
|
|
20
20
|
const pa = parseSemverLike(a);
|
|
21
21
|
const pb = parseSemverLike(b);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return diff;
|
|
26
|
-
}
|
|
22
|
+
const mainOrder = compareMainParts(pa.mainParts, pb.mainParts);
|
|
23
|
+
if (mainOrder !== 0)
|
|
24
|
+
return mainOrder;
|
|
27
25
|
// Main version equal — apply semver prerelease rule: no prerelease > with prerelease.
|
|
28
26
|
if (!pa.pre && pb.pre)
|
|
29
27
|
return 1;
|
|
@@ -80,17 +78,23 @@ function comparePrereleaseSegments(preA, preB) {
|
|
|
80
78
|
export function compareApkInstallOrder(a, b) {
|
|
81
79
|
const pa = parseSemverLike(a);
|
|
82
80
|
const pb = parseSemverLike(b);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return diff;
|
|
87
|
-
}
|
|
81
|
+
const mainOrder = compareMainParts(pa.mainParts, pb.mainParts);
|
|
82
|
+
if (mainOrder !== 0)
|
|
83
|
+
return mainOrder;
|
|
88
84
|
if (pa.isDebug !== pb.isDebug)
|
|
89
85
|
return pa.isDebug ? 1 : -1;
|
|
90
86
|
if (pa.isDebug && pb.isDebug)
|
|
91
87
|
return comparePrereleaseSegments(pa.pre, pb.pre);
|
|
92
88
|
return 0;
|
|
93
89
|
}
|
|
90
|
+
function compareMainParts(a, b) {
|
|
91
|
+
for (let i = 0; i < 3; i++) {
|
|
92
|
+
const diff = (a[i] || 0) - (b[i] || 0);
|
|
93
|
+
if (diff !== 0)
|
|
94
|
+
return diff;
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
94
98
|
function parseSemverLike(v) {
|
|
95
99
|
const withoutBuild = v.trim().replace(/^v/, "").split("+")[0] ?? "";
|
|
96
100
|
const [main, ...rest] = withoutBuild.split("-");
|