@linxin666/dsh-remote-web-ui 0.1.10 → 0.1.11
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.md +17 -6
- package/lib/client.js +403 -14
- package/lib/client.js.map +1 -1
- package/lib/index.js +406 -17
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +9 -7
- package/lib/types/client/UpdateEntry.d.ts +14 -0
- package/lib/types/client/UpdateEntry.d.ts.map +1 -0
- package/lib/types/client/UpdateEntry.js +68 -0
- package/lib/types/client/UpdatePanel.d.ts +40 -0
- package/lib/types/client/UpdatePanel.d.ts.map +1 -0
- package/lib/types/client/UpdatePanel.js +82 -0
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +56 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +58 -0
- package/lib/types/client/update-api.d.ts +19 -0
- package/lib/types/client/update-api.d.ts.map +1 -0
- package/lib/types/client/update-api.js +27 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +45 -1
- package/lib/types/routes.d.ts +15 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +1 -1
- package/lib/types/update-routes.d.ts +30 -0
- package/lib/types/update-routes.d.ts.map +1 -0
- package/lib/types/update-routes.js +52 -0
- package/lib/types/update.d.ts +191 -0
- package/lib/types/update.d.ts.map +1 -0
- package/lib/types/update.js +333 -0
- package/package.json +1 -1
- package/src/client/RemoteEntry.tsx +12 -7
- package/src/client/UpdateEntry.tsx +96 -0
- package/src/client/UpdatePanel.tsx +203 -0
- package/src/client/index.ts +2 -0
- package/src/client/locales.ts +58 -0
- package/src/client/remote.module.css +109 -0
- package/src/client/update-api.ts +29 -0
- package/src/index.ts +51 -1
- package/src/routes.ts +1 -1
- package/src/update-routes.ts +71 -0
- package/src/update.ts +435 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote update support for the dsh-web-ui family — host half. Detects the
|
|
3
|
+
* installed aggregate package (@linxin666/dsh-web-ui-all) and its family
|
|
4
|
+
* children, probes the npm registry for newer releases, and runs the actual
|
|
5
|
+
* update as `pnpm update` inside the owning dsh profile directory.
|
|
6
|
+
*
|
|
7
|
+
* Pure logic with injected seams (manifest reading, registry fetches, process
|
|
8
|
+
* spawning) so the whole surface is unit-testable without touching disk,
|
|
9
|
+
* network, or a real profile.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
/** npm registry base used for version probes. */
|
|
13
|
+
export declare const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
14
|
+
/** The family scope every dsh-web-ui package is published under. */
|
|
15
|
+
export declare const FAMILY_SCOPE = "@linxin666/";
|
|
16
|
+
/** The aggregate package that is the canonical update entry point. */
|
|
17
|
+
export declare const AGGREGATE_PACKAGE = "@linxin666/dsh-web-ui-all";
|
|
18
|
+
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
19
|
+
export declare const SELF_PACKAGE = "@linxin666/dsh-remote-web-ui";
|
|
20
|
+
/** A parsed semantic version (prerelease identifiers kept as strings). */
|
|
21
|
+
export interface SemverParts {
|
|
22
|
+
major: number;
|
|
23
|
+
minor: number;
|
|
24
|
+
patch: number;
|
|
25
|
+
/** Dot-split prerelease identifiers; empty when absent. */
|
|
26
|
+
prerelease: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a semantic version string (leading `v` tolerated, build metadata
|
|
30
|
+
* ignored). Returns undefined for unparseable input.
|
|
31
|
+
* @param value - the version string, e.g. `0.1.10` or `0.1.11-rc.1`.
|
|
32
|
+
* @returns the parsed parts, or undefined.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseSemver(value: string): SemverParts | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Compare two semantic versions per the semver precedence rules (a release
|
|
37
|
+
* outranks any of its prereleases; numeric prerelease identifiers compare
|
|
38
|
+
* numerically and sort below alphanumeric ones). An unparseable version sorts
|
|
39
|
+
* below every parseable one; two unparseable versions compare equal.
|
|
40
|
+
* @param a - first version.
|
|
41
|
+
* @param b - second version.
|
|
42
|
+
* @returns negative when a < b, 0 when equal, positive when a > b.
|
|
43
|
+
*/
|
|
44
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
45
|
+
/** The found dsh profile owning an installed package. */
|
|
46
|
+
export interface FoundProfile {
|
|
47
|
+
/** Profile name (e.g. `web`). */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Absolute profile directory. */
|
|
50
|
+
dir: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Locate the owning dsh profile by walking up from an installed package's
|
|
54
|
+
* manifest until a manifest named `dsh-profile-*` appears (the profile
|
|
55
|
+
* directory is the first ancestor whose package.json carries that name).
|
|
56
|
+
* @param anchorManifestPath - absolute path of the anchor package.json.
|
|
57
|
+
* @returns the profile name/dir, or undefined when not profile-installed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function findProfile(anchorManifestPath: string): FoundProfile | undefined;
|
|
60
|
+
/** A package version spec in a manifest dependencies map. */
|
|
61
|
+
type DependencySpec = string | {
|
|
62
|
+
version: string;
|
|
63
|
+
} | undefined;
|
|
64
|
+
/** Whether a dependency spec is a local link/file/dev-mode install. */
|
|
65
|
+
export declare function isLinkedSpec(spec: DependencySpec): boolean;
|
|
66
|
+
/** One package's current-vs-latest comparison. */
|
|
67
|
+
export interface UpdatePackageStatus {
|
|
68
|
+
/** Package name. */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Locally installed version. */
|
|
71
|
+
current: string;
|
|
72
|
+
/** Latest npm release — undefined when the registry probe failed. */
|
|
73
|
+
latest?: string;
|
|
74
|
+
/** Whether npm carries a strictly newer release. */
|
|
75
|
+
outdated: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** The full update-status snapshot served to the browser half. */
|
|
78
|
+
export interface UpdateStatus {
|
|
79
|
+
/** npm = registry-managed (updatable); link = local dev install; missing = no anchor package. */
|
|
80
|
+
mode: 'npm' | 'link' | 'missing';
|
|
81
|
+
/** Owning profile name (npm mode). */
|
|
82
|
+
profileName?: string;
|
|
83
|
+
/** The anchor package the update targets. */
|
|
84
|
+
anchor?: string;
|
|
85
|
+
/** Per-package version comparison (anchor first). */
|
|
86
|
+
packages: UpdatePackageStatus[];
|
|
87
|
+
/** True when any package has a newer npm release. */
|
|
88
|
+
outdated: boolean;
|
|
89
|
+
/** Whole-check failure (e.g. registry unreachable). */
|
|
90
|
+
error?: string;
|
|
91
|
+
}
|
|
92
|
+
/** Dependency-injection seam for checkUpdates (testable without network). */
|
|
93
|
+
export interface UpdateCheckDeps {
|
|
94
|
+
/** Absolute path of the anchor package manifest, when resolvable. */
|
|
95
|
+
anchorManifestPath?: string;
|
|
96
|
+
/** Resolve a package.json specifier to its absolute path (host require). */
|
|
97
|
+
resolve(specifier: string): string | undefined;
|
|
98
|
+
/** Probe one package's latest npm version; undefined on failure. */
|
|
99
|
+
fetchLatest(name: string): Promise<string | undefined>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
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.
|
|
104
|
+
* @param resolve - a Node resolve implementation scoped to the host process.
|
|
105
|
+
* @returns the absolute manifest path, or undefined when neither is installed.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveAnchorManifest(resolve: (specifier: string) => string): string | undefined;
|
|
108
|
+
/** The resolved update target: the profile pnpm runs in plus the package list. */
|
|
109
|
+
export interface UpdateTarget {
|
|
110
|
+
/** Owning profile name. */
|
|
111
|
+
profileName: string;
|
|
112
|
+
/** Absolute profile directory pnpm runs in. */
|
|
113
|
+
profileDir: string;
|
|
114
|
+
/** The package names pnpm updates (anchor first). */
|
|
115
|
+
packages: string[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolve what an update would touch: the owning profile directory and the
|
|
119
|
+
* family package list. Fails with an error code when the anchor is missing
|
|
120
|
+
* ('not-found') or is a local dev install ('link').
|
|
121
|
+
* @param deps - the anchor manifest path (resolveAnchorManifest output).
|
|
122
|
+
* @returns the target, or the failure code.
|
|
123
|
+
*/
|
|
124
|
+
export declare function resolveUpdateTarget(deps: {
|
|
125
|
+
anchorManifestPath?: string;
|
|
126
|
+
}): UpdateTarget | {
|
|
127
|
+
error: 'not-found' | 'link';
|
|
128
|
+
};
|
|
129
|
+
/** Family children of the anchor: its dependencies under the family scope. */
|
|
130
|
+
export declare function familyChildren(anchorManifest: Record<string, unknown>): string[];
|
|
131
|
+
/**
|
|
132
|
+
* Probe the npm registry for one package's latest release.
|
|
133
|
+
* @param name - the package name (scope slash URL-encoded).
|
|
134
|
+
* @param fetchImpl - the fetch implementation (global fetch in the host).
|
|
135
|
+
* @param timeoutMs - probe timeout.
|
|
136
|
+
* @returns the latest version string, or undefined on any failure.
|
|
137
|
+
*/
|
|
138
|
+
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string) => Promise<{
|
|
139
|
+
ok: boolean;
|
|
140
|
+
json(): Promise<unknown>;
|
|
141
|
+
}>, timeoutMs?: number): Promise<string | undefined>;
|
|
142
|
+
/**
|
|
143
|
+
* Build the update status: locate the anchor, detect the install mode, and
|
|
144
|
+
* compare every family package against the npm registry.
|
|
145
|
+
* @param deps - manifest resolution + registry probe seams.
|
|
146
|
+
* @returns the status snapshot.
|
|
147
|
+
*/
|
|
148
|
+
export declare function checkUpdates(deps: UpdateCheckDeps): Promise<UpdateStatus>;
|
|
149
|
+
/** Structured failure codes the browser half translates. */
|
|
150
|
+
export type UpdateErrorCode =
|
|
151
|
+
/** pnpm is not on PATH. */
|
|
152
|
+
'pnpm-missing'
|
|
153
|
+
/** The install exceeded the hard timeout. */
|
|
154
|
+
| 'timeout'
|
|
155
|
+
/** The anchor package is not installed. */
|
|
156
|
+
| 'not-found'
|
|
157
|
+
/** The anchor is a local link/dev install pnpm cannot update. */
|
|
158
|
+
| 'link'
|
|
159
|
+
/** pnpm exited non-zero. */
|
|
160
|
+
| 'pnpm-failed';
|
|
161
|
+
/** Result of one update run. */
|
|
162
|
+
export interface UpdateRunResult {
|
|
163
|
+
ok: boolean;
|
|
164
|
+
/** pnpm exit code (null when the process never started or was killed). */
|
|
165
|
+
exitCode: number | null;
|
|
166
|
+
/** Captured pnpm output tail (diagnostics for the panel). */
|
|
167
|
+
output: string;
|
|
168
|
+
/** Human-readable failure description (fallback copy). */
|
|
169
|
+
error?: string;
|
|
170
|
+
/** Structured failure code (translated by the browser half). */
|
|
171
|
+
errorCode?: UpdateErrorCode;
|
|
172
|
+
}
|
|
173
|
+
/** Dependency-injection seam for runUpdate. */
|
|
174
|
+
export interface UpdateRunDeps {
|
|
175
|
+
/** The profile directory pnpm runs in. */
|
|
176
|
+
profileDir: string;
|
|
177
|
+
/** The package names pnpm updates. */
|
|
178
|
+
packages: readonly string[];
|
|
179
|
+
/** Spawn seam (defaults to child_process.spawn). */
|
|
180
|
+
spawnImpl?: typeof spawn;
|
|
181
|
+
/** Hard timeout; the child is killed on expiry. */
|
|
182
|
+
timeoutMs?: number;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Run the update: `pnpm update <packages>` inside the profile directory.
|
|
186
|
+
* @param deps - profile dir, package list, and spawn/timeout seams.
|
|
187
|
+
* @returns the outcome with captured output.
|
|
188
|
+
*/
|
|
189
|
+
export declare function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult>;
|
|
190
|
+
export {};
|
|
191
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote update support for the dsh-web-ui family — host half. Detects the
|
|
3
|
+
* installed aggregate package (@linxin666/dsh-web-ui-all) and its family
|
|
4
|
+
* children, probes the npm registry for newer releases, and runs the actual
|
|
5
|
+
* update as `pnpm update` inside the owning dsh profile directory.
|
|
6
|
+
*
|
|
7
|
+
* Pure logic with injected seams (manifest reading, registry fetches, process
|
|
8
|
+
* spawning) so the whole surface is unit-testable without touching disk,
|
|
9
|
+
* network, or a real profile.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
/** npm registry base used for version probes. */
|
|
15
|
+
export const NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
16
|
+
/** The family scope every dsh-web-ui package is published under. */
|
|
17
|
+
export const FAMILY_SCOPE = '@linxin666/';
|
|
18
|
+
/** The aggregate package that is the canonical update entry point. */
|
|
19
|
+
export const AGGREGATE_PACKAGE = '@linxin666/dsh-web-ui-all';
|
|
20
|
+
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
21
|
+
export const SELF_PACKAGE = '@linxin666/dsh-remote-web-ui';
|
|
22
|
+
/** A profile manifest `name` prefix (e.g. `dsh-profile-web`). */
|
|
23
|
+
const PROFILE_NAME_PREFIX = 'dsh-profile-';
|
|
24
|
+
/** How many ancestor directories a profile search walks before giving up. */
|
|
25
|
+
const PROFILE_WALK_DEPTH = 12;
|
|
26
|
+
/**
|
|
27
|
+
* Parse a semantic version string (leading `v` tolerated, build metadata
|
|
28
|
+
* ignored). Returns undefined for unparseable input.
|
|
29
|
+
* @param value - the version string, e.g. `0.1.10` or `0.1.11-rc.1`.
|
|
30
|
+
* @returns the parsed parts, or undefined.
|
|
31
|
+
*/
|
|
32
|
+
export function parseSemver(value) {
|
|
33
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value.trim());
|
|
34
|
+
if (match === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
return {
|
|
37
|
+
major: Number(match[1]),
|
|
38
|
+
minor: Number(match[2]),
|
|
39
|
+
patch: Number(match[3]),
|
|
40
|
+
prerelease: match[4] === undefined ? [] : match[4].split('.'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compare two semantic versions per the semver precedence rules (a release
|
|
45
|
+
* outranks any of its prereleases; numeric prerelease identifiers compare
|
|
46
|
+
* numerically and sort below alphanumeric ones). An unparseable version sorts
|
|
47
|
+
* below every parseable one; two unparseable versions compare equal.
|
|
48
|
+
* @param a - first version.
|
|
49
|
+
* @param b - second version.
|
|
50
|
+
* @returns negative when a < b, 0 when equal, positive when a > b.
|
|
51
|
+
*/
|
|
52
|
+
export function compareVersions(a, b) {
|
|
53
|
+
const pa = parseSemver(a);
|
|
54
|
+
const pb = parseSemver(b);
|
|
55
|
+
if (pa === undefined && pb === undefined)
|
|
56
|
+
return 0;
|
|
57
|
+
if (pa === undefined)
|
|
58
|
+
return -1;
|
|
59
|
+
if (pb === undefined)
|
|
60
|
+
return 1;
|
|
61
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
62
|
+
if (pa[key] !== pb[key])
|
|
63
|
+
return pa[key] < pb[key] ? -1 : 1;
|
|
64
|
+
}
|
|
65
|
+
if (pa.prerelease.length === 0 && pb.prerelease.length === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
if (pa.prerelease.length === 0)
|
|
68
|
+
return 1;
|
|
69
|
+
if (pb.prerelease.length === 0)
|
|
70
|
+
return -1;
|
|
71
|
+
for (let index = 0; index < Math.max(pa.prerelease.length, pb.prerelease.length); index++) {
|
|
72
|
+
const ra = pa.prerelease[index];
|
|
73
|
+
const rb = pb.prerelease[index];
|
|
74
|
+
if (ra === undefined)
|
|
75
|
+
return -1;
|
|
76
|
+
if (rb === undefined)
|
|
77
|
+
return 1;
|
|
78
|
+
if (ra === rb)
|
|
79
|
+
continue;
|
|
80
|
+
const numericA = /^\d+$/.test(ra);
|
|
81
|
+
const numericB = /^\d+$/.test(rb);
|
|
82
|
+
if (numericA && numericB)
|
|
83
|
+
return Number(ra) < Number(rb) ? -1 : 1;
|
|
84
|
+
// Numeric identifiers always sort below alphanumeric ones.
|
|
85
|
+
if (numericA)
|
|
86
|
+
return -1;
|
|
87
|
+
if (numericB)
|
|
88
|
+
return 1;
|
|
89
|
+
return ra < rb ? -1 : 1;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
/** Read a package.json at a path, tolerating any parse/IO failure. */
|
|
94
|
+
function readManifest(path) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
97
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Locate the owning dsh profile by walking up from an installed package's
|
|
105
|
+
* manifest until a manifest named `dsh-profile-*` appears (the profile
|
|
106
|
+
* directory is the first ancestor whose package.json carries that name).
|
|
107
|
+
* @param anchorManifestPath - absolute path of the anchor package.json.
|
|
108
|
+
* @returns the profile name/dir, or undefined when not profile-installed.
|
|
109
|
+
*/
|
|
110
|
+
export function findProfile(anchorManifestPath) {
|
|
111
|
+
let dir = dirname(anchorManifestPath);
|
|
112
|
+
for (let depth = 0; depth < PROFILE_WALK_DEPTH; depth++) {
|
|
113
|
+
const manifest = readManifest(join(dir, 'package.json'));
|
|
114
|
+
const name = manifest?.name;
|
|
115
|
+
if (typeof name === 'string' && name.startsWith(PROFILE_NAME_PREFIX)) {
|
|
116
|
+
return { name: name.slice(PROFILE_NAME_PREFIX.length), dir };
|
|
117
|
+
}
|
|
118
|
+
const parent = dirname(dir);
|
|
119
|
+
if (parent === dir)
|
|
120
|
+
return undefined;
|
|
121
|
+
dir = parent;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
/** Whether a dependency spec is a local link/file/dev-mode install. */
|
|
126
|
+
export function isLinkedSpec(spec) {
|
|
127
|
+
if (typeof spec !== 'string')
|
|
128
|
+
return false;
|
|
129
|
+
return /^(?:link|file):|^\.{1,2}(?:[/\\]|$)/.test(spec);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
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.
|
|
134
|
+
* @param resolve - a Node resolve implementation scoped to the host process.
|
|
135
|
+
* @returns the absolute manifest path, or undefined when neither is installed.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveAnchorManifest(resolve) {
|
|
138
|
+
for (const name of [AGGREGATE_PACKAGE, SELF_PACKAGE]) {
|
|
139
|
+
try {
|
|
140
|
+
return resolve(name + '/package.json');
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Not installed — try the next candidate.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve what an update would touch: the owning profile directory and the
|
|
150
|
+
* family package list. Fails with an error code when the anchor is missing
|
|
151
|
+
* ('not-found') or is a local dev install ('link').
|
|
152
|
+
* @param deps - the anchor manifest path (resolveAnchorManifest output).
|
|
153
|
+
* @returns the target, or the failure code.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveUpdateTarget(deps) {
|
|
156
|
+
const manifestPath = deps.anchorManifestPath;
|
|
157
|
+
if (manifestPath === undefined)
|
|
158
|
+
return { error: 'not-found' };
|
|
159
|
+
const manifest = readManifest(manifestPath);
|
|
160
|
+
if (manifest === undefined)
|
|
161
|
+
return { error: 'not-found' };
|
|
162
|
+
const anchor = typeof manifest.name === 'string' ? manifest.name : undefined;
|
|
163
|
+
if (anchor === undefined)
|
|
164
|
+
return { error: 'not-found' };
|
|
165
|
+
const profile = findProfile(manifestPath);
|
|
166
|
+
if (profile === undefined)
|
|
167
|
+
return { error: 'link' };
|
|
168
|
+
const profileManifest = readManifest(join(profile.dir, 'package.json'));
|
|
169
|
+
const spec = profileManifest?.dependencies?.[anchor];
|
|
170
|
+
if (isLinkedSpec(spec))
|
|
171
|
+
return { error: 'link' };
|
|
172
|
+
return {
|
|
173
|
+
profileName: profile.name,
|
|
174
|
+
profileDir: profile.dir,
|
|
175
|
+
packages: [anchor, ...familyChildren(manifest)],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** Family children of the anchor: its dependencies under the family scope. */
|
|
179
|
+
export function familyChildren(anchorManifest) {
|
|
180
|
+
const dependencies = anchorManifest.dependencies;
|
|
181
|
+
if (typeof dependencies !== 'object' || dependencies === null)
|
|
182
|
+
return [];
|
|
183
|
+
const names = [];
|
|
184
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
185
|
+
if (name.startsWith(FAMILY_SCOPE) && typeof spec === 'string')
|
|
186
|
+
names.push(name);
|
|
187
|
+
}
|
|
188
|
+
return names;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Probe the npm registry for one package's latest release.
|
|
192
|
+
* @param name - the package name (scope slash URL-encoded).
|
|
193
|
+
* @param fetchImpl - the fetch implementation (global fetch in the host).
|
|
194
|
+
* @param timeoutMs - probe timeout.
|
|
195
|
+
* @returns the latest version string, or undefined on any failure.
|
|
196
|
+
*/
|
|
197
|
+
export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
198
|
+
try {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
201
|
+
try {
|
|
202
|
+
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest');
|
|
203
|
+
if (!response.ok)
|
|
204
|
+
return undefined;
|
|
205
|
+
const body = await response.json();
|
|
206
|
+
if (typeof body !== 'object' || body === null)
|
|
207
|
+
return undefined;
|
|
208
|
+
const version = body.version;
|
|
209
|
+
return typeof version === 'string' ? version : undefined;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** The resolved current version of one family package (probe failure tolerated). */
|
|
220
|
+
function readInstalledVersion(resolve, name) {
|
|
221
|
+
try {
|
|
222
|
+
const path = resolve(name + '/package.json');
|
|
223
|
+
const version = path === undefined ? undefined : readManifest(path)?.version;
|
|
224
|
+
return typeof version === 'string' ? version : '0.0.0';
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return '0.0.0';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Build the update status: locate the anchor, detect the install mode, and
|
|
232
|
+
* compare every family package against the npm registry.
|
|
233
|
+
* @param deps - manifest resolution + registry probe seams.
|
|
234
|
+
* @returns the status snapshot.
|
|
235
|
+
*/
|
|
236
|
+
export async function checkUpdates(deps) {
|
|
237
|
+
const manifestPath = deps.anchorManifestPath;
|
|
238
|
+
if (manifestPath === undefined) {
|
|
239
|
+
return { mode: 'missing', packages: [], outdated: false };
|
|
240
|
+
}
|
|
241
|
+
const manifest = readManifest(manifestPath);
|
|
242
|
+
if (manifest === undefined) {
|
|
243
|
+
return { mode: 'missing', packages: [], outdated: false };
|
|
244
|
+
}
|
|
245
|
+
const anchor = typeof manifest.name === 'string' ? manifest.name : undefined;
|
|
246
|
+
if (anchor === undefined) {
|
|
247
|
+
return { mode: 'missing', packages: [], outdated: false };
|
|
248
|
+
}
|
|
249
|
+
// A package inside a dsh profile directory is a registry install; one that
|
|
250
|
+
// lives outside every profile (e.g. a repo checkout wired through
|
|
251
|
+
// link-profile.mjs) is a local dev install pnpm cannot update.
|
|
252
|
+
const profile = findProfile(manifestPath);
|
|
253
|
+
const profileManifest = profile === undefined ? undefined : readManifest(join(profile.dir, "package.json"));
|
|
254
|
+
const linked = profile === undefined
|
|
255
|
+
|| isLinkedSpec(profileManifest?.dependencies?.[anchor]);
|
|
256
|
+
if (profile === undefined) {
|
|
257
|
+
return { mode: 'link', packages: [], outdated: false };
|
|
258
|
+
}
|
|
259
|
+
const names = [anchor, ...familyChildren(manifest)];
|
|
260
|
+
const packages = [];
|
|
261
|
+
let probeFailures = 0;
|
|
262
|
+
for (const name of names) {
|
|
263
|
+
const latest = await deps.fetchLatest(name);
|
|
264
|
+
if (latest === undefined)
|
|
265
|
+
probeFailures++;
|
|
266
|
+
const current = readInstalledVersion(deps.resolve, name);
|
|
267
|
+
packages.push({
|
|
268
|
+
name,
|
|
269
|
+
current,
|
|
270
|
+
latest,
|
|
271
|
+
outdated: latest !== undefined && latest !== current && compareVersions(latest, current) > 0,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// Registry unreachable: every probe failed — report the outage distinctly
|
|
275
|
+
// instead of a misleading "all up to date" (the panel needs the reason).
|
|
276
|
+
const error = probeFailures === names.length && names.length > 0 ? 'registry-unreachable' : undefined;
|
|
277
|
+
return {
|
|
278
|
+
mode: linked ? 'link' : 'npm',
|
|
279
|
+
profileName: profile.name,
|
|
280
|
+
anchor,
|
|
281
|
+
packages,
|
|
282
|
+
outdated: packages.some(packageStatus => packageStatus.outdated),
|
|
283
|
+
...(error !== undefined ? { error } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** Cap on captured pnpm output (keeps error payloads bounded). */
|
|
287
|
+
const OUTPUT_CAP = 16 * 1024;
|
|
288
|
+
/**
|
|
289
|
+
* Run the update: `pnpm update <packages>` inside the profile directory.
|
|
290
|
+
* @param deps - profile dir, package list, and spawn/timeout seams.
|
|
291
|
+
* @returns the outcome with captured output.
|
|
292
|
+
*/
|
|
293
|
+
export function runUpdate(deps) {
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
const child = (deps.spawnImpl ?? spawn)('pnpm', ['update', ...deps.packages], {
|
|
296
|
+
cwd: deps.profileDir,
|
|
297
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
298
|
+
});
|
|
299
|
+
let output = '';
|
|
300
|
+
const append = (chunk) => {
|
|
301
|
+
output += chunk.toString('utf8');
|
|
302
|
+
if (output.length > OUTPUT_CAP)
|
|
303
|
+
output = output.slice(output.length - OUTPUT_CAP);
|
|
304
|
+
};
|
|
305
|
+
child.stdout?.on('data', append);
|
|
306
|
+
child.stderr?.on('data', append);
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
child.kill('SIGTERM');
|
|
309
|
+
resolve({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' });
|
|
310
|
+
}, deps.timeoutMs ?? 10 * 60_000);
|
|
311
|
+
child.on('error', (error) => {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
const missing = error.code === 'ENOENT';
|
|
314
|
+
resolve({
|
|
315
|
+
ok: false,
|
|
316
|
+
exitCode: null,
|
|
317
|
+
output,
|
|
318
|
+
error: missing ? 'pnpm not found on PATH' : error.message,
|
|
319
|
+
errorCode: missing ? 'pnpm-missing' : undefined,
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
child.on('close', (code) => {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
resolve({
|
|
325
|
+
ok: code === 0,
|
|
326
|
+
exitCode: code,
|
|
327
|
+
output,
|
|
328
|
+
error: code === 0 ? undefined : 'pnpm exited with code ' + String(code),
|
|
329
|
+
...(code === 0 ? {} : { errorCode: 'pnpm-failed' }),
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
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.11",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sidebar remote-control seat: the
|
|
3
|
-
* settings button
|
|
4
|
-
* token minting on open, the status SSE subscription,
|
|
5
|
-
* and renders the pure {@link RemotePanel} body.
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
2
|
+
* The sidebar remote-control seat: the update trigger plus the phone-icon
|
|
3
|
+
* trigger beside the settings button, and the pairing panel modal. Owns the
|
|
4
|
+
* panel behavior — token minting on open, the status SSE subscription,
|
|
5
|
+
* stop/refresh/copy — and renders the pure {@link RemotePanel} body. The
|
|
6
|
+
* update seat (the dsh-web-ui self-update flow) rides the same footer row,
|
|
7
|
+
* rendered by {@link UpdateEntry}. Component-local state per the client
|
|
8
|
+
* stack rules: nothing here survives remounts or crosses entries.
|
|
8
9
|
*/
|
|
9
10
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
10
11
|
import { createPortal } from 'react-dom'
|
|
@@ -13,6 +14,7 @@ import type { PairingPhase } from '../pairing.ts'
|
|
|
13
14
|
import { RemotePanel, type PanelState } from './RemotePanel.tsx'
|
|
14
15
|
import { copyText, issuePair, stopPair, type IssueResponse, type PairStateFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
15
16
|
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
17
|
+
import { UpdateEntry } from './UpdateEntry.tsx'
|
|
16
18
|
import css from './remote.module.css'
|
|
17
19
|
|
|
18
20
|
/** Entry props: the sidebar column state + the standard locale seat. */
|
|
@@ -164,7 +166,10 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
164
166
|
|
|
165
167
|
return (
|
|
166
168
|
<>
|
|
167
|
-
<
|
|
169
|
+
<div className={css.entryRow} data-rail={wide ? undefined : 'rail'}>
|
|
170
|
+
<UpdateEntry wide={wide} t={t} />
|
|
171
|
+
<TooltipAnchor wide={wide} label={t('entry.label')} onClick={openPanel} />
|
|
172
|
+
</div>
|
|
168
173
|
{open && createPortal((
|
|
169
174
|
<div className={css.overlay} role="presentation">
|
|
170
175
|
<div className={css.mask} aria-hidden="true" onClick={closePanel} />
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar update seat: the download trigger beside the remote-control
|
|
3
|
+
* trigger plus the update panel modal. Owns the flow — probe the registry
|
|
4
|
+
* on open, auto-run the update when a newer release exists, report the
|
|
5
|
+
* outcome (restart hint on success, translated failure on error).
|
|
6
|
+
* Component-local state per the client stack rules.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
9
|
+
import { createPortal } from "react-dom"
|
|
10
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
11
|
+
import { IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
12
|
+
import type { UpdateStatus } from '../update.ts'
|
|
13
|
+
import { fetchUpdateStatus, runUpdate } from "./update-api.ts"
|
|
14
|
+
import { UpdatePanel, type UpdateView } from "./UpdatePanel.tsx"
|
|
15
|
+
import css from "./remote.module.css"
|
|
16
|
+
|
|
17
|
+
/** Entry props: the sidebar column state + the standard locale seat. */
|
|
18
|
+
export interface UpdateEntryProps {
|
|
19
|
+
/** Whether the sidebar renders wide content (false = 56px rail). */
|
|
20
|
+
wide: boolean
|
|
21
|
+
t: TranslateNS<'remote'>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render the update trigger and panel.
|
|
26
|
+
* @param props - column state and locale seat.
|
|
27
|
+
* @returns the entry element tree.
|
|
28
|
+
*/
|
|
29
|
+
export function UpdateEntry({ wide, t }: UpdateEntryProps) {
|
|
30
|
+
const [open, setOpen] = useState(false)
|
|
31
|
+
const [view, setView] = useState<UpdateView>({ kind: "checking" })
|
|
32
|
+
const runToken = useRef(0)
|
|
33
|
+
|
|
34
|
+
const check = useCallback(async (): Promise<void> => {
|
|
35
|
+
setView({ kind: "checking" })
|
|
36
|
+
let status: UpdateStatus
|
|
37
|
+
try {
|
|
38
|
+
status = await fetchUpdateStatus()
|
|
39
|
+
} catch {
|
|
40
|
+
setView({ kind: "error", message: t("update.offline"), detail: t("update.offlineDetail") })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (status.error === "registry-unreachable") {
|
|
44
|
+
setView({ kind: "result", status })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
setView({ kind: "result", status })
|
|
48
|
+
// Auto-update: an npm install with a newer release proceeds without a
|
|
49
|
+
// second confirmation — clicking the update trigger is the intent.
|
|
50
|
+
if (status.mode !== "npm" || !status.outdated) return
|
|
51
|
+
setView({ kind: "updating", status })
|
|
52
|
+
const token = ++runToken.current
|
|
53
|
+
try {
|
|
54
|
+
const result = await runUpdate()
|
|
55
|
+
if (token !== runToken.current) return
|
|
56
|
+
setView({ kind: "done", result })
|
|
57
|
+
} catch {
|
|
58
|
+
if (token !== runToken.current) return
|
|
59
|
+
setView({ kind: "error", message: t("update.error"), detail: t("update.offlineDetail") })
|
|
60
|
+
}
|
|
61
|
+
}, [t])
|
|
62
|
+
|
|
63
|
+
const openPanel = useCallback((): void => {
|
|
64
|
+
setOpen(true)
|
|
65
|
+
void check()
|
|
66
|
+
}, [check])
|
|
67
|
+
|
|
68
|
+
const closePanel = useCallback((): void => {
|
|
69
|
+
runToken.current++
|
|
70
|
+
setOpen(false)
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
// Unmount safety: an in-flight update must not land on a dead component.
|
|
74
|
+
useEffect(() => () => { runToken.current++ }, [])
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className={css.trigger}
|
|
81
|
+
data-wide={wide ? undefined : "rail"}
|
|
82
|
+
aria-label={t("update.label")}
|
|
83
|
+
title={t("update.label")}
|
|
84
|
+
onClick={openPanel}
|
|
85
|
+
>
|
|
86
|
+
<IconDownloadOutline16 size={wide ? 16 : 18} />
|
|
87
|
+
</button>
|
|
88
|
+
{open && createPortal((
|
|
89
|
+
<div className={css.overlay} role="presentation">
|
|
90
|
+
<div className={css.mask} aria-hidden="true" onClick={closePanel} />
|
|
91
|
+
<UpdatePanel t={t} view={view} onClose={closePanel} onRecheck={() => { void check() }} />
|
|
92
|
+
</div>
|
|
93
|
+
), document.body)}
|
|
94
|
+
</>
|
|
95
|
+
)
|
|
96
|
+
}
|