@linxin666/dsh-remote-web-ui 0.1.14 → 0.1.16
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/README.i18n.yaml +2 -2
- package/README.md +9 -5
- package/README.zh.md +1 -1
- package/lib/client.js +84 -23
- package/lib/client.js.map +1 -1
- package/lib/index.js +173 -18
- package/lib/mobile.js +44 -25
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +30 -8
- package/lib/types/client/UpdatePanel.js +2 -0
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +97 -20
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +36 -10
- package/lib/types/mobile/mux.d.ts +13 -4
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +50 -30
- package/lib/types/mobile/mux.test.js +75 -6
- package/lib/types/mobile-api.d.ts +2 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +4 -1
- package/lib/types/routes.d.ts +17 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +49 -10
- package/lib/types/update.d.ts +44 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +118 -8
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/UpdatePanel.tsx +2 -0
- package/src/client/locales.ts +4 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/index.ts +34 -10
- package/src/mobile/mux.test.ts +84 -6
- package/src/mobile/mux.ts +46 -25
- package/src/mobile-api.ts +6 -1
- package/src/routes.ts +51 -10
- package/src/update.ts +128 -9
package/lib/types/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { makeMobileRoutes } from "./mobile-routes.js";
|
|
|
18
18
|
import { makeMobileApiRoutes } from "./mobile-api.js";
|
|
19
19
|
import { lanIPv4Addresses } from "./lan.js";
|
|
20
20
|
import { TunnelManager } from "./tunnel.js";
|
|
21
|
-
import { checkUpdates, fetchLatestVersion, resolveAnchorManifest, resolveUpdateTarget,
|
|
21
|
+
import { checkUpdates, fetchLatestVersion, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
|
|
22
22
|
import { makeUpdateRoutes } from "./update-routes.js";
|
|
23
23
|
/** Stable cordis plugin name. */
|
|
24
24
|
export const name = 'remote-web-ui';
|
|
@@ -152,19 +152,27 @@ export function apply(ctx, config) {
|
|
|
152
152
|
}
|
|
153
153
|
// ── remote update ────────────────────────────────────────────────────────
|
|
154
154
|
// The dsh-web-ui self-update surface: probe the npm registry for family
|
|
155
|
-
// releases and run `pnpm update` in the owning profile. Resolutions
|
|
156
|
-
// on the host process's own module graph, so the update always
|
|
157
|
-
// profile the running web GUI was booted from. The
|
|
158
|
-
//
|
|
159
|
-
//
|
|
155
|
+
// releases and run `pnpm update --latest` in the owning profile. Resolutions
|
|
156
|
+
// anchor on the host process's own module graph, so the update always
|
|
157
|
+
// targets the profile the running web GUI was booted from. The anchor path
|
|
158
|
+
// is re-resolved per operation: pnpm removes the old version's .pnpm
|
|
159
|
+
// directory on update, so a boot-time captured path would fail to read
|
|
160
|
+
// after a successful update; versions are re-read from disk per check.
|
|
160
161
|
const requireFromHost = createRequire(import.meta.url);
|
|
161
|
-
const
|
|
162
|
+
const resolveAnchorPath = () => resolveAnchorManifest(specifier => {
|
|
163
|
+
try {
|
|
164
|
+
return requireFromHost.resolve(specifier);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
162
170
|
const updateRoutes = makeUpdateRoutes({
|
|
163
171
|
// Control endpoints are host-surface only: a LAN/phone origin must never
|
|
164
172
|
// trigger a real install on this machine.
|
|
165
173
|
fence: request => isTrustedApiRequest(request, []),
|
|
166
174
|
check: () => checkUpdates({
|
|
167
|
-
anchorManifestPath,
|
|
175
|
+
anchorManifestPath: resolveAnchorPath(),
|
|
168
176
|
resolve: specifier => {
|
|
169
177
|
try {
|
|
170
178
|
return requireFromHost.resolve(specifier);
|
|
@@ -176,7 +184,7 @@ export function apply(ctx, config) {
|
|
|
176
184
|
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
177
185
|
}),
|
|
178
186
|
run: async () => {
|
|
179
|
-
const target = resolveUpdateTarget({ anchorManifestPath });
|
|
187
|
+
const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() });
|
|
180
188
|
if ('error' in target) {
|
|
181
189
|
const code = target.error;
|
|
182
190
|
return {
|
|
@@ -187,7 +195,25 @@ export function apply(ctx, config) {
|
|
|
187
195
|
errorCode: code,
|
|
188
196
|
};
|
|
189
197
|
}
|
|
190
|
-
|
|
198
|
+
// Verify the versions actually moved after a green pnpm exit: the pnpm
|
|
199
|
+
// 11 minimumReleaseAge gate can silently keep the installed versions
|
|
200
|
+
// (same-day releases), which a plain exit-0 check would report as
|
|
201
|
+
// success — the user then restarts and nothing changed.
|
|
202
|
+
return runUpdateVerified({
|
|
203
|
+
run: { profileDir: target.profileDir, packages: target.packages },
|
|
204
|
+
check: {
|
|
205
|
+
anchorManifestPath: resolveAnchorPath(),
|
|
206
|
+
resolve: specifier => {
|
|
207
|
+
try {
|
|
208
|
+
return requireFromHost.resolve(specifier);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
191
217
|
},
|
|
192
218
|
});
|
|
193
219
|
const routes = [
|
|
@@ -78,9 +78,11 @@ export declare class MuxClient {
|
|
|
78
78
|
private sseAlive;
|
|
79
79
|
/** Per-session highest event seq already emitted, for poll dedup. */
|
|
80
80
|
private readonly pollWatermark;
|
|
81
|
-
|
|
82
|
-
private
|
|
81
|
+
/** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
|
|
82
|
+
private tickTimer;
|
|
83
83
|
private polling;
|
|
84
|
+
/** Epoch ms of the next due poll while polling (kept on the same tick timer). */
|
|
85
|
+
private nextPollAt;
|
|
84
86
|
/**
|
|
85
87
|
* @param url - the mobile events endpoint (browser-relative).
|
|
86
88
|
* @param options - seams.
|
|
@@ -99,8 +101,15 @@ export declare class MuxClient {
|
|
|
99
101
|
*/
|
|
100
102
|
observe(sessionId: string | undefined): void;
|
|
101
103
|
private connect;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The single scheduler tick is the only interval this client owns. One
|
|
106
|
+
* timer (at the finer of the stall-check and poll cadences) both arms the
|
|
107
|
+
* polling fallback once the stall threshold passes and drives each poll at
|
|
108
|
+
* {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
|
|
109
|
+
*/
|
|
110
|
+
private startTick;
|
|
111
|
+
private stopTick;
|
|
112
|
+
private tick;
|
|
104
113
|
private startPolling;
|
|
105
114
|
private stopPolling;
|
|
106
115
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;
|
|
1
|
+
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAuBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IAEtB;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IASrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IASZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAY5C,OAAO,CAAC,OAAO;IAqBf;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,IAAI;IAkBZ,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAInB;;;;OAIG;YACW,QAAQ;IAsBtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
|
package/lib/types/mobile/mux.js
CHANGED
|
@@ -34,6 +34,12 @@ function browserSource(url) {
|
|
|
34
34
|
}
|
|
35
35
|
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
36
36
|
const DEFAULT_STALL_THRESHOLD_MS = 12000;
|
|
37
|
+
/**
|
|
38
|
+
* Stall-check granularity: the single scheduler tick runs at least this
|
|
39
|
+
* often so fallback arms within a second of the stall threshold passing,
|
|
40
|
+
* while the poll cadence itself stays {@link MuxClientOptions.pollIntervalMs}.
|
|
41
|
+
*/
|
|
42
|
+
const STALL_CHECK_MS = 1000;
|
|
37
43
|
/** Poll window: enough recent events to cover a few seconds of agent output. */
|
|
38
44
|
const DEFAULT_POLL_PAGE_SIZE = 50;
|
|
39
45
|
/**
|
|
@@ -64,9 +70,11 @@ export class MuxClient {
|
|
|
64
70
|
sseAlive = false;
|
|
65
71
|
/** Per-session highest event seq already emitted, for poll dedup. */
|
|
66
72
|
pollWatermark = new Map();
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
/** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
|
|
74
|
+
tickTimer;
|
|
69
75
|
polling = false;
|
|
76
|
+
/** Epoch ms of the next due poll while polling (kept on the same tick timer). */
|
|
77
|
+
nextPollAt = 0;
|
|
70
78
|
/**
|
|
71
79
|
* @param url - the mobile events endpoint (browser-relative).
|
|
72
80
|
* @param options - seams.
|
|
@@ -85,15 +93,16 @@ export class MuxClient {
|
|
|
85
93
|
this.lastDataAt = this.now();
|
|
86
94
|
if (this.source === undefined)
|
|
87
95
|
this.connect();
|
|
88
|
-
this.
|
|
96
|
+
this.startTick();
|
|
89
97
|
}
|
|
90
98
|
/** Close for good. */
|
|
91
99
|
stop() {
|
|
92
100
|
this.stopped = true;
|
|
93
|
-
this.
|
|
101
|
+
this.stopTick();
|
|
94
102
|
this.stopPolling();
|
|
95
103
|
this.closeSource();
|
|
96
104
|
this.observeSessionId = undefined;
|
|
105
|
+
this.nextPollAt = 0;
|
|
97
106
|
}
|
|
98
107
|
/** Subscribe to validated frames; returns an unsubscribe function. */
|
|
99
108
|
onFrame(listener) {
|
|
@@ -137,43 +146,54 @@ export class MuxClient {
|
|
|
137
146
|
this.startPolling();
|
|
138
147
|
};
|
|
139
148
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// that has never delivered.
|
|
152
|
-
if (this.sseAlive)
|
|
153
|
-
return;
|
|
154
|
-
if ((this.now() - this.lastDataAt) > this.stallThresholdMs)
|
|
155
|
-
this.startPolling();
|
|
156
|
-
}, 1000);
|
|
149
|
+
/**
|
|
150
|
+
* The single scheduler tick is the only interval this client owns. One
|
|
151
|
+
* timer (at the finer of the stall-check and poll cadences) both arms the
|
|
152
|
+
* polling fallback once the stall threshold passes and drives each poll at
|
|
153
|
+
* {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
|
|
154
|
+
*/
|
|
155
|
+
startTick() {
|
|
156
|
+
if (this.tickTimer !== undefined)
|
|
157
|
+
return;
|
|
158
|
+
const cadence = Math.min(this.pollIntervalMs, STALL_CHECK_MS);
|
|
159
|
+
this.tickTimer = setInterval(() => { this.tick(); }, cadence);
|
|
157
160
|
}
|
|
158
|
-
|
|
159
|
-
if (this.
|
|
160
|
-
clearInterval(this.
|
|
161
|
-
this.
|
|
161
|
+
stopTick() {
|
|
162
|
+
if (this.tickTimer !== undefined) {
|
|
163
|
+
clearInterval(this.tickTimer);
|
|
164
|
+
this.tickTimer = undefined;
|
|
162
165
|
}
|
|
163
166
|
}
|
|
167
|
+
tick() {
|
|
168
|
+
if (this.stopped)
|
|
169
|
+
return;
|
|
170
|
+
if (this.observeSessionId === undefined)
|
|
171
|
+
return;
|
|
172
|
+
if (this.polling) {
|
|
173
|
+
// The stall phase has ended; the same tick now paces the polls.
|
|
174
|
+
if (this.now() >= this.nextPollAt) {
|
|
175
|
+
this.nextPollAt = this.now() + this.pollIntervalMs;
|
|
176
|
+
void this.pollTick();
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// A live SSE channel only goes silent while the agent idles; never poll
|
|
181
|
+
// against it. Fallback arms again only via onerror or a stream that has
|
|
182
|
+
// never delivered.
|
|
183
|
+
if (this.sseAlive)
|
|
184
|
+
return;
|
|
185
|
+
if ((this.now() - this.lastDataAt) > this.stallThresholdMs)
|
|
186
|
+
this.startPolling();
|
|
187
|
+
}
|
|
164
188
|
startPolling() {
|
|
165
189
|
if (this.polling || this.stopped)
|
|
166
190
|
return;
|
|
167
191
|
this.polling = true;
|
|
192
|
+
this.nextPollAt = this.now() + this.pollIntervalMs;
|
|
168
193
|
void this.pollTick();
|
|
169
|
-
this.pollTimer = setInterval(() => { void this.pollTick(); }, this.pollIntervalMs);
|
|
170
194
|
}
|
|
171
195
|
stopPolling() {
|
|
172
196
|
this.polling = false;
|
|
173
|
-
if (this.pollTimer !== undefined) {
|
|
174
|
-
clearInterval(this.pollTimer);
|
|
175
|
-
this.pollTimer = undefined;
|
|
176
|
-
}
|
|
177
197
|
}
|
|
178
198
|
/**
|
|
179
199
|
* Fetch the latest history page for the observed session and re-emit any
|
|
@@ -63,8 +63,9 @@ describe('MuxClient polling fallback', () => {
|
|
|
63
63
|
// Nothing live within a poll interval until the stall window passes.
|
|
64
64
|
await vi.advanceTimersByTimeAsync(400);
|
|
65
65
|
expect(pollLatest).not.toHaveBeenCalled();
|
|
66
|
-
//
|
|
67
|
-
|
|
66
|
+
// The single tick (400 ms) crosses the 800 ms stall threshold at 1200 ms:
|
|
67
|
+
// the same tick then runs the first poll and emits seq 0.
|
|
68
|
+
await vi.advanceTimersByTimeAsync(800); // 1200ms total -> first stall crossing tick
|
|
68
69
|
expect(pollLatest).toHaveBeenCalledWith('s1');
|
|
69
70
|
expect(frames).toHaveLength(1);
|
|
70
71
|
expect(frames[0]).toMatchObject({ type: 'session/event', sessionId: 's1' });
|
|
@@ -84,7 +85,7 @@ describe('MuxClient polling fallback', () => {
|
|
|
84
85
|
client.onFrame(frame => { frames.push(frame); });
|
|
85
86
|
client.start();
|
|
86
87
|
client.observe('s1');
|
|
87
|
-
await vi.advanceTimersByTimeAsync(
|
|
88
|
+
await vi.advanceTimersByTimeAsync(1200); // first poll -> 3 events
|
|
88
89
|
expect(frames).toHaveLength(3);
|
|
89
90
|
await vi.advanceTimersByTimeAsync(400); // second poll -> same page, nothing new
|
|
90
91
|
expect(frames).toHaveLength(3);
|
|
@@ -96,7 +97,7 @@ describe('MuxClient polling fallback', () => {
|
|
|
96
97
|
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
97
98
|
client.start();
|
|
98
99
|
client.observe('s1');
|
|
99
|
-
await vi.advanceTimersByTimeAsync(
|
|
100
|
+
await vi.advanceTimersByTimeAsync(1200);
|
|
100
101
|
expect(pollLatest).toHaveBeenCalled();
|
|
101
102
|
client.observe(undefined);
|
|
102
103
|
const callsAfterClear = pollLatest.mock.calls.length;
|
|
@@ -110,7 +111,7 @@ describe('MuxClient polling fallback', () => {
|
|
|
110
111
|
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
111
112
|
client.start();
|
|
112
113
|
client.observe('s1');
|
|
113
|
-
await vi.advanceTimersByTimeAsync(
|
|
114
|
+
await vi.advanceTimersByTimeAsync(1200);
|
|
114
115
|
expect(pollLatest).toHaveBeenCalled();
|
|
115
116
|
client.stop();
|
|
116
117
|
const callsAfterStop = pollLatest.mock.calls.length;
|
|
@@ -127,7 +128,7 @@ describe('MuxClient polling fallback', () => {
|
|
|
127
128
|
client.start();
|
|
128
129
|
client.observe('s1');
|
|
129
130
|
// Stall into polling.
|
|
130
|
-
await vi.advanceTimersByTimeAsync(
|
|
131
|
+
await vi.advanceTimersByTimeAsync(1200);
|
|
131
132
|
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
132
133
|
// A live mux frame proves SSE delivers again -> fallback stops.
|
|
133
134
|
sources[0]?.onmessage?.({ data: envelopeWith({ type: 'session/subscribed', sessionId: 's1', lastSeq: 4 }) });
|
|
@@ -138,4 +139,72 @@ describe('MuxClient polling fallback', () => {
|
|
|
138
139
|
expect(live[0]).toMatchObject({ type: 'session/subscribed', sessionId: 's1' });
|
|
139
140
|
client.stop();
|
|
140
141
|
});
|
|
142
|
+
it('drives the whole lifecycle on a single scheduler tick (one interval)', async () => {
|
|
143
|
+
const { factory } = makeSources();
|
|
144
|
+
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
|
|
145
|
+
try {
|
|
146
|
+
const pages = [pageOf([0]), pageOf([0, 1])];
|
|
147
|
+
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
148
|
+
const client = new MuxClient('/m/api/events.mux', {
|
|
149
|
+
sourceFactory: factory,
|
|
150
|
+
pollLatest,
|
|
151
|
+
stallThresholdMs: 1500,
|
|
152
|
+
pollIntervalMs: 2000,
|
|
153
|
+
});
|
|
154
|
+
client.start();
|
|
155
|
+
client.observe('s1');
|
|
156
|
+
// Exactly one interval is ever created: the single tick scheduler.
|
|
157
|
+
expect(setIntervalSpy).toHaveBeenCalledTimes(1);
|
|
158
|
+
// The tick fires below the stall threshold without polling.
|
|
159
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
160
|
+
expect(pollLatest).not.toHaveBeenCalled();
|
|
161
|
+
// The same interval arms the fallback once silence passes the threshold
|
|
162
|
+
// (tick at 2000 crosses 1500) and runs the first poll immediately.
|
|
163
|
+
await vi.advanceTimersByTimeAsync(1100); // 2100ms total
|
|
164
|
+
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(setIntervalSpy).toHaveBeenCalledTimes(1); // still one timer
|
|
166
|
+
// Subsequent polls ride the same tick at the poll cadence (2000 ms).
|
|
167
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
168
|
+
expect(pollLatest).toHaveBeenCalledTimes(2);
|
|
169
|
+
client.stop();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
setIntervalSpy.mockRestore();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
it('paces polls on the tick only after the stall phase ends', async () => {
|
|
176
|
+
const { factory } = makeSources();
|
|
177
|
+
const pages = [pageOf([0]), pageOf([0, 1]), pageOf([0, 1, 2]), pageOf([0, 1, 2, 3])];
|
|
178
|
+
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
179
|
+
const client = new MuxClient('/m/api/events.mux', {
|
|
180
|
+
sourceFactory: factory,
|
|
181
|
+
pollLatest,
|
|
182
|
+
stallThresholdMs: 800,
|
|
183
|
+
pollIntervalMs: 400,
|
|
184
|
+
});
|
|
185
|
+
client.start();
|
|
186
|
+
client.observe('s1');
|
|
187
|
+
// At 1200 ms the stall threshold is crossed on this single 400 ms tick.
|
|
188
|
+
await vi.advanceTimersByTimeAsync(1200);
|
|
189
|
+
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
190
|
+
// Each subsequent 400 ms tick is a poll, matching the poll cadence.
|
|
191
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
192
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
193
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
194
|
+
expect(pollLatest).toHaveBeenCalledTimes(4);
|
|
195
|
+
client.stop();
|
|
196
|
+
});
|
|
197
|
+
it('observe() already in the stall window starts the single-tick poller immediately', async () => {
|
|
198
|
+
const { factory } = makeSources();
|
|
199
|
+
const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
|
|
200
|
+
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
201
|
+
client.start();
|
|
202
|
+
// Advance well past the stall threshold while nothing is observed.
|
|
203
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
204
|
+
expect(pollLatest).not.toHaveBeenCalled();
|
|
205
|
+
// Observing a session now is already in the stall window: poll right away.
|
|
206
|
+
client.observe('s1');
|
|
207
|
+
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
208
|
+
client.stop();
|
|
209
|
+
});
|
|
141
210
|
});
|
|
@@ -29,6 +29,8 @@ export interface MobileApiDeps {
|
|
|
29
29
|
apiProxy: ApiProxy;
|
|
30
30
|
/** The resolved mobile composer preference (live per request). */
|
|
31
31
|
mobileEnterToSend: () => boolean;
|
|
32
|
+
/** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
|
|
33
|
+
eventsHeartbeatMs?: number;
|
|
32
34
|
}
|
|
33
35
|
/** Mobile API route paths. */
|
|
34
36
|
export declare const MOBILE_API_PATHS: {
|
|
@@ -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;
|
|
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"}
|
package/lib/types/mobile-api.js
CHANGED
|
@@ -40,6 +40,8 @@ const MOBILE_ALLOWLIST = new Set([
|
|
|
40
40
|
const MOBILE_PREFERENCES_METHOD = 'mobile.preferences';
|
|
41
41
|
/** One session.list page (thin phones load incrementally). */
|
|
42
42
|
const SESSION_PAGE_SIZE = 20;
|
|
43
|
+
/** SSE keep-alive ping cadence for the live mux stream (single connection). */
|
|
44
|
+
const DEFAULT_EVENTS_HEARTBEAT_MS = 15_000;
|
|
43
45
|
/** Encode one list position as an opaque continuation cursor. */
|
|
44
46
|
function sessionListCursor(updatedAt, sessionId) {
|
|
45
47
|
return `${updatedAt}:${sessionId}`;
|
|
@@ -74,6 +76,7 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
|
|
|
74
76
|
*/
|
|
75
77
|
export function makeMobileApiRoutes(deps) {
|
|
76
78
|
const { service, apiProxy, mobileEnterToSend } = deps;
|
|
79
|
+
const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;
|
|
77
80
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
78
81
|
const gateOk = (req) => {
|
|
79
82
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
@@ -167,7 +170,7 @@ export function makeMobileApiRoutes(deps) {
|
|
|
167
170
|
catch {
|
|
168
171
|
// The write failed; the close handler tears the subscription down.
|
|
169
172
|
}
|
|
170
|
-
},
|
|
173
|
+
}, eventsHeartbeatMs);
|
|
171
174
|
const onClose = () => {
|
|
172
175
|
if (closed)
|
|
173
176
|
return;
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
11
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
12
|
+
import { z } from 'zod';
|
|
12
13
|
import { type PairingService, type PairingSnapshot } from './pairing.ts';
|
|
13
14
|
/**
|
|
14
15
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
@@ -43,6 +44,22 @@ export declare const PAIR_PATHS: {
|
|
|
43
44
|
readonly status: "/api/pair/status";
|
|
44
45
|
readonly events: "/api/pair/events";
|
|
45
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* /api/pair request payload contracts. Each POST endpoint validates its body
|
|
49
|
+
* against one of these instead of reaching into a hand-parsed object: the
|
|
50
|
+
* control-plane endpoints that carry no meaningful payload use the permissive
|
|
51
|
+
* pairActionPayloadSchema so their smoke calls keep working unchanged, while
|
|
52
|
+
* issue/accept enforce their optional/required fields. Unknown (extra) keys
|
|
53
|
+
* are tolerated exactly as the previous manual reads ignored them.
|
|
54
|
+
*/
|
|
55
|
+
export declare const issuePayloadSchema: z.ZodObject<{
|
|
56
|
+
workspaceId: z.ZodOptional<z.ZodString>;
|
|
57
|
+
address: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export declare const acceptPayloadSchema: z.ZodObject<{
|
|
60
|
+
token: z.ZodDefault<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
export declare const pairActionPayloadSchema: z.ZodObject<{}, z.core.$loose>;
|
|
46
63
|
/** The SSE fan-out for desktop panel status. */
|
|
47
64
|
export declare class PairingEventsStream {
|
|
48
65
|
private readonly streams;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAGhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAGhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA4CjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;gBACS,OAAO,EAAE,cAAc;IAInC,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;IAiBrD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYrC,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CA4K3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* cookie set on accept is the device identity the api/gate listener checks
|
|
8
8
|
* on every other /api request.
|
|
9
9
|
*/
|
|
10
|
+
import { z } from 'zod';
|
|
10
11
|
import { UnknownLanAddressError } from "./pairing.js";
|
|
11
12
|
import { isLoopbackClient, readCookie } from "./gate.js";
|
|
12
13
|
/**
|
|
@@ -90,6 +91,32 @@ export const PAIR_PATHS = {
|
|
|
90
91
|
status: '/api/pair/status',
|
|
91
92
|
events: '/api/pair/events',
|
|
92
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* /api/pair request payload contracts. Each POST endpoint validates its body
|
|
96
|
+
* against one of these instead of reaching into a hand-parsed object: the
|
|
97
|
+
* control-plane endpoints that carry no meaningful payload use the permissive
|
|
98
|
+
* pairActionPayloadSchema so their smoke calls keep working unchanged, while
|
|
99
|
+
* issue/accept enforce their optional/required fields. Unknown (extra) keys
|
|
100
|
+
* are tolerated exactly as the previous manual reads ignored them.
|
|
101
|
+
*/
|
|
102
|
+
export const issuePayloadSchema = z.object({
|
|
103
|
+
workspaceId: z.string().min(1).optional(),
|
|
104
|
+
address: z.string().min(1).optional(),
|
|
105
|
+
});
|
|
106
|
+
export const acceptPayloadSchema = z.object({
|
|
107
|
+
token: z.string().default(''),
|
|
108
|
+
});
|
|
109
|
+
export const pairActionPayloadSchema = z.object({}).passthrough();
|
|
110
|
+
/**
|
|
111
|
+
* Parse a pair request body through schema. A missing/empty or non-object
|
|
112
|
+
* body is treated as an empty object (the same way the previous manual reads
|
|
113
|
+
* yielded absent fields), and a value that fails the schema returns
|
|
114
|
+
* `undefined` so the caller can answer with the existing error shape.
|
|
115
|
+
*/
|
|
116
|
+
function parsePairPayload(schema, body) {
|
|
117
|
+
const result = schema.safeParse(body ?? {});
|
|
118
|
+
return result.success ? result.data : undefined;
|
|
119
|
+
}
|
|
93
120
|
/** One JSON response. */
|
|
94
121
|
function writeJson(res, status, body) {
|
|
95
122
|
const payload = JSON.stringify(body);
|
|
@@ -205,12 +232,12 @@ export function makeRoutes(deps) {
|
|
|
205
232
|
return;
|
|
206
233
|
}
|
|
207
234
|
const body = await readJsonBody(req);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
const payload = parsePairPayload(issuePayloadSchema, body);
|
|
236
|
+
if (payload === undefined) {
|
|
237
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const { workspaceId, address } = payload;
|
|
214
241
|
try {
|
|
215
242
|
const { token, expiresAt } = service.issue(workspaceId, address);
|
|
216
243
|
// The default base is the public (tunneled) URL when configured — a
|
|
@@ -255,8 +282,12 @@ export function makeRoutes(deps) {
|
|
|
255
282
|
return;
|
|
256
283
|
}
|
|
257
284
|
const body = await readJsonBody(req);
|
|
258
|
-
const
|
|
259
|
-
|
|
285
|
+
const payload = parsePairPayload(acceptPayloadSchema, body);
|
|
286
|
+
if (payload === undefined) {
|
|
287
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const result = service.accept(payload.token);
|
|
260
291
|
if (!result.ok) {
|
|
261
292
|
writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code });
|
|
262
293
|
return;
|
|
@@ -276,7 +307,11 @@ export function makeRoutes(deps) {
|
|
|
276
307
|
writeJson(res, 403, { ok: false, code: 'forbidden' });
|
|
277
308
|
return;
|
|
278
309
|
}
|
|
279
|
-
await readJsonBody(req);
|
|
310
|
+
const body = await readJsonBody(req);
|
|
311
|
+
if (parsePairPayload(pairActionPayloadSchema, body) === undefined) {
|
|
312
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
280
315
|
service.stop();
|
|
281
316
|
writeJson(res, 200, { ok: true });
|
|
282
317
|
};
|
|
@@ -287,7 +322,11 @@ export function makeRoutes(deps) {
|
|
|
287
322
|
writeJson(res, 403, { ok: false, code: 'forbidden' });
|
|
288
323
|
return;
|
|
289
324
|
}
|
|
290
|
-
await readJsonBody(req);
|
|
325
|
+
const body = await readJsonBody(req);
|
|
326
|
+
if (parsePairPayload(pairActionPayloadSchema, body) === undefined) {
|
|
327
|
+
writeJson(res, 400, { ok: false, code: 'bad-payload' });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
291
330
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
292
331
|
if (deviceId === undefined || !service.heartbeat(deviceId)) {
|
|
293
332
|
writeJson(res, 401, { ok: false, code: 'unpaired' });
|
package/lib/types/update.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Remote update support for the dsh-web-ui family — host half. Detects the
|
|
3
3
|
* installed aggregate package (@linxin666/dsh-web-ui-all) and its family
|
|
4
4
|
* children, probes the npm registry for newer releases, and runs the actual
|
|
5
|
-
* update as `pnpm update` inside the owning dsh profile directory.
|
|
5
|
+
* update as `pnpm update --latest` inside the owning dsh profile directory.
|
|
6
6
|
*
|
|
7
7
|
* Pure logic with injected seams (manifest reading, registry fetches, process
|
|
8
8
|
* spawning) so the whole surface is unit-testable without touching disk,
|
|
@@ -100,11 +100,13 @@ export interface UpdateCheckDeps {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Resolve the anchor package's manifest path. The aggregate package is the
|
|
103
|
-
* canonical entry point; this plugin's own package is the fallback.
|
|
103
|
+
* canonical entry point; this plugin's own package is the fallback. Both a
|
|
104
|
+
* throwing resolve and an undefined return mean "not installed" and move on
|
|
105
|
+
* to the next candidate.
|
|
104
106
|
* @param resolve - a Node resolve implementation scoped to the host process.
|
|
105
107
|
* @returns the absolute manifest path, or undefined when neither is installed.
|
|
106
108
|
*/
|
|
107
|
-
export declare function resolveAnchorManifest(resolve: (specifier: string) => string): string | undefined;
|
|
109
|
+
export declare function resolveAnchorManifest(resolve: (specifier: string) => string | undefined): string | undefined;
|
|
108
110
|
/** The resolved update target: the profile pnpm runs in plus the package list. */
|
|
109
111
|
export interface UpdateTarget {
|
|
110
112
|
/** Owning profile name. */
|
|
@@ -157,7 +159,11 @@ export type UpdateErrorCode =
|
|
|
157
159
|
/** The anchor is a local link/dev install pnpm cannot update. */
|
|
158
160
|
| 'link'
|
|
159
161
|
/** pnpm exited non-zero. */
|
|
160
|
-
| 'pnpm-failed'
|
|
162
|
+
| 'pnpm-failed'
|
|
163
|
+
/** pnpm exited 0 but the installed versions did not move. */
|
|
164
|
+
| 'stale'
|
|
165
|
+
/** pnpm exited 0 but the post-run version check could not verify the install. */
|
|
166
|
+
| 'verify-failed';
|
|
161
167
|
/** Result of one update run. */
|
|
162
168
|
export interface UpdateRunResult {
|
|
163
169
|
ok: boolean;
|
|
@@ -191,5 +197,39 @@ export interface UpdateRunDeps {
|
|
|
191
197
|
* @returns the outcome with captured output.
|
|
192
198
|
*/
|
|
193
199
|
export declare function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult>;
|
|
200
|
+
/** Seam set for the verified update run (run + post-run status check). */
|
|
201
|
+
export interface UpdateRunVerifiedDeps {
|
|
202
|
+
/** The pnpm run (profile dir + package list). */
|
|
203
|
+
run: UpdateRunDeps;
|
|
204
|
+
/** The post-run status check (same seams as checkUpdates). */
|
|
205
|
+
check: UpdateCheckDeps;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
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
|
|
212
|
+
* installed versions afterwards and surface a `stale` failure (with the
|
|
213
|
+
* captured pnpm output) when nothing moved, so the panel can tell the user
|
|
214
|
+
* how to unblock the gate instead of claiming success.
|
|
215
|
+
*
|
|
216
|
+
* The stale decision anchors on the pre-run installed versions, not on the
|
|
217
|
+
* registry latest: under a lenient gate pnpm may move 0.1.12 -> 0.1.13 while
|
|
218
|
+
* latest stays 0.1.15 — that is a real update, not stale. The post-run anchor
|
|
219
|
+
* path is re-resolved rather than reused from boot time: pnpm removes the old
|
|
220
|
+
* version's .pnpm directory on update, so a boot-time captured path would
|
|
221
|
+
* fail to read and collapse a successful update into a bogus 'missing'.
|
|
222
|
+
*
|
|
223
|
+
* A green exit whose verification has nothing to compare (registry probe
|
|
224
|
+
* outage, missing anchor, non-npm mode) is reported as `verify-failed` — it
|
|
225
|
+
* must never read as success. A green exit where no package moved but the
|
|
226
|
+
* post-run check could not prove the install is fully current (a partial
|
|
227
|
+
* probe failure hides whether a gate kept something back) is also
|
|
228
|
+
* `verify-failed`: it must not collapse into a success either.
|
|
229
|
+
* @param deps - the run and check seams.
|
|
230
|
+
* @returns the run result; `stale` when exit 0 left every version in place,
|
|
231
|
+
* `verify-failed` when the post-run check could not verify anything.
|
|
232
|
+
*/
|
|
233
|
+
export declare function runUpdateVerified(deps: UpdateRunVerifiedDeps): Promise<UpdateRunResult>;
|
|
194
234
|
export {};
|
|
195
235
|
//# sourceMappingURL=update.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,oEAAoE;AACpE,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,8BAA8B,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,YAAY,iCAAiC,CAAA;AAQ1D,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CASlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5D;AAYD,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAahF;AAED,6DAA6D;AAC7D,KAAK,cAAc,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAE9D,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG1D;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,iGAAiG;IACjG,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;IAChC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAA;IACjB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9C,oEAAoE;IACpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD;AAED
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|