sessix-server 0.2.6 → 0.2.7

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/index.js CHANGED
@@ -46,7 +46,10 @@ var zh = {
46
46
  autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
47
47
  autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
48
48
  pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
49
+ pressT: " \u{1F511} \u6309 t \u91CD\u7F6E Token\uFF08\u6CC4\u9732\u540E\u5237\u65B0\uFF09",
49
50
  pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
51
+ tokenRegenerated: "\u{1F511} Token \u5DF2\u91CD\u7F6E\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u914D\u5BF9",
52
+ tokenRegenerateFailed: "Token \u91CD\u7F6E\u5931\u8D25:",
50
53
  updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
51
54
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
52
55
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
@@ -75,7 +78,8 @@ var zh = {
75
78
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
76
79
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
77
80
  activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
78
- noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
81
+ noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B",
82
+ tokenRegenerated: "Token \u5DF2\u91CD\u7F6E: {{token}}"
79
83
  },
80
84
  ws: {
81
85
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -152,7 +156,10 @@ var en = {
152
156
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
153
157
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
154
158
  pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
159
+ pressT: " \u{1F511} Press t to regenerate token (refresh after leak)",
155
160
  pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
161
+ tokenRegenerated: "\u{1F511} Token regenerated, all clients disconnected. Scan QR to re-pair",
162
+ tokenRegenerateFailed: "Token regeneration failed:",
156
163
  updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
157
164
  receivedSignal: "Received {{signal}}, graceful shutdown...",
158
165
  goodbye: "All services closed, goodbye!",
@@ -181,7 +188,8 @@ var en = {
181
188
  activityPushEnabled: "ActivityKit Push enabled",
182
189
  activityPushFailed: "ActivityKit Push init failed:",
183
190
  activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
184
- noActiveLoginProcess: "No active login process"
191
+ noActiveLoginProcess: "No active login process",
192
+ tokenRegenerated: "Token regenerated: {{token}}"
185
193
  },
186
194
  ws: {
187
195
  started: "WebSocket server started on port {{port}}",
@@ -287,11 +295,11 @@ function t(key, params) {
287
295
  }
288
296
 
289
297
  // src/server.ts
290
- var import_uuid4 = require("uuid");
298
+ var import_uuid5 = require("uuid");
291
299
  var import_promises4 = require("fs/promises");
292
300
  var import_node_os6 = require("os");
293
301
  var import_node_path5 = require("path");
294
- var import_node_child_process4 = require("child_process");
302
+ var import_node_child_process5 = require("child_process");
295
303
  var import_node_util = require("util");
296
304
 
297
305
  // src/providers/ProcessProvider.ts
@@ -1538,6 +1546,13 @@ var WsBridge = class _WsBridge {
1538
1546
  getConnectionCount() {
1539
1547
  return this.wss.clients.size;
1540
1548
  }
1549
+ /** 更新 token 并断开所有现有连接(token 刷新后需重新配对) */
1550
+ updateToken(newToken) {
1551
+ this.token = newToken;
1552
+ for (const ws of this.wss.clients) {
1553
+ ws.close(4001, "Token regenerated");
1554
+ }
1555
+ }
1541
1556
  /** 优雅关闭 WebSocket 服务 */
1542
1557
  close() {
1543
1558
  return new Promise((resolve, reject) => {
@@ -1977,6 +1992,10 @@ var ApprovalProxy = class _ApprovalProxy {
1977
1992
  });
1978
1993
  }
1979
1994
  }
1995
+ /** 更新 token(token 刷新时调用) */
1996
+ updateToken(newToken) {
1997
+ this.token = newToken;
1998
+ }
1980
1999
  /** 返回连接 token(仅本机访问) */
1981
2000
  handleToken(req, res) {
1982
2001
  const remoteAddress = req.socket.remoteAddress;
@@ -3245,6 +3264,9 @@ var PairingManager = class {
3245
3264
  this.close();
3246
3265
  return result;
3247
3266
  }
3267
+ updateToken(newToken) {
3268
+ this.token = newToken;
3269
+ }
3248
3270
  getRemainingSeconds() {
3249
3271
  if (this._state !== "open") return 0;
3250
3272
  return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
@@ -3377,9 +3399,98 @@ var AuthManager = class extends import_events2.EventEmitter {
3377
3399
 
3378
3400
  // src/server.ts
3379
3401
  var import_promises5 = require("fs/promises");
3402
+
3403
+ // src/terminal/TerminalExecutor.ts
3404
+ var import_node_child_process4 = require("child_process");
3405
+ var import_uuid4 = require("uuid");
3406
+ var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
3407
+ var TerminalExecutor = class {
3408
+ processes = /* @__PURE__ */ new Map();
3409
+ eventCallbacks = [];
3410
+ onEvent(callback) {
3411
+ this.eventCallbacks.push(callback);
3412
+ return () => {
3413
+ const idx = this.eventCallbacks.indexOf(callback);
3414
+ if (idx !== -1) this.eventCallbacks.splice(idx, 1);
3415
+ };
3416
+ }
3417
+ emit(event) {
3418
+ for (const cb of this.eventCallbacks) {
3419
+ try {
3420
+ cb(event);
3421
+ } catch (err) {
3422
+ console.error("[TerminalExecutor] Event callback error:", err);
3423
+ }
3424
+ }
3425
+ }
3426
+ exec(sessionId, command, cwd) {
3427
+ const execId = (0, import_uuid4.v4)();
3428
+ const shell = isWindows ? "powershell" : "bash";
3429
+ const args = isWindows ? ["-Command", command] : ["-c", command];
3430
+ const proc = (0, import_node_child_process4.spawn)(shell, args, {
3431
+ cwd,
3432
+ stdio: ["ignore", "pipe", "pipe"],
3433
+ env: { ...process.env }
3434
+ });
3435
+ this.processes.set(execId, proc);
3436
+ proc.stdout?.on("data", (chunk) => {
3437
+ this.emit({
3438
+ type: "terminal_output",
3439
+ sessionId,
3440
+ execId,
3441
+ stream: "stdout",
3442
+ data: chunk.toString()
3443
+ });
3444
+ });
3445
+ proc.stderr?.on("data", (chunk) => {
3446
+ this.emit({
3447
+ type: "terminal_output",
3448
+ sessionId,
3449
+ execId,
3450
+ stream: "stderr",
3451
+ data: chunk.toString()
3452
+ });
3453
+ });
3454
+ proc.on("exit", (code, signal) => {
3455
+ clearTimeout(timer);
3456
+ this.processes.delete(execId);
3457
+ this.emit({
3458
+ type: "terminal_exit",
3459
+ sessionId,
3460
+ execId,
3461
+ code,
3462
+ signal
3463
+ });
3464
+ });
3465
+ const timer = setTimeout(() => {
3466
+ if (this.processes.has(execId)) {
3467
+ killProcessCrossPlatform(proc);
3468
+ }
3469
+ }, EXEC_TIMEOUT_MS);
3470
+ console.log(`[TerminalExecutor] exec ${execId}: ${command.substring(0, 100)} (cwd: ${cwd})`);
3471
+ return execId;
3472
+ }
3473
+ kill(execId) {
3474
+ const proc = this.processes.get(execId);
3475
+ if (proc) {
3476
+ killProcessCrossPlatform(proc);
3477
+ console.log(`[TerminalExecutor] kill ${execId}`);
3478
+ }
3479
+ }
3480
+ destroy() {
3481
+ for (const [execId, proc] of this.processes) {
3482
+ killProcessCrossPlatform(proc);
3483
+ console.log(`[TerminalExecutor] cleanup ${execId}`);
3484
+ }
3485
+ this.processes.clear();
3486
+ this.eventCallbacks.length = 0;
3487
+ }
3488
+ };
3489
+
3490
+ // src/server.ts
3380
3491
  var WS_PORT = 3745;
3381
3492
  var HTTP_PORT = 3746;
3382
- var execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
3493
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process5.exec);
3383
3494
  async function killPortProcess(port) {
3384
3495
  try {
3385
3496
  if (isWindows) {
@@ -3434,7 +3545,7 @@ async function start(opts = {}) {
3434
3545
  try {
3435
3546
  token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
3436
3547
  } catch {
3437
- token = (0, import_uuid4.v4)();
3548
+ token = (0, import_uuid5.v4)();
3438
3549
  await (0, import_promises4.mkdir)(configDir, { recursive: true });
3439
3550
  await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
3440
3551
  }
@@ -3442,6 +3553,20 @@ async function start(opts = {}) {
3442
3553
  }
3443
3554
  const provider = new ProcessProvider();
3444
3555
  const sessionManager = new SessionManager(provider);
3556
+ const terminalExecutor = new TerminalExecutor();
3557
+ const wsBridge = await createWithRetry(
3558
+ "WsBridge",
3559
+ WS_PORT,
3560
+ () => WsBridge.create({ port: WS_PORT, token })
3561
+ );
3562
+ const unreadSessionIds = /* @__PURE__ */ new Set();
3563
+ sessionManager.onEvent((event) => {
3564
+ if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3565
+ if (!wsBridge.isViewingSession(event.sessionId)) {
3566
+ unreadSessionIds.add(event.sessionId);
3567
+ }
3568
+ }
3569
+ });
3445
3570
  const expoChannel = new ExpoNotificationChannel();
3446
3571
  const notificationService = new NotificationService(sessionManager, expoChannel);
3447
3572
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
@@ -3456,11 +3581,6 @@ async function start(opts = {}) {
3456
3581
  console.log(`[Server] ${t("server.activityPushContinue")}`);
3457
3582
  }
3458
3583
  }
3459
- const wsBridge = await createWithRetry(
3460
- "WsBridge",
3461
- WS_PORT,
3462
- () => WsBridge.create({ port: WS_PORT, token })
3463
- );
3464
3584
  const sessionFileWatcher = new SessionFileWatcher((event) => {
3465
3585
  wsBridge.broadcast(event);
3466
3586
  });
@@ -3489,7 +3609,6 @@ async function start(opts = {}) {
3489
3609
  });
3490
3610
  }
3491
3611
  });
3492
- const unreadSessionIds = /* @__PURE__ */ new Set();
3493
3612
  notificationService.setGlobalPendingCountProvider(
3494
3613
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3495
3614
  );
@@ -3520,6 +3639,8 @@ async function start(opts = {}) {
3520
3639
  switch (event.type) {
3521
3640
  case "create_session": {
3522
3641
  await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
3642
+ const resumeId = event.resumeSessionId ?? event.newSessionId;
3643
+ if (resumeId) sessionFileWatcher.unwatch(resumeId);
3523
3644
  await sessionManager.createSession(
3524
3645
  event.projectPath,
3525
3646
  event.message,
@@ -3537,6 +3658,7 @@ async function start(opts = {}) {
3537
3658
  break;
3538
3659
  }
3539
3660
  case "send_message": {
3661
+ sessionFileWatcher.unwatch(event.sessionId);
3540
3662
  await sessionManager.sendMessage(event.sessionId, event.message, event.permissionMode, event.images);
3541
3663
  wsBridge.broadcast({
3542
3664
  type: "session_list",
@@ -3625,6 +3747,10 @@ async function start(opts = {}) {
3625
3747
  code: "PROJECT_LIST_ERROR"
3626
3748
  });
3627
3749
  }
3750
+ wsBridge.send(ws, {
3751
+ type: "session_list",
3752
+ sessions: sessionManager.getActiveSessions()
3753
+ });
3628
3754
  break;
3629
3755
  }
3630
3756
  case "list_sessions": {
@@ -3719,6 +3845,20 @@ async function start(opts = {}) {
3719
3845
  notificationService.setSoundPreferences(event.preferences);
3720
3846
  break;
3721
3847
  }
3848
+ case "terminal_exec": {
3849
+ const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3850
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
3851
+ if (!cwd) {
3852
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
3853
+ break;
3854
+ }
3855
+ terminalExecutor.exec(event.sessionId, event.command, cwd);
3856
+ break;
3857
+ }
3858
+ case "terminal_kill": {
3859
+ terminalExecutor.kill(event.execId);
3860
+ break;
3861
+ }
3722
3862
  case "register_activity_push_token": {
3723
3863
  notificationService.addActivityPushToken(event.sessionId, event.token);
3724
3864
  break;
@@ -3790,12 +3930,14 @@ async function start(opts = {}) {
3790
3930
  sessionManager.onEvent((event) => {
3791
3931
  wsBridge.broadcast(event);
3792
3932
  if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3793
- if (!wsBridge.isViewingSession(event.sessionId)) {
3794
- unreadSessionIds.add(event.sessionId);
3933
+ if (unreadSessionIds.has(event.sessionId)) {
3795
3934
  broadcastUnreadSessions();
3796
3935
  }
3797
3936
  }
3798
3937
  });
3938
+ terminalExecutor.onEvent((event) => {
3939
+ wsBridge.broadcast(event);
3940
+ });
3799
3941
  wsBridge.onDisconnect(() => {
3800
3942
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
3801
3943
  approvalProxy.approveAll(t("server.phoneDisconnected"));
@@ -3895,6 +4037,7 @@ async function start(opts = {}) {
3895
4037
  await attempt(() => wsBridge.close(), "WebSocket");
3896
4038
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3897
4039
  await attempt(() => sessionManager.destroy(), "SessionManager");
4040
+ await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
3898
4041
  await attempt(() => notificationService.destroy(), "NotificationService");
3899
4042
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3900
4043
  if (errors.length > 0) {
@@ -3903,7 +4046,7 @@ async function start(opts = {}) {
3903
4046
  }
3904
4047
  console.log(`[Server] ${t("server.shutdownComplete")}`);
3905
4048
  };
3906
- return {
4049
+ const instance = {
3907
4050
  token,
3908
4051
  wsPort: WS_PORT,
3909
4052
  httpPort: HTTP_PORT,
@@ -3921,8 +4064,21 @@ async function start(opts = {}) {
3921
4064
  }
3922
4065
  },
3923
4066
  openPairing: (duration) => pairingManager.open(duration),
3924
- closePairing: () => pairingManager.close()
4067
+ closePairing: () => pairingManager.close(),
4068
+ regenerateToken: async () => {
4069
+ const newToken = (0, import_uuid5.v4)();
4070
+ await (0, import_promises4.mkdir)(configDir, { recursive: true });
4071
+ await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
4072
+ instance.token = newToken;
4073
+ wsBridge.updateToken(newToken);
4074
+ approvalProxy.updateToken(newToken);
4075
+ pairingManager.updateToken(newToken);
4076
+ pairingManager.open();
4077
+ console.log(`[Server] ${t("server.tokenRegenerated", { token: newToken })}`);
4078
+ return newToken;
4079
+ }
3925
4080
  };
4081
+ return instance;
3926
4082
  }
3927
4083
 
3928
4084
  // src/index.ts
@@ -3994,6 +4150,7 @@ async function main() {
3994
4150
  console.log(t("startup.waitingConnection"));
3995
4151
  console.log();
3996
4152
  console.log(t("startup.pairingOpen"));
4153
+ console.log(t("startup.pressT"));
3997
4154
  console.log();
3998
4155
  fetchLatestVersion().then((latest) => {
3999
4156
  if (!latest || latest === PKG_VERSION) return;
@@ -4024,6 +4181,23 @@ async function main() {
4024
4181
  console.log(`
4025
4182
  ${t("startup.pairingReopened")}`);
4026
4183
  }
4184
+ if (key === "t" || key === "T") {
4185
+ server.regenerateToken().then((newToken) => {
4186
+ console.log();
4187
+ console.log(` ${t("startup.tokenRegenerated")}`);
4188
+ console.log(t("startup.token", { token: newToken }));
4189
+ console.log();
4190
+ const newQrUrl = buildQrUrl(getLocalIp(), server.wsPort, newToken);
4191
+ console.log(t("startup.scanToPair"));
4192
+ import_qrcode_terminal.default.generate(newQrUrl, { small: true }, (qr) => {
4193
+ qr.split("\n").forEach((line) => console.log(` ${line}`));
4194
+ });
4195
+ console.log();
4196
+ }).catch((err) => {
4197
+ console.error(`
4198
+ ${t("startup.tokenRegenerateFailed")}`, err);
4199
+ });
4200
+ }
4027
4201
  if (key === "") {
4028
4202
  shutdown("SIGINT");
4029
4203
  }
package/dist/server.d.ts CHANGED
@@ -19,6 +19,8 @@ interface ServerInstance {
19
19
  openPairing: (duration?: number) => void;
20
20
  /** 运行时关闭配对窗口 */
21
21
  closePairing: () => void;
22
+ /** 重新生成 token(泄露后刷新),断开所有客户端并开启配对窗口 */
23
+ regenerateToken: () => Promise<string>;
22
24
  }
23
25
  interface ServerOptions {
24
26
  /** 覆盖 token(默认读取 ~/.sessix/token 或自动生成) */
package/dist/server.js CHANGED
@@ -52,7 +52,10 @@ var zh = {
52
52
  autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
53
53
  autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
54
54
  pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
55
+ pressT: " \u{1F511} \u6309 t \u91CD\u7F6E Token\uFF08\u6CC4\u9732\u540E\u5237\u65B0\uFF09",
55
56
  pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
57
+ tokenRegenerated: "\u{1F511} Token \u5DF2\u91CD\u7F6E\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u914D\u5BF9",
58
+ tokenRegenerateFailed: "Token \u91CD\u7F6E\u5931\u8D25:",
56
59
  updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
57
60
  receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
58
61
  goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
@@ -81,7 +84,8 @@ var zh = {
81
84
  activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
82
85
  activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
83
86
  activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
84
- noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
87
+ noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B",
88
+ tokenRegenerated: "Token \u5DF2\u91CD\u7F6E: {{token}}"
85
89
  },
86
90
  ws: {
87
91
  started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
@@ -158,7 +162,10 @@ var en = {
158
162
  autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
159
163
  autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
160
164
  pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
165
+ pressT: " \u{1F511} Press t to regenerate token (refresh after leak)",
161
166
  pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
167
+ tokenRegenerated: "\u{1F511} Token regenerated, all clients disconnected. Scan QR to re-pair",
168
+ tokenRegenerateFailed: "Token regeneration failed:",
162
169
  updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
163
170
  receivedSignal: "Received {{signal}}, graceful shutdown...",
164
171
  goodbye: "All services closed, goodbye!",
@@ -187,7 +194,8 @@ var en = {
187
194
  activityPushEnabled: "ActivityKit Push enabled",
188
195
  activityPushFailed: "ActivityKit Push init failed:",
189
196
  activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
190
- noActiveLoginProcess: "No active login process"
197
+ noActiveLoginProcess: "No active login process",
198
+ tokenRegenerated: "Token regenerated: {{token}}"
191
199
  },
192
200
  ws: {
193
201
  started: "WebSocket server started on port {{port}}",
@@ -293,11 +301,11 @@ function t(key, params) {
293
301
  }
294
302
 
295
303
  // src/server.ts
296
- var import_uuid4 = require("uuid");
304
+ var import_uuid5 = require("uuid");
297
305
  var import_promises4 = require("fs/promises");
298
306
  var import_node_os6 = require("os");
299
307
  var import_node_path5 = require("path");
300
- var import_node_child_process4 = require("child_process");
308
+ var import_node_child_process5 = require("child_process");
301
309
  var import_node_util = require("util");
302
310
 
303
311
  // src/providers/ProcessProvider.ts
@@ -1544,6 +1552,13 @@ var WsBridge = class _WsBridge {
1544
1552
  getConnectionCount() {
1545
1553
  return this.wss.clients.size;
1546
1554
  }
1555
+ /** 更新 token 并断开所有现有连接(token 刷新后需重新配对) */
1556
+ updateToken(newToken) {
1557
+ this.token = newToken;
1558
+ for (const ws of this.wss.clients) {
1559
+ ws.close(4001, "Token regenerated");
1560
+ }
1561
+ }
1547
1562
  /** 优雅关闭 WebSocket 服务 */
1548
1563
  close() {
1549
1564
  return new Promise((resolve, reject) => {
@@ -1983,6 +1998,10 @@ var ApprovalProxy = class _ApprovalProxy {
1983
1998
  });
1984
1999
  }
1985
2000
  }
2001
+ /** 更新 token(token 刷新时调用) */
2002
+ updateToken(newToken) {
2003
+ this.token = newToken;
2004
+ }
1986
2005
  /** 返回连接 token(仅本机访问) */
1987
2006
  handleToken(req, res) {
1988
2007
  const remoteAddress = req.socket.remoteAddress;
@@ -3251,6 +3270,9 @@ var PairingManager = class {
3251
3270
  this.close();
3252
3271
  return result;
3253
3272
  }
3273
+ updateToken(newToken) {
3274
+ this.token = newToken;
3275
+ }
3254
3276
  getRemainingSeconds() {
3255
3277
  if (this._state !== "open") return 0;
3256
3278
  return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
@@ -3383,9 +3405,98 @@ var AuthManager = class extends import_events2.EventEmitter {
3383
3405
 
3384
3406
  // src/server.ts
3385
3407
  var import_promises5 = require("fs/promises");
3408
+
3409
+ // src/terminal/TerminalExecutor.ts
3410
+ var import_node_child_process4 = require("child_process");
3411
+ var import_uuid4 = require("uuid");
3412
+ var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
3413
+ var TerminalExecutor = class {
3414
+ processes = /* @__PURE__ */ new Map();
3415
+ eventCallbacks = [];
3416
+ onEvent(callback) {
3417
+ this.eventCallbacks.push(callback);
3418
+ return () => {
3419
+ const idx = this.eventCallbacks.indexOf(callback);
3420
+ if (idx !== -1) this.eventCallbacks.splice(idx, 1);
3421
+ };
3422
+ }
3423
+ emit(event) {
3424
+ for (const cb of this.eventCallbacks) {
3425
+ try {
3426
+ cb(event);
3427
+ } catch (err) {
3428
+ console.error("[TerminalExecutor] Event callback error:", err);
3429
+ }
3430
+ }
3431
+ }
3432
+ exec(sessionId, command, cwd) {
3433
+ const execId = (0, import_uuid4.v4)();
3434
+ const shell = isWindows ? "powershell" : "bash";
3435
+ const args = isWindows ? ["-Command", command] : ["-c", command];
3436
+ const proc = (0, import_node_child_process4.spawn)(shell, args, {
3437
+ cwd,
3438
+ stdio: ["ignore", "pipe", "pipe"],
3439
+ env: { ...process.env }
3440
+ });
3441
+ this.processes.set(execId, proc);
3442
+ proc.stdout?.on("data", (chunk) => {
3443
+ this.emit({
3444
+ type: "terminal_output",
3445
+ sessionId,
3446
+ execId,
3447
+ stream: "stdout",
3448
+ data: chunk.toString()
3449
+ });
3450
+ });
3451
+ proc.stderr?.on("data", (chunk) => {
3452
+ this.emit({
3453
+ type: "terminal_output",
3454
+ sessionId,
3455
+ execId,
3456
+ stream: "stderr",
3457
+ data: chunk.toString()
3458
+ });
3459
+ });
3460
+ proc.on("exit", (code, signal) => {
3461
+ clearTimeout(timer);
3462
+ this.processes.delete(execId);
3463
+ this.emit({
3464
+ type: "terminal_exit",
3465
+ sessionId,
3466
+ execId,
3467
+ code,
3468
+ signal
3469
+ });
3470
+ });
3471
+ const timer = setTimeout(() => {
3472
+ if (this.processes.has(execId)) {
3473
+ killProcessCrossPlatform(proc);
3474
+ }
3475
+ }, EXEC_TIMEOUT_MS);
3476
+ console.log(`[TerminalExecutor] exec ${execId}: ${command.substring(0, 100)} (cwd: ${cwd})`);
3477
+ return execId;
3478
+ }
3479
+ kill(execId) {
3480
+ const proc = this.processes.get(execId);
3481
+ if (proc) {
3482
+ killProcessCrossPlatform(proc);
3483
+ console.log(`[TerminalExecutor] kill ${execId}`);
3484
+ }
3485
+ }
3486
+ destroy() {
3487
+ for (const [execId, proc] of this.processes) {
3488
+ killProcessCrossPlatform(proc);
3489
+ console.log(`[TerminalExecutor] cleanup ${execId}`);
3490
+ }
3491
+ this.processes.clear();
3492
+ this.eventCallbacks.length = 0;
3493
+ }
3494
+ };
3495
+
3496
+ // src/server.ts
3386
3497
  var WS_PORT = 3745;
3387
3498
  var HTTP_PORT = 3746;
3388
- var execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
3499
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process5.exec);
3389
3500
  async function killPortProcess(port) {
3390
3501
  try {
3391
3502
  if (isWindows) {
@@ -3440,7 +3551,7 @@ async function start(opts = {}) {
3440
3551
  try {
3441
3552
  token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
3442
3553
  } catch {
3443
- token = (0, import_uuid4.v4)();
3554
+ token = (0, import_uuid5.v4)();
3444
3555
  await (0, import_promises4.mkdir)(configDir, { recursive: true });
3445
3556
  await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
3446
3557
  }
@@ -3448,6 +3559,20 @@ async function start(opts = {}) {
3448
3559
  }
3449
3560
  const provider = new ProcessProvider();
3450
3561
  const sessionManager = new SessionManager(provider);
3562
+ const terminalExecutor = new TerminalExecutor();
3563
+ const wsBridge = await createWithRetry(
3564
+ "WsBridge",
3565
+ WS_PORT,
3566
+ () => WsBridge.create({ port: WS_PORT, token })
3567
+ );
3568
+ const unreadSessionIds = /* @__PURE__ */ new Set();
3569
+ sessionManager.onEvent((event) => {
3570
+ if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3571
+ if (!wsBridge.isViewingSession(event.sessionId)) {
3572
+ unreadSessionIds.add(event.sessionId);
3573
+ }
3574
+ }
3575
+ });
3451
3576
  const expoChannel = new ExpoNotificationChannel();
3452
3577
  const notificationService = new NotificationService(sessionManager, expoChannel);
3453
3578
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
@@ -3462,11 +3587,6 @@ async function start(opts = {}) {
3462
3587
  console.log(`[Server] ${t("server.activityPushContinue")}`);
3463
3588
  }
3464
3589
  }
3465
- const wsBridge = await createWithRetry(
3466
- "WsBridge",
3467
- WS_PORT,
3468
- () => WsBridge.create({ port: WS_PORT, token })
3469
- );
3470
3590
  const sessionFileWatcher = new SessionFileWatcher((event) => {
3471
3591
  wsBridge.broadcast(event);
3472
3592
  });
@@ -3495,7 +3615,6 @@ async function start(opts = {}) {
3495
3615
  });
3496
3616
  }
3497
3617
  });
3498
- const unreadSessionIds = /* @__PURE__ */ new Set();
3499
3618
  notificationService.setGlobalPendingCountProvider(
3500
3619
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
3501
3620
  );
@@ -3526,6 +3645,8 @@ async function start(opts = {}) {
3526
3645
  switch (event.type) {
3527
3646
  case "create_session": {
3528
3647
  await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
3648
+ const resumeId = event.resumeSessionId ?? event.newSessionId;
3649
+ if (resumeId) sessionFileWatcher.unwatch(resumeId);
3529
3650
  await sessionManager.createSession(
3530
3651
  event.projectPath,
3531
3652
  event.message,
@@ -3543,6 +3664,7 @@ async function start(opts = {}) {
3543
3664
  break;
3544
3665
  }
3545
3666
  case "send_message": {
3667
+ sessionFileWatcher.unwatch(event.sessionId);
3546
3668
  await sessionManager.sendMessage(event.sessionId, event.message, event.permissionMode, event.images);
3547
3669
  wsBridge.broadcast({
3548
3670
  type: "session_list",
@@ -3631,6 +3753,10 @@ async function start(opts = {}) {
3631
3753
  code: "PROJECT_LIST_ERROR"
3632
3754
  });
3633
3755
  }
3756
+ wsBridge.send(ws, {
3757
+ type: "session_list",
3758
+ sessions: sessionManager.getActiveSessions()
3759
+ });
3634
3760
  break;
3635
3761
  }
3636
3762
  case "list_sessions": {
@@ -3725,6 +3851,20 @@ async function start(opts = {}) {
3725
3851
  notificationService.setSoundPreferences(event.preferences);
3726
3852
  break;
3727
3853
  }
3854
+ case "terminal_exec": {
3855
+ const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3856
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
3857
+ if (!cwd) {
3858
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
3859
+ break;
3860
+ }
3861
+ terminalExecutor.exec(event.sessionId, event.command, cwd);
3862
+ break;
3863
+ }
3864
+ case "terminal_kill": {
3865
+ terminalExecutor.kill(event.execId);
3866
+ break;
3867
+ }
3728
3868
  case "register_activity_push_token": {
3729
3869
  notificationService.addActivityPushToken(event.sessionId, event.token);
3730
3870
  break;
@@ -3796,12 +3936,14 @@ async function start(opts = {}) {
3796
3936
  sessionManager.onEvent((event) => {
3797
3937
  wsBridge.broadcast(event);
3798
3938
  if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3799
- if (!wsBridge.isViewingSession(event.sessionId)) {
3800
- unreadSessionIds.add(event.sessionId);
3939
+ if (unreadSessionIds.has(event.sessionId)) {
3801
3940
  broadcastUnreadSessions();
3802
3941
  }
3803
3942
  }
3804
3943
  });
3944
+ terminalExecutor.onEvent((event) => {
3945
+ wsBridge.broadcast(event);
3946
+ });
3805
3947
  wsBridge.onDisconnect(() => {
3806
3948
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
3807
3949
  approvalProxy.approveAll(t("server.phoneDisconnected"));
@@ -3901,6 +4043,7 @@ async function start(opts = {}) {
3901
4043
  await attempt(() => wsBridge.close(), "WebSocket");
3902
4044
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3903
4045
  await attempt(() => sessionManager.destroy(), "SessionManager");
4046
+ await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
3904
4047
  await attempt(() => notificationService.destroy(), "NotificationService");
3905
4048
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3906
4049
  if (errors.length > 0) {
@@ -3909,7 +4052,7 @@ async function start(opts = {}) {
3909
4052
  }
3910
4053
  console.log(`[Server] ${t("server.shutdownComplete")}`);
3911
4054
  };
3912
- return {
4055
+ const instance = {
3913
4056
  token,
3914
4057
  wsPort: WS_PORT,
3915
4058
  httpPort: HTTP_PORT,
@@ -3927,8 +4070,21 @@ async function start(opts = {}) {
3927
4070
  }
3928
4071
  },
3929
4072
  openPairing: (duration) => pairingManager.open(duration),
3930
- closePairing: () => pairingManager.close()
4073
+ closePairing: () => pairingManager.close(),
4074
+ regenerateToken: async () => {
4075
+ const newToken = (0, import_uuid5.v4)();
4076
+ await (0, import_promises4.mkdir)(configDir, { recursive: true });
4077
+ await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
4078
+ instance.token = newToken;
4079
+ wsBridge.updateToken(newToken);
4080
+ approvalProxy.updateToken(newToken);
4081
+ pairingManager.updateToken(newToken);
4082
+ pairingManager.open();
4083
+ console.log(`[Server] ${t("server.tokenRegenerated", { token: newToken })}`);
4084
+ return newToken;
4085
+ }
3931
4086
  };
4087
+ return instance;
3932
4088
  }
3933
4089
  // Annotate the CommonJS export names for ESM import in node:
3934
4090
  0 && (module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },