@linxin666/dsh-remote-web-ui 0.1.16 → 0.1.17

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.
Binary file
package/lib/index.js CHANGED
@@ -981,6 +981,10 @@ function makeRoutes(deps) {
981
981
  function mobileBundlePath() {
982
982
  return fileURLToPath(new URL("../lib/mobile.js", import.meta.url));
983
983
  }
984
+ /** The apple-touch-icon shipped with the package (iOS home-screen icon, 180x180 PNG). */
985
+ function appleTouchIconPath() {
986
+ return fileURLToPath(new URL("../assets/apple-touch-icon.png", import.meta.url));
987
+ }
984
988
  /** The mobile page shell: minimal, offline-safe, no external assets. */
985
989
  function pageHtml(bundleUrl) {
986
990
  return [
@@ -991,6 +995,7 @@ function pageHtml(bundleUrl) {
991
995
  "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover\">",
992
996
  "<meta name=\"theme-color\" content=\"#f3f5f9\">",
993
997
  "<meta name=\"referrer\" content=\"no-referrer\">",
998
+ "<link rel=\"apple-touch-icon\" href=\"/m/apple-touch-icon.png\">",
994
999
  "<title>移动端远程控制</title>",
995
1000
  "</head>",
996
1001
  "<body>",
@@ -1009,6 +1014,15 @@ function writeStatic(res, status, type, body) {
1009
1014
  });
1010
1015
  res.end(body);
1011
1016
  }
1017
+ /** Send the PNG icon as a binary body (unlike text assets, the PNG must not be decoded to utf-8). */
1018
+ function writePng(res, status, body) {
1019
+ res.writeHead(status, {
1020
+ "content-type": "image/png",
1021
+ "cache-control": "public, max-age=31536000, immutable",
1022
+ "referrer-policy": "no-referrer"
1023
+ });
1024
+ res.end(body);
1025
+ }
1012
1026
  /**
1013
1027
  * Build the mobile page routes.
1014
1028
  * @returns the two exact routes to register on webServer.
@@ -1029,15 +1043,35 @@ function makeMobileRoutes() {
1029
1043
  writeStatic(res, 500, "text/plain", "failed to read the mobile bundle");
1030
1044
  }
1031
1045
  };
1032
- return [{
1033
- kind: "exact",
1034
- path: "/m",
1035
- handler: handlePage
1036
- }, {
1037
- kind: "exact",
1038
- path: "/m/mobile.js",
1039
- handler: handleBundle
1040
- }];
1046
+ const handleIcon = async (_req, res) => {
1047
+ const path = appleTouchIconPath();
1048
+ if (!existsSync(path)) {
1049
+ writeStatic(res, 404, "text/plain", "apple-touch-icon not found");
1050
+ return;
1051
+ }
1052
+ try {
1053
+ writePng(res, 200, await readFile(path));
1054
+ } catch {
1055
+ writeStatic(res, 500, "text/plain", "failed to read the apple-touch-icon");
1056
+ }
1057
+ };
1058
+ return [
1059
+ {
1060
+ kind: "exact",
1061
+ path: "/m",
1062
+ handler: handlePage
1063
+ },
1064
+ {
1065
+ kind: "exact",
1066
+ path: "/m/mobile.js",
1067
+ handler: handleBundle
1068
+ },
1069
+ {
1070
+ kind: "exact",
1071
+ path: "/m/apple-touch-icon.png",
1072
+ handler: handleIcon
1073
+ }
1074
+ ];
1041
1075
  }
1042
1076
  //#endregion
1043
1077
  //#region src/mobile-api.ts
@@ -1096,10 +1130,22 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
1096
1130
  function makeMobileApiRoutes(deps) {
1097
1131
  const { service, apiProxy, mobileEnterToSend } = deps;
1098
1132
  const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;
1133
+ /**
1134
+ * Refresh the paired device's presence and report whether it is live.
1135
+ * The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
1136
+ * sender, so any activity on the mobile channel — a gated RPC, or the live
1137
+ * SSE stream staying open — must count as presence. Without this, an
1138
+ * idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
1139
+ * wrongly reports it as disconnected.
1140
+ */
1141
+ const touchDeviceFor = (req) => {
1142
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
1143
+ if (deviceId === void 0) return false;
1144
+ return service.touchDevice(deviceId);
1145
+ };
1099
1146
  /** The phone gate: a live paired-device cookie, or nothing else proceeds. */
1100
1147
  const gateOk = (req) => {
1101
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
1102
- return deviceId !== void 0 && service.hasDevice(deviceId);
1148
+ return touchDeviceFor(req);
1103
1149
  };
1104
1150
  const writeJson = (res, status, body) => {
1105
1151
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
@@ -1219,6 +1265,7 @@ function makeMobileApiRoutes(deps) {
1219
1265
  let closed = false;
1220
1266
  const heartbeat = setInterval(() => {
1221
1267
  if (closed) return;
1268
+ touchDeviceFor(req);
1222
1269
  try {
1223
1270
  res.write(": ping\n\n");
1224
1271
  } catch {}
@@ -1787,6 +1834,16 @@ const OUTPUT_CAP = 16 * 1024;
1787
1834
  */
1788
1835
  const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
1789
1836
  /**
1837
+ * Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
1838
+ * silently skips same-day releases — `pnpm update --latest` then exits 0
1839
+ * without moving anything and the post-run verification would misreport a
1840
+ * stale no-op. The update here is explicitly user-initiated and the panel
1841
+ * shows exactly which versions are being installed, so the gate only adds a
1842
+ * confusing silent no-op; override it per-invocation instead of asking every
1843
+ * user to edit the profile's pnpm-workspace.yaml.
1844
+ */
1845
+ const MIN_RELEASE_AGE_OVERRIDE = "--config.minimumReleaseAge=0";
1846
+ /**
1790
1847
  * Run the update inside the profile directory. Tries pnpm first, falls back
1791
1848
  * to corepack and then npx when the previous command is missing (ENOENT);
1792
1849
  * all candidates share one hard timeout and keep accumulating output.
@@ -1813,6 +1870,7 @@ function runUpdate(deps) {
1813
1870
  args: [
1814
1871
  "update",
1815
1872
  "--latest",
1873
+ MIN_RELEASE_AGE_OVERRIDE,
1816
1874
  ...packages
1817
1875
  ]
1818
1876
  },
@@ -1822,6 +1880,7 @@ function runUpdate(deps) {
1822
1880
  "pnpm",
1823
1881
  "update",
1824
1882
  "--latest",
1883
+ MIN_RELEASE_AGE_OVERRIDE,
1825
1884
  ...packages
1826
1885
  ]
1827
1886
  },
@@ -1832,6 +1891,7 @@ function runUpdate(deps) {
1832
1891
  "pnpm",
1833
1892
  "update",
1834
1893
  "--latest",
1894
+ MIN_RELEASE_AGE_OVERRIDE,
1835
1895
  ...packages
1836
1896
  ]
1837
1897
  }
@@ -1930,9 +1990,11 @@ function runUpdate(deps) {
1930
1990
  }
1931
1991
  /**
1932
1992
  * Run the update, then verify the installed versions actually moved. pnpm
1933
- * exits 0 even when it silently kept the installed versions — the pnpm 11
1934
- * `minimumReleaseAge` gate refuses same-day releases by default — so a green
1935
- * exit alone would report a misleading "update complete". Re-read the
1993
+ * exits 0 even when it silently kept the installed versions — runUpdate
1994
+ * overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
1995
+ * `--config.minimumReleaseAge=0`, but an older pnpm without the override or
1996
+ * another silent no-op can still keep versions in place — so a green exit
1997
+ * alone must not report a misleading "update complete". Re-read the
1936
1998
  * installed versions afterwards and surface a `stale` failure (with the
1937
1999
  * captured pnpm output) when nothing moved, so the panel can tell the user
1938
2000
  * how to unblock the gate instead of claiming success.
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA4HnE"}
1
+ {"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA6InE"}
@@ -77,10 +77,23 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
77
77
  export function makeMobileApiRoutes(deps) {
78
78
  const { service, apiProxy, mobileEnterToSend } = deps;
79
79
  const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;
80
+ /**
81
+ * Refresh the paired device's presence and report whether it is live.
82
+ * The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
83
+ * sender, so any activity on the mobile channel — a gated RPC, or the live
84
+ * SSE stream staying open — must count as presence. Without this, an
85
+ * idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
86
+ * wrongly reports it as disconnected.
87
+ */
88
+ const touchDeviceFor = (req) => {
89
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
90
+ if (deviceId === undefined)
91
+ return false;
92
+ return service.touchDevice(deviceId);
93
+ };
80
94
  /** The phone gate: a live paired-device cookie, or nothing else proceeds. */
81
95
  const gateOk = (req) => {
82
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
83
- return deviceId !== undefined && service.hasDevice(deviceId);
96
+ return touchDeviceFor(req);
84
97
  };
85
98
  const writeJson = (res, status, body) => {
86
99
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
@@ -164,6 +177,10 @@ export function makeMobileApiRoutes(deps) {
164
177
  const heartbeat = setInterval(() => {
165
178
  if (closed)
166
179
  return;
180
+ // An open SSE stream proves the phone is still live even while the agent
181
+ // idles (no RPC traffic), so refresh presence alongside the transport
182
+ // keepalive — otherwise an idle phone drifts to "disconnected".
183
+ touchDeviceFor(req);
167
184
  try {
168
185
  res.write(': ping\n\n');
169
186
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAqC/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAqB7C"}
1
+ {"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwD/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAmC7C"}
@@ -14,6 +14,10 @@ import { fileURLToPath } from 'node:url';
14
14
  function mobileBundlePath() {
15
15
  return fileURLToPath(new URL('../lib/mobile.js', import.meta.url));
16
16
  }
17
+ /** The apple-touch-icon shipped with the package (iOS home-screen icon, 180x180 PNG). */
18
+ function appleTouchIconPath() {
19
+ return fileURLToPath(new URL('../assets/apple-touch-icon.png', import.meta.url));
20
+ }
17
21
  /** The mobile page shell: minimal, offline-safe, no external assets. */
18
22
  function pageHtml(bundleUrl) {
19
23
  return [
@@ -24,6 +28,10 @@ function pageHtml(bundleUrl) {
24
28
  '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">',
25
29
  '<meta name="theme-color" content="#f3f5f9">',
26
30
  '<meta name="referrer" content="no-referrer">',
31
+ // iOS "Add to Home Screen" uses apple-touch-icon for the tile icon;
32
+ // the PNG ships with the package (opaque background — iOS composites
33
+ // transparency onto black, so the icon avoids transparent regions).
34
+ '<link rel="apple-touch-icon" href="/m/apple-touch-icon.png">',
27
35
  '<title>移动端远程控制</title>',
28
36
  '</head>',
29
37
  '<body>',
@@ -42,6 +50,15 @@ function writeStatic(res, status, type, body) {
42
50
  });
43
51
  res.end(body);
44
52
  }
53
+ /** Send the PNG icon as a binary body (unlike text assets, the PNG must not be decoded to utf-8). */
54
+ function writePng(res, status, body) {
55
+ res.writeHead(status, {
56
+ 'content-type': 'image/png',
57
+ 'cache-control': 'public, max-age=31536000, immutable',
58
+ 'referrer-policy': 'no-referrer',
59
+ });
60
+ res.end(body);
61
+ }
45
62
  /**
46
63
  * Build the mobile page routes.
47
64
  * @returns the two exact routes to register on webServer.
@@ -64,8 +81,23 @@ export function makeMobileRoutes() {
64
81
  writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle');
65
82
  }
66
83
  };
84
+ const handleIcon = async (_req, res) => {
85
+ const path = appleTouchIconPath();
86
+ if (!existsSync(path)) {
87
+ writeStatic(res, 404, 'text/plain', 'apple-touch-icon not found');
88
+ return;
89
+ }
90
+ try {
91
+ const body = await readFile(path);
92
+ writePng(res, 200, body);
93
+ }
94
+ catch {
95
+ writeStatic(res, 500, 'text/plain', 'failed to read the apple-touch-icon');
96
+ }
97
+ };
67
98
  return [
68
99
  { kind: 'exact', path: '/m', handler: handlePage },
69
100
  { kind: 'exact', path: '/m/mobile.js', handler: handleBundle },
101
+ { kind: 'exact', path: '/m/apple-touch-icon.png', handler: handleIcon },
70
102
  ];
71
103
  }
@@ -206,9 +206,11 @@ export interface UpdateRunVerifiedDeps {
206
206
  }
207
207
  /**
208
208
  * Run the update, then verify the installed versions actually moved. pnpm
209
- * exits 0 even when it silently kept the installed versions — the pnpm 11
210
- * `minimumReleaseAge` gate refuses same-day releases by default — so a green
211
- * exit alone would report a misleading "update complete". Re-read the
209
+ * exits 0 even when it silently kept the installed versions — runUpdate
210
+ * overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
211
+ * `--config.minimumReleaseAge=0`, but an older pnpm without the override or
212
+ * another silent no-op can still keep versions in place — so a green exit
213
+ * alone must not report a misleading "update complete". Re-read the
212
214
  * installed versions afterwards and surface a `stale` failure (with the
213
215
  * captured pnpm output) when nothing moved, so the panel can tell the user
214
216
  * how to unblock the gate instead of claiming success.
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
@@ -302,6 +302,16 @@ const OUTPUT_CAP = 16 * 1024;
302
302
  * detects this message instead of the spawn error event.
303
303
  */
304
304
  const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
305
+ /**
306
+ * Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
307
+ * silently skips same-day releases — `pnpm update --latest` then exits 0
308
+ * without moving anything and the post-run verification would misreport a
309
+ * stale no-op. The update here is explicitly user-initiated and the panel
310
+ * shows exactly which versions are being installed, so the gate only adds a
311
+ * confusing silent no-op; override it per-invocation instead of asking every
312
+ * user to edit the profile's pnpm-workspace.yaml.
313
+ */
314
+ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0';
305
315
  /**
306
316
  * Run the update inside the profile directory. Tries pnpm first, falls back
307
317
  * to corepack and then npx when the previous command is missing (ENOENT);
@@ -328,10 +338,14 @@ export function runUpdate(deps) {
328
338
  // "0.1.12" without a range), and plain `pnpm update` treats an exact spec
329
339
  // as pinned — it prints "Already up to date" and exits 0 without moving
330
340
  // the installed version, so the panel would report a false success.
341
+ // `MIN_RELEASE_AGE_OVERRIDE` rides along every candidate: without it the
342
+ // pnpm 11 minimumReleaseAge gate (default 24 h) silently keeps same-day
343
+ // releases in place, which the post-run verification would then have to
344
+ // surface as `stale` even though the update is legitimate.
331
345
  const candidates = [
332
- { command: 'pnpm', args: ['update', '--latest', ...packages] },
333
- { command: 'corepack', args: ['pnpm', 'update', '--latest', ...packages] },
334
- { command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', ...packages] },
346
+ { command: 'pnpm', args: ['update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
347
+ { command: 'corepack', args: ['pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
348
+ { command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
335
349
  ];
336
350
  // `output` accumulates across candidates for UI display; `currentOutput`
337
351
  // is reset per candidate and carries only that candidate's own diagnostics
@@ -444,9 +458,11 @@ export function runUpdate(deps) {
444
458
  }
445
459
  /**
446
460
  * Run the update, then verify the installed versions actually moved. pnpm
447
- * exits 0 even when it silently kept the installed versions — the pnpm 11
448
- * `minimumReleaseAge` gate refuses same-day releases by default — so a green
449
- * exit alone would report a misleading "update complete". Re-read the
461
+ * exits 0 even when it silently kept the installed versions — runUpdate
462
+ * overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
463
+ * `--config.minimumReleaseAge=0`, but an older pnpm without the override or
464
+ * another silent no-op can still keep versions in place — so a green exit
465
+ * alone must not report a misleading "update complete". Re-read the
450
466
  * installed versions afterwards and surface a `stale` failure (with the
451
467
  * captured pnpm output) when nothing moved, so the panel can tell the user
452
468
  * how to unblock the gate instead of claiming success.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
3
  "description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
4
- "version": "0.1.16",
4
+ "version": "0.1.17",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -30,6 +30,7 @@
30
30
  "lib/**/*.d.ts",
31
31
  "lib/**/*.d.ts.map",
32
32
  "src",
33
+ "assets",
33
34
  "cordis.patch.yml"
34
35
  ],
35
36
  "license": "Apache-2.0",
package/src/mobile-api.ts CHANGED
@@ -103,10 +103,23 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
103
103
  const { service, apiProxy, mobileEnterToSend } = deps
104
104
  const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS
105
105
 
106
+ /**
107
+ * Refresh the paired device's presence and report whether it is live.
108
+ * The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
109
+ * sender, so any activity on the mobile channel — a gated RPC, or the live
110
+ * SSE stream staying open — must count as presence. Without this, an
111
+ * idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
112
+ * wrongly reports it as disconnected.
113
+ */
114
+ const touchDeviceFor = (req: IncomingMessage): boolean => {
115
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
116
+ if (deviceId === undefined) return false
117
+ return service.touchDevice(deviceId)
118
+ }
119
+
106
120
  /** The phone gate: a live paired-device cookie, or nothing else proceeds. */
107
121
  const gateOk = (req: IncomingMessage): boolean => {
108
- const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
109
- return deviceId !== undefined && service.hasDevice(deviceId)
122
+ return touchDeviceFor(req)
110
123
  }
111
124
 
112
125
  const writeJson = (res: ServerResponse, status: number, body: unknown): void => {
@@ -190,6 +203,10 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
190
203
  let closed = false
191
204
  const heartbeat = setInterval(() => {
192
205
  if (closed) return
206
+ // An open SSE stream proves the phone is still live even while the agent
207
+ // idles (no RPC traffic), so refresh presence alongside the transport
208
+ // keepalive — otherwise an idle phone drifts to "disconnected".
209
+ touchDeviceFor(req)
193
210
  try {
194
211
  res.write(': ping\n\n')
195
212
  } catch {
@@ -19,6 +19,11 @@ function mobileBundlePath(): string {
19
19
  return fileURLToPath(new URL('../lib/mobile.js', import.meta.url))
20
20
  }
21
21
 
22
+ /** The apple-touch-icon shipped with the package (iOS home-screen icon, 180x180 PNG). */
23
+ function appleTouchIconPath(): string {
24
+ return fileURLToPath(new URL('../assets/apple-touch-icon.png', import.meta.url))
25
+ }
26
+
22
27
  /** The mobile page shell: minimal, offline-safe, no external assets. */
23
28
  function pageHtml(bundleUrl: string): string {
24
29
  return [
@@ -29,6 +34,10 @@ function pageHtml(bundleUrl: string): string {
29
34
  '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">',
30
35
  '<meta name="theme-color" content="#f3f5f9">',
31
36
  '<meta name="referrer" content="no-referrer">',
37
+ // iOS "Add to Home Screen" uses apple-touch-icon for the tile icon;
38
+ // the PNG ships with the package (opaque background — iOS composites
39
+ // transparency onto black, so the icon avoids transparent regions).
40
+ '<link rel="apple-touch-icon" href="/m/apple-touch-icon.png">',
32
41
  '<title>移动端远程控制</title>',
33
42
  '</head>',
34
43
  '<body>',
@@ -49,6 +58,16 @@ function writeStatic(res: ServerResponse, status: number, type: string, body: st
49
58
  res.end(body)
50
59
  }
51
60
 
61
+ /** Send the PNG icon as a binary body (unlike text assets, the PNG must not be decoded to utf-8). */
62
+ function writePng(res: ServerResponse, status: number, body: Buffer): void {
63
+ res.writeHead(status, {
64
+ 'content-type': 'image/png',
65
+ 'cache-control': 'public, max-age=31536000, immutable',
66
+ 'referrer-policy': 'no-referrer',
67
+ })
68
+ res.end(body)
69
+ }
70
+
52
71
  /**
53
72
  * Build the mobile page routes.
54
73
  * @returns the two exact routes to register on webServer.
@@ -70,8 +89,22 @@ export function makeMobileRoutes(): WebRoute[] {
70
89
  writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle')
71
90
  }
72
91
  }
92
+ const handleIcon = async (_req: IncomingMessage, res: ServerResponse): Promise<void> => {
93
+ const path = appleTouchIconPath()
94
+ if (!existsSync(path)) {
95
+ writeStatic(res, 404, 'text/plain', 'apple-touch-icon not found')
96
+ return
97
+ }
98
+ try {
99
+ const body = await readFile(path)
100
+ writePng(res, 200, body)
101
+ } catch {
102
+ writeStatic(res, 500, 'text/plain', 'failed to read the apple-touch-icon')
103
+ }
104
+ }
73
105
  return [
74
106
  { kind: 'exact', path: '/m', handler: handlePage },
75
107
  { kind: 'exact', path: '/m/mobile.js', handler: handleBundle },
108
+ { kind: 'exact', path: '/m/apple-touch-icon.png', handler: handleIcon },
76
109
  ]
77
110
  }
package/src/update.ts CHANGED
@@ -412,6 +412,17 @@ const OUTPUT_CAP = 16 * 1024
412
412
  */
413
413
  const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i
414
414
 
415
+ /**
416
+ * Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
417
+ * silently skips same-day releases — `pnpm update --latest` then exits 0
418
+ * without moving anything and the post-run verification would misreport a
419
+ * stale no-op. The update here is explicitly user-initiated and the panel
420
+ * shows exactly which versions are being installed, so the gate only adds a
421
+ * confusing silent no-op; override it per-invocation instead of asking every
422
+ * user to edit the profile's pnpm-workspace.yaml.
423
+ */
424
+ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
425
+
415
426
  /**
416
427
  * Run the update inside the profile directory. Tries pnpm first, falls back
417
428
  * to corepack and then npx when the previous command is missing (ENOENT);
@@ -438,10 +449,14 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
438
449
  // "0.1.12" without a range), and plain `pnpm update` treats an exact spec
439
450
  // as pinned — it prints "Already up to date" and exits 0 without moving
440
451
  // the installed version, so the panel would report a false success.
452
+ // `MIN_RELEASE_AGE_OVERRIDE` rides along every candidate: without it the
453
+ // pnpm 11 minimumReleaseAge gate (default 24 h) silently keeps same-day
454
+ // releases in place, which the post-run verification would then have to
455
+ // surface as `stale` even though the update is legitimate.
441
456
  const candidates: ReadonlyArray<{ command: string; args: string[] }> = [
442
- { command: 'pnpm', args: ['update', '--latest', ...packages] },
443
- { command: 'corepack', args: ['pnpm', 'update', '--latest', ...packages] },
444
- { command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', ...packages] },
457
+ { command: 'pnpm', args: ['update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
458
+ { command: 'corepack', args: ['pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
459
+ { command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', MIN_RELEASE_AGE_OVERRIDE, ...packages] },
445
460
  ]
446
461
  // `output` accumulates across candidates for UI display; `currentOutput`
447
462
  // is reset per candidate and carries only that candidate's own diagnostics
@@ -553,9 +568,11 @@ export interface UpdateRunVerifiedDeps {
553
568
 
554
569
  /**
555
570
  * Run the update, then verify the installed versions actually moved. pnpm
556
- * exits 0 even when it silently kept the installed versions — the pnpm 11
557
- * `minimumReleaseAge` gate refuses same-day releases by default — so a green
558
- * exit alone would report a misleading "update complete". Re-read the
571
+ * exits 0 even when it silently kept the installed versions — runUpdate
572
+ * overrides pnpm 11's `minimumReleaseAge` gate (default 24 h) with
573
+ * `--config.minimumReleaseAge=0`, but an older pnpm without the override or
574
+ * another silent no-op can still keep versions in place — so a green exit
575
+ * alone must not report a misleading "update complete". Re-read the
559
576
  * installed versions afterwards and surface a `stale` failure (with the
560
577
  * captured pnpm output) when nothing moved, so the panel can tell the user
561
578
  * how to unblock the gate instead of claiming success.