@linxin666/dsh-remote-web-ui 0.1.15 → 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 +58 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +110 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -1
- 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 +62 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +61 -12
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +36 -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 +7 -1
- package/src/client/UpdatePanel.tsx +2 -0
- package/src/client/locales.ts +4 -0
- package/src/client/settings-card.module.css +3 -0
- package/src/client/settings-form.ts +114 -12
- package/src/index.ts +34 -10
- package/src/update.ts +128 -9
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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"}
|
package/lib/types/update.js
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,
|
|
@@ -130,14 +130,18 @@ export function isLinkedSpec(spec) {
|
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* Resolve the anchor package's manifest path. The aggregate package is the
|
|
133
|
-
* canonical entry point; this plugin's own package is the fallback.
|
|
133
|
+
* canonical entry point; this plugin's own package is the fallback. Both a
|
|
134
|
+
* throwing resolve and an undefined return mean "not installed" and move on
|
|
135
|
+
* to the next candidate.
|
|
134
136
|
* @param resolve - a Node resolve implementation scoped to the host process.
|
|
135
137
|
* @returns the absolute manifest path, or undefined when neither is installed.
|
|
136
138
|
*/
|
|
137
139
|
export function resolveAnchorManifest(resolve) {
|
|
138
140
|
for (const name of [AGGREGATE_PACKAGE, SELF_PACKAGE]) {
|
|
139
141
|
try {
|
|
140
|
-
|
|
142
|
+
const path = resolve(name + '/package.json');
|
|
143
|
+
if (path !== undefined)
|
|
144
|
+
return path;
|
|
141
145
|
}
|
|
142
146
|
catch {
|
|
143
147
|
// Not installed — try the next candidate.
|
|
@@ -216,15 +220,22 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
216
220
|
return undefined;
|
|
217
221
|
}
|
|
218
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Sentinel for an installed version that could not be read (missing manifest
|
|
225
|
+
* or resolve failure). It is a real-looking version so `checkUpdates` can
|
|
226
|
+
* render the affected row, but the verified-update comparison must never
|
|
227
|
+
* treat it as evidence — a read failure is not a version that "moved".
|
|
228
|
+
*/
|
|
229
|
+
const VERSION_UNKNOWN = '0.0.0';
|
|
219
230
|
/** The resolved current version of one family package (probe failure tolerated). */
|
|
220
231
|
function readInstalledVersion(resolve, name) {
|
|
221
232
|
try {
|
|
222
233
|
const path = resolve(name + '/package.json');
|
|
223
234
|
const version = path === undefined ? undefined : readManifest(path)?.version;
|
|
224
|
-
return typeof version === 'string' ? version :
|
|
235
|
+
return typeof version === 'string' ? version : VERSION_UNKNOWN;
|
|
225
236
|
}
|
|
226
237
|
catch {
|
|
227
|
-
return
|
|
238
|
+
return VERSION_UNKNOWN;
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
/**
|
|
@@ -313,10 +324,14 @@ export function runUpdate(deps) {
|
|
|
313
324
|
};
|
|
314
325
|
// Ordered fallback chain: each is tried only when the previous one is
|
|
315
326
|
// missing on PATH (ENOENT on the spawn error event, never on close).
|
|
327
|
+
// `--latest` is required: dsh plugin writes exact-version specs (e.g.
|
|
328
|
+
// "0.1.12" without a range), and plain `pnpm update` treats an exact spec
|
|
329
|
+
// as pinned — it prints "Already up to date" and exits 0 without moving
|
|
330
|
+
// the installed version, so the panel would report a false success.
|
|
316
331
|
const candidates = [
|
|
317
|
-
{ command: 'pnpm', args: ['update', ...packages] },
|
|
318
|
-
{ command: 'corepack', args: ['pnpm', 'update', ...packages] },
|
|
319
|
-
{ command: 'npx', args: ['--yes', 'pnpm', 'update', ...packages] },
|
|
332
|
+
{ command: 'pnpm', args: ['update', '--latest', ...packages] },
|
|
333
|
+
{ command: 'corepack', args: ['pnpm', 'update', '--latest', ...packages] },
|
|
334
|
+
{ command: 'npx', args: ['--yes', 'pnpm', 'update', '--latest', ...packages] },
|
|
320
335
|
];
|
|
321
336
|
// `output` accumulates across candidates for UI display; `currentOutput`
|
|
322
337
|
// is reset per candidate and carries only that candidate's own diagnostics
|
|
@@ -427,3 +442,98 @@ export function runUpdate(deps) {
|
|
|
427
442
|
runCandidate(0);
|
|
428
443
|
});
|
|
429
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* 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
|
|
450
|
+
* installed versions afterwards and surface a `stale` failure (with the
|
|
451
|
+
* captured pnpm output) when nothing moved, so the panel can tell the user
|
|
452
|
+
* how to unblock the gate instead of claiming success.
|
|
453
|
+
*
|
|
454
|
+
* The stale decision anchors on the pre-run installed versions, not on the
|
|
455
|
+
* registry latest: under a lenient gate pnpm may move 0.1.12 -> 0.1.13 while
|
|
456
|
+
* latest stays 0.1.15 — that is a real update, not stale. The post-run anchor
|
|
457
|
+
* path is re-resolved rather than reused from boot time: pnpm removes the old
|
|
458
|
+
* version's .pnpm directory on update, so a boot-time captured path would
|
|
459
|
+
* fail to read and collapse a successful update into a bogus 'missing'.
|
|
460
|
+
*
|
|
461
|
+
* A green exit whose verification has nothing to compare (registry probe
|
|
462
|
+
* outage, missing anchor, non-npm mode) is reported as `verify-failed` — it
|
|
463
|
+
* must never read as success. A green exit where no package moved but the
|
|
464
|
+
* post-run check could not prove the install is fully current (a partial
|
|
465
|
+
* probe failure hides whether a gate kept something back) is also
|
|
466
|
+
* `verify-failed`: it must not collapse into a success either.
|
|
467
|
+
* @param deps - the run and check seams.
|
|
468
|
+
* @returns the run result; `stale` when exit 0 left every version in place,
|
|
469
|
+
* `verify-failed` when the post-run check could not verify anything.
|
|
470
|
+
*/
|
|
471
|
+
export async function runUpdateVerified(deps) {
|
|
472
|
+
// Pre-run snapshot: the installed versions the update starts from. Only
|
|
473
|
+
// successfully-read versions are recorded — a read failure (VERSION_UNKNOWN)
|
|
474
|
+
// is not a baseline and must not look like a version that "moved", or a
|
|
475
|
+
// green exit that left everything in place could be mistaken for success.
|
|
476
|
+
const before = new Map();
|
|
477
|
+
for (const name of deps.run.packages) {
|
|
478
|
+
const version = readInstalledVersion(deps.check.resolve, name);
|
|
479
|
+
if (version !== VERSION_UNKNOWN)
|
|
480
|
+
before.set(name, version);
|
|
481
|
+
}
|
|
482
|
+
const result = await runUpdate(deps.run);
|
|
483
|
+
if (!result.ok)
|
|
484
|
+
return result;
|
|
485
|
+
// Re-resolve the anchor after the run: pnpm removes the old version's
|
|
486
|
+
// .pnpm directory, so a boot-time captured path no longer reads. Fall back
|
|
487
|
+
// to the provided path only when nothing resolves now.
|
|
488
|
+
const anchorManifestPath = resolveAnchorManifest(deps.check.resolve) ?? deps.check.anchorManifestPath;
|
|
489
|
+
const status = await checkUpdates({ ...deps.check, anchorManifestPath });
|
|
490
|
+
// A green exit with no comparable result is not a success: registry probe
|
|
491
|
+
// outage, missing anchor, or a non-npm install mode all mean the post-run
|
|
492
|
+
// check could not verify anything.
|
|
493
|
+
if (status.error !== undefined || status.mode !== 'npm') {
|
|
494
|
+
return {
|
|
495
|
+
...result,
|
|
496
|
+
ok: false,
|
|
497
|
+
errorCode: 'verify-failed',
|
|
498
|
+
error: 'pnpm exited 0 but the post-run version check could not verify the install',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// A package "moved" only when a known pre-run version differs from a known
|
|
502
|
+
// post-run version. A read failure on either side (VERSION_UNKNOWN) is not
|
|
503
|
+
// evidence of movement and must not turn a no-op update into a success; a
|
|
504
|
+
// package without a pre-run baseline is ignored — only the packages pnpm
|
|
505
|
+
// was told to update count as evidence.
|
|
506
|
+
const moved = status.packages.some(packageStatus => {
|
|
507
|
+
const beforeVersion = before.get(packageStatus.name);
|
|
508
|
+
if (beforeVersion === undefined || packageStatus.current === VERSION_UNKNOWN)
|
|
509
|
+
return false;
|
|
510
|
+
return packageStatus.current !== beforeVersion;
|
|
511
|
+
});
|
|
512
|
+
if (moved)
|
|
513
|
+
return result;
|
|
514
|
+
// Nothing moved. A green exit is only a true no-op when the post-run check
|
|
515
|
+
// proves the install is already current: every probe succeeded and nothing
|
|
516
|
+
// is outdated. If a probe failed we cannot tell "already up to date" from
|
|
517
|
+
// "a gate silently kept it back", so report verify-failed rather than a
|
|
518
|
+
// false success.
|
|
519
|
+
if (status.outdated) {
|
|
520
|
+
// stale: the check ran, a newer release exists, and no package moved
|
|
521
|
+
// (e.g. the minimumReleaseAge gate kept everything in place).
|
|
522
|
+
return {
|
|
523
|
+
...result,
|
|
524
|
+
ok: false,
|
|
525
|
+
errorCode: 'stale',
|
|
526
|
+
error: 'pnpm exited 0 but the installed versions did not change',
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const unverifiable = status.packages.some(packageStatus => packageStatus.latest === undefined);
|
|
530
|
+
if (unverifiable) {
|
|
531
|
+
return {
|
|
532
|
+
...result,
|
|
533
|
+
ok: false,
|
|
534
|
+
errorCode: 'verify-failed',
|
|
535
|
+
error: 'pnpm exited 0 but the post-run version check could not verify the install',
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
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.
|
|
4
|
+
"version": "0.1.16",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -138,7 +138,13 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
138
138
|
{!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
|
|
139
139
|
{props.children}
|
|
140
140
|
<div className={css.footer}>
|
|
141
|
-
{state.failed
|
|
141
|
+
{state.failed
|
|
142
|
+
? (
|
|
143
|
+
<p className={css.failed} role="status">
|
|
144
|
+
{props.t('settings.saveFailed')}{state.failedReason ? ' - ' + state.failedReason : ''}
|
|
145
|
+
</p>
|
|
146
|
+
)
|
|
147
|
+
: null}
|
|
142
148
|
<button
|
|
143
149
|
type="button"
|
|
144
150
|
className={css.discard}
|
|
@@ -198,6 +198,8 @@ function errorMessageOf(t: TranslateNS<"remote">, result: UpdateRunResult): stri
|
|
|
198
198
|
case "not-found": return t("update.error.notFound")
|
|
199
199
|
case "link": return t("update.error.link")
|
|
200
200
|
case "pnpm-failed": return t("update.error.pnpmFailed", { code: String(result.exitCode ?? "?") })
|
|
201
|
+
case "stale": return t("update.error.stale")
|
|
202
|
+
case "verify-failed": return t("update.error.verifyFailed")
|
|
201
203
|
default: return result.error ?? t("update.error.unknown")
|
|
202
204
|
}
|
|
203
205
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -102,6 +102,8 @@ export const zh = {
|
|
|
102
102
|
'update.error.notFound': '未找到 dsh-web-ui 聚合包安装,无法更新。',
|
|
103
103
|
'update.error.link': '当前为本地开发模式(链接安装),无法自动更新。',
|
|
104
104
|
'update.error.pnpmFailed': '更新执行失败(pnpm 退出码 {code}),详见下方输出。',
|
|
105
|
+
'update.error.stale': '更新命令已执行,但安装版本未变化。常见原因:pnpm 11 的 minimumReleaseAge 门禁会静默跳过发布不足 24 小时的新版本。请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml 中配置 minimumReleaseAgeExclude(例如 "@linxin666/*")或 minimumReleaseAge: 0 后重新点击更新。',
|
|
106
|
+
'update.error.verifyFailed': '更新命令已执行,但无法核对安装后的版本(可能是 registry 探测失败或安装路径变化)。请查看下方输出,或稍后重试。',
|
|
105
107
|
'update.error.unknown': '更新失败,请重试。',
|
|
106
108
|
} satisfies Record<string, string>
|
|
107
109
|
|
|
@@ -213,5 +215,7 @@ export const en = {
|
|
|
213
215
|
'update.error.notFound': 'The dsh-web-ui aggregate package is not installed; nothing to update.',
|
|
214
216
|
'update.error.link': 'Local development mode (link install) — automatic update is unavailable.',
|
|
215
217
|
'update.error.pnpmFailed': 'The update failed (pnpm exited with code {code}); see the output below.',
|
|
218
|
+
'update.error.stale': 'The update command ran but the installed versions did not change. Likely cause: the pnpm 11 minimumReleaseAge gate silently skips releases published less than 24 hours ago. Add minimumReleaseAgeExclude (e.g. "@linxin666/*") or set minimumReleaseAge: 0 in pnpm-workspace.yaml under the profile directory (~/.dsh/profiles/<profile>), then run the update again.',
|
|
219
|
+
'update.error.verifyFailed': 'The update command ran but the installed versions could not be verified afterwards (registry probe failed or the install path changed). Inspect the output below, or retry later.',
|
|
216
220
|
'update.error.unknown': 'Update failed; try again.',
|
|
217
221
|
} satisfies Record<RemoteKey, string>
|
|
@@ -20,6 +20,14 @@ export type FieldWrite =
|
|
|
20
20
|
export interface FieldSpec {
|
|
21
21
|
/** Field name inside the namespace section. */
|
|
22
22
|
field: string
|
|
23
|
+
/**
|
|
24
|
+
* Whether the Host treats this field as a secret and redacts its value from
|
|
25
|
+
* the read-back (role('secret') in the section schema). Redacted secrets are
|
|
26
|
+
* never compared against the draft on save; the field lands when the scope
|
|
27
|
+
* reports the write succeeded (its secret-set marker under the bridge), so
|
|
28
|
+
* a successful secret save is not misreported as failed.
|
|
29
|
+
*/
|
|
30
|
+
secret?: boolean
|
|
23
31
|
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
24
32
|
format: (value: unknown) => string
|
|
25
33
|
/**
|
|
@@ -61,6 +69,12 @@ export interface CardShell {
|
|
|
61
69
|
saving: boolean
|
|
62
70
|
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
63
71
|
failed: boolean
|
|
72
|
+
/**
|
|
73
|
+
* The rejection code/message the Host returned for the last failed save,
|
|
74
|
+
* surfaced next to the generic failure text. Undefined while no save has
|
|
75
|
+
* failed (or the failure carried no server reason).
|
|
76
|
+
*/
|
|
77
|
+
failedReason?: string
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
/** The write actions the card's slot entry injects. */
|
|
@@ -87,10 +101,52 @@ interface StagedEdit {
|
|
|
87
101
|
interface PlannedWrite {
|
|
88
102
|
/** Field this entry writes. */
|
|
89
103
|
field: string
|
|
104
|
+
/** The durable write this entry performs, described for a batched scope. */
|
|
105
|
+
op: BatchedWrite
|
|
90
106
|
/** Perform the write and report whether the Host holds the staged value afterwards. */
|
|
91
107
|
run: (() => Promise<boolean>) | undefined
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
/** One durable write a batched settings scope performs. */
|
|
111
|
+
export interface BatchedWrite {
|
|
112
|
+
/** Field this entry writes. */
|
|
113
|
+
field: string
|
|
114
|
+
/** set stores a value; unset drops the leaf. */
|
|
115
|
+
op: 'set' | 'unset'
|
|
116
|
+
/** Value for op set (absent for unset). */
|
|
117
|
+
value?: unknown
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Per-field outcome of one batched scope write. */
|
|
121
|
+
export interface BatchedFieldResult {
|
|
122
|
+
/** Field this entry writes. */
|
|
123
|
+
field: string
|
|
124
|
+
/** Whether the Host accepted this field's write (per the read-back view). */
|
|
125
|
+
landed: boolean
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Result of a batched scope write. The bridge scope posts every planned write
|
|
130
|
+
* in one /mutate so the Host validate hook judges baseURL+model together; a
|
|
131
|
+
* batched refusal fails the whole save rather than per-field.
|
|
132
|
+
*/
|
|
133
|
+
export interface BatchResult {
|
|
134
|
+
/** Whether the whole mutate was accepted. */
|
|
135
|
+
ok: boolean
|
|
136
|
+
/** Per-field success, in the request order (always present when ok). */
|
|
137
|
+
fields: BatchedFieldResult[]
|
|
138
|
+
/** Host rejection code (mutate refused). */
|
|
139
|
+
code?: string
|
|
140
|
+
/** Host rejection message (mutate refused). */
|
|
141
|
+
message?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The optional batch surface the bridge scope adds over the SettingsScope contract. */
|
|
145
|
+
interface BatchedSettingsScope {
|
|
146
|
+
/** Write every operation in one scope mutation, reporting per-field success. */
|
|
147
|
+
mutate: (writes: BatchedWrite[]) => Promise<BatchResult>
|
|
148
|
+
}
|
|
149
|
+
|
|
94
150
|
/** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
|
|
95
151
|
export interface NumberConstraints {
|
|
96
152
|
/** The accepted value must be a whole number. */
|
|
@@ -129,6 +185,16 @@ export function textField(field: string): FieldSpec {
|
|
|
129
185
|
}
|
|
130
186
|
}
|
|
131
187
|
|
|
188
|
+
/**
|
|
189
|
+
* A free-text field the Host treats as a secret and redacts from the read-back
|
|
190
|
+
* (role('secret') in the section schema). The card still edits it like text,
|
|
191
|
+
* but a save never compares the redacted value back and relies on the scope
|
|
192
|
+
* reporting the write landed.
|
|
193
|
+
*/
|
|
194
|
+
export function secretField(field: string): FieldSpec {
|
|
195
|
+
return { ...textField(field), secret: true }
|
|
196
|
+
}
|
|
197
|
+
|
|
132
198
|
/** A boolean field, edited through true/false draft text. */
|
|
133
199
|
export function booleanField(field: string): FieldSpec {
|
|
134
200
|
return {
|
|
@@ -170,6 +236,7 @@ export class CardForm<T> {
|
|
|
170
236
|
private readonly listeners = new Set<() => void>()
|
|
171
237
|
private saving = false
|
|
172
238
|
private failed = false
|
|
239
|
+
private failedReason: string | undefined
|
|
173
240
|
|
|
174
241
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
175
242
|
constructor(
|
|
@@ -199,6 +266,7 @@ export class CardForm<T> {
|
|
|
199
266
|
invalid: plan.some(item => item.run === undefined),
|
|
200
267
|
saving: this.saving,
|
|
201
268
|
failed: this.failed,
|
|
269
|
+
...this.failedReason === undefined ? {} : { failedReason: this.failedReason },
|
|
202
270
|
}
|
|
203
271
|
}
|
|
204
272
|
|
|
@@ -229,6 +297,7 @@ export class CardForm<T> {
|
|
|
229
297
|
if (this.staged.size === 0 && !this.failed) return
|
|
230
298
|
this.staged.clear()
|
|
231
299
|
this.failed = false
|
|
300
|
+
this.failedReason = undefined
|
|
232
301
|
this.publish()
|
|
233
302
|
},
|
|
234
303
|
}
|
|
@@ -236,30 +305,57 @@ export class CardForm<T> {
|
|
|
236
305
|
|
|
237
306
|
/**
|
|
238
307
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
308
|
+
*
|
|
309
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
310
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
311
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
312
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
313
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
314
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
239
315
|
* @returns settlement after every write and the read-back.
|
|
240
316
|
*/
|
|
241
317
|
async save(): Promise<void> {
|
|
242
318
|
const plan = this.plan()
|
|
243
|
-
const
|
|
244
|
-
if (plan.length === 0 || this.saving ||
|
|
319
|
+
const valid = plan.filter(item => item.run !== undefined)
|
|
320
|
+
if (plan.length === 0 || this.saving || valid.length !== plan.length) return
|
|
321
|
+
const plannedWrites = valid.map(item => item.op)
|
|
245
322
|
// Snapshot the fields this save writes, so edits staged while it is in
|
|
246
323
|
// flight survive: only the staged keys this save actually wrote are cleared.
|
|
247
324
|
const fields = new Set(plan.map(item => item.field))
|
|
248
325
|
this.saving = true
|
|
249
326
|
this.failed = false
|
|
327
|
+
this.failedReason = undefined
|
|
250
328
|
this.publish()
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
329
|
+
const landed = new Set<string>()
|
|
330
|
+
const batch = this.batchedScope()
|
|
331
|
+
if (batch !== undefined) {
|
|
332
|
+
const result = await batch.mutate(plannedWrites)
|
|
333
|
+
if (result.ok) {
|
|
334
|
+
for (const field of result.fields) {
|
|
335
|
+
if (field.landed) landed.add(field.field)
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
this.failedReason = result.message
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
for (const item of valid) {
|
|
342
|
+
if (await item.run!()) landed.add(item.field)
|
|
343
|
+
}
|
|
254
344
|
}
|
|
255
|
-
|
|
256
|
-
|
|
345
|
+
for (const field of fields) {
|
|
346
|
+
if (landed.has(field)) this.staged.delete(field)
|
|
257
347
|
}
|
|
258
348
|
this.saving = false
|
|
259
|
-
this.failed =
|
|
349
|
+
this.failed = landed.size !== fields.size
|
|
260
350
|
this.publish()
|
|
261
351
|
}
|
|
262
352
|
|
|
353
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
354
|
+
private batchedScope(): BatchedSettingsScope | undefined {
|
|
355
|
+
const candidate = this.scope as unknown as BatchedSettingsScope | undefined
|
|
356
|
+
return typeof candidate?.mutate === 'function' ? candidate : undefined
|
|
357
|
+
}
|
|
358
|
+
|
|
263
359
|
/**
|
|
264
360
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
265
361
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -272,14 +368,14 @@ export class CardForm<T> {
|
|
|
272
368
|
for (const [field, staged] of this.staged) {
|
|
273
369
|
const spec = this.specOf(field)
|
|
274
370
|
if (staged.clear) {
|
|
275
|
-
if (this.stored(field)) plan.push({ field, run: () => this.clear(field) })
|
|
371
|
+
if (this.stored(field)) plan.push({ field, op: { field, op: 'unset' }, run: () => this.clear(field) })
|
|
276
372
|
continue
|
|
277
373
|
}
|
|
278
374
|
if (staged.text === spec.format(this.sectionValue(field))) continue
|
|
279
375
|
const write = spec.parse(staged.text)
|
|
280
|
-
if (write === undefined) plan.push({ field, run: undefined })
|
|
281
|
-
else if (write.kind === 'clear') plan.push({ field, run: () => this.clear(field) })
|
|
282
|
-
else plan.push({ field, run: () => this.store(field, write.value) })
|
|
376
|
+
if (write === undefined) plan.push({ field, op: { field, op: 'unset' }, run: undefined })
|
|
377
|
+
else if (write.kind === 'clear') plan.push({ field, op: { field, op: 'unset' }, run: () => this.clear(field) })
|
|
378
|
+
else plan.push({ field, op: { field, op: 'set', value: write.value }, run: () => this.store(field, write.value) })
|
|
283
379
|
}
|
|
284
380
|
return plan
|
|
285
381
|
}
|
|
@@ -291,12 +387,18 @@ export class CardForm<T> {
|
|
|
291
387
|
|
|
292
388
|
private async store(field: string, value: unknown): Promise<boolean> {
|
|
293
389
|
await this.scope.set(field, value)
|
|
390
|
+
// A redacted secret never appears in the user layer read-back; judging it
|
|
391
|
+
// by value would misreport a successful secret save as failed. The bridge
|
|
392
|
+
// reports secret writes through its secret-set markers (batch path); on
|
|
393
|
+
// the per-field path the scope resolved, so the write is landed.
|
|
394
|
+
if (this.specOf(field).secret) return true
|
|
294
395
|
return this.userLayer()?.[field] === value
|
|
295
396
|
}
|
|
296
397
|
|
|
297
398
|
private stage(field: string, edit: StagedEdit): void {
|
|
298
399
|
this.staged.set(field, edit)
|
|
299
400
|
this.failed = false
|
|
401
|
+
this.failedReason = undefined
|
|
300
402
|
this.publish()
|
|
301
403
|
}
|
|
302
404
|
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
fetchLatestVersion,
|
|
28
28
|
resolveAnchorManifest,
|
|
29
29
|
resolveUpdateTarget,
|
|
30
|
-
|
|
30
|
+
runUpdateVerified,
|
|
31
31
|
type UpdateRunResult,
|
|
32
32
|
} from './update.ts'
|
|
33
33
|
import { makeUpdateRoutes } from './update-routes.ts'
|
|
@@ -238,19 +238,26 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
238
238
|
}
|
|
239
239
|
// ── remote update ────────────────────────────────────────────────────────
|
|
240
240
|
// The dsh-web-ui self-update surface: probe the npm registry for family
|
|
241
|
-
// releases and run `pnpm update` in the owning profile. Resolutions
|
|
242
|
-
// on the host process's own module graph, so the update always
|
|
243
|
-
// profile the running web GUI was booted from. The
|
|
244
|
-
//
|
|
245
|
-
//
|
|
241
|
+
// releases and run `pnpm update --latest` in the owning profile. Resolutions
|
|
242
|
+
// anchor on the host process's own module graph, so the update always
|
|
243
|
+
// targets the profile the running web GUI was booted from. The anchor path
|
|
244
|
+
// is re-resolved per operation: pnpm removes the old version's .pnpm
|
|
245
|
+
// directory on update, so a boot-time captured path would fail to read
|
|
246
|
+
// after a successful update; versions are re-read from disk per check.
|
|
246
247
|
const requireFromHost = createRequire(import.meta.url)
|
|
247
|
-
const
|
|
248
|
+
const resolveAnchorPath = (): string | undefined => resolveAnchorManifest(specifier => {
|
|
249
|
+
try {
|
|
250
|
+
return requireFromHost.resolve(specifier)
|
|
251
|
+
} catch {
|
|
252
|
+
return undefined
|
|
253
|
+
}
|
|
254
|
+
})
|
|
248
255
|
const updateRoutes = makeUpdateRoutes({
|
|
249
256
|
// Control endpoints are host-surface only: a LAN/phone origin must never
|
|
250
257
|
// trigger a real install on this machine.
|
|
251
258
|
fence: request => isTrustedApiRequest(request, []),
|
|
252
259
|
check: () => checkUpdates({
|
|
253
|
-
anchorManifestPath,
|
|
260
|
+
anchorManifestPath: resolveAnchorPath(),
|
|
254
261
|
resolve: specifier => {
|
|
255
262
|
try {
|
|
256
263
|
return requireFromHost.resolve(specifier)
|
|
@@ -261,7 +268,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
261
268
|
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
262
269
|
}),
|
|
263
270
|
run: async (): Promise<UpdateRunResult> => {
|
|
264
|
-
const target = resolveUpdateTarget({ anchorManifestPath })
|
|
271
|
+
const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() })
|
|
265
272
|
if ('error' in target) {
|
|
266
273
|
const code = target.error
|
|
267
274
|
return {
|
|
@@ -272,7 +279,24 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
272
279
|
errorCode: code,
|
|
273
280
|
}
|
|
274
281
|
}
|
|
275
|
-
|
|
282
|
+
// Verify the versions actually moved after a green pnpm exit: the pnpm
|
|
283
|
+
// 11 minimumReleaseAge gate can silently keep the installed versions
|
|
284
|
+
// (same-day releases), which a plain exit-0 check would report as
|
|
285
|
+
// success — the user then restarts and nothing changed.
|
|
286
|
+
return runUpdateVerified({
|
|
287
|
+
run: { profileDir: target.profileDir, packages: target.packages },
|
|
288
|
+
check: {
|
|
289
|
+
anchorManifestPath: resolveAnchorPath(),
|
|
290
|
+
resolve: specifier => {
|
|
291
|
+
try {
|
|
292
|
+
return requireFromHost.resolve(specifier)
|
|
293
|
+
} catch {
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
298
|
+
},
|
|
299
|
+
})
|
|
276
300
|
},
|
|
277
301
|
})
|
|
278
302
|
const routes = [
|