dsh-update-status 0.1.7 → 0.1.9
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/CHANGELOG.md +26 -0
- package/README.md +17 -6
- package/README.zh.md +17 -6
- package/lib/client.js +29 -0
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +75 -3
- package/lib/index.js +348 -29
- package/package.json +5 -3
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import z from "@deepseek-ai/schemastery";
|
|
2
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
//#region src/shared/types.d.ts
|
|
4
4
|
declare const RELEASE_CHANNELS: readonly ['latest', 'next', 'alpha'];
|
|
5
5
|
type ReleaseChannel = (typeof RELEASE_CHANNELS)[number];
|
|
@@ -38,6 +38,18 @@ type UpdateWarning = {
|
|
|
38
38
|
code: 'preview-unverified';
|
|
39
39
|
channel: ReleaseChannel;
|
|
40
40
|
version: string;
|
|
41
|
+
} |
|
|
42
|
+
/**
|
|
43
|
+
* The Host resolved a `@deepseek-ai/schemastery` that DSH does not ship, so the
|
|
44
|
+
* Loader's volatile projection is unavailable and the preference fields cannot
|
|
45
|
+
* be marked volatile. Advisory: the version answer itself is complete, and the
|
|
46
|
+
* text carries the exact directory to remove. See `host/schemastery.ts`.
|
|
47
|
+
*/
|
|
48
|
+
{
|
|
49
|
+
code: 'stale-schemastery';
|
|
50
|
+
version: string | null;
|
|
51
|
+
path: string;
|
|
52
|
+
nodeModulesDir: string | null;
|
|
41
53
|
};
|
|
42
54
|
interface ChannelRelease {
|
|
43
55
|
channel: ReleaseChannel;
|
|
@@ -98,7 +110,8 @@ interface UpdateStatusConfig {
|
|
|
98
110
|
* The explicit two-argument annotation is load-bearing: a `.volatile()` field's
|
|
99
111
|
* output is a stable reference (`Volatile<T>`) rather than the bare value the
|
|
100
112
|
* input side takes, so the inferred schema type cannot be named by the emitted
|
|
101
|
-
* `.d.ts` (TS2883) without stating the input side here.
|
|
113
|
+
* `.d.ts` (TS2883) without stating the input side here. The builder preserves
|
|
114
|
+
* that contract even on the degraded path, where no field is volatile.
|
|
102
115
|
*/
|
|
103
116
|
export declare const Config: z<UpdateStatusConfig, Record<string, unknown>>;
|
|
104
117
|
//#endregion
|
|
@@ -123,6 +136,11 @@ interface UpdateStatusServiceOptions {
|
|
|
123
136
|
now?: () => number;
|
|
124
137
|
ttlMs?: number;
|
|
125
138
|
releaseUrl?: string;
|
|
139
|
+
/**
|
|
140
|
+
* Facts about this process's own runtime, appended to every status. The schema
|
|
141
|
+
* resolution is the only producer today; the registry read never sets these.
|
|
142
|
+
*/
|
|
143
|
+
runtimeWarnings?: readonly UpdateWarning[];
|
|
126
144
|
}
|
|
127
145
|
export declare class UpdateStatusService {
|
|
128
146
|
private readonly installation;
|
|
@@ -130,6 +148,7 @@ export declare class UpdateStatusService {
|
|
|
130
148
|
private readonly now;
|
|
131
149
|
private readonly ttlMs;
|
|
132
150
|
private readonly releaseUrl;
|
|
151
|
+
private readonly runtimeWarnings;
|
|
133
152
|
private cache;
|
|
134
153
|
private inFlight;
|
|
135
154
|
constructor(options: UpdateStatusServiceOptions);
|
|
@@ -142,6 +161,59 @@ export declare class UpdateStatusService {
|
|
|
142
161
|
private statusWithoutRemoteRelease;
|
|
143
162
|
}
|
|
144
163
|
//#endregion
|
|
164
|
+
//#region src/host/schemastery.d.ts
|
|
165
|
+
/** Where a candidate copy of schemastery was looked for. */
|
|
166
|
+
type SchemaCandidateSource = 'dsh-install' | 'profile-peers' | 'profile-local' | 'plugin-local';
|
|
167
|
+
interface SchemaCandidate {
|
|
168
|
+
source: SchemaCandidateSource;
|
|
169
|
+
/** Absolute file path `createRequire` anchors on; the file need not exist. */
|
|
170
|
+
anchor: string;
|
|
171
|
+
}
|
|
172
|
+
interface SchemaCandidateReport {
|
|
173
|
+
source: SchemaCandidateSource;
|
|
174
|
+
anchor: string;
|
|
175
|
+
/** Absolute path of the module that loaded, or null when it did not load. */
|
|
176
|
+
path: string | null;
|
|
177
|
+
version: string | null;
|
|
178
|
+
/** The loaded module exposes `volatile()`. */
|
|
179
|
+
volatile: boolean;
|
|
180
|
+
/** Why this candidate was skipped, or null when it loaded. */
|
|
181
|
+
error: string | null;
|
|
182
|
+
}
|
|
183
|
+
interface SchemaRuntime {
|
|
184
|
+
/** The loaded schemastery factory — structurally the package's default export. */
|
|
185
|
+
z: unknown;
|
|
186
|
+
volatile: boolean;
|
|
187
|
+
source: SchemaCandidateSource;
|
|
188
|
+
path: string | null;
|
|
189
|
+
version: string | null;
|
|
190
|
+
/** Every candidate that was tried, in order, for diagnostics. */
|
|
191
|
+
candidates: SchemaCandidateReport[];
|
|
192
|
+
/** Set when the resolved copy cannot project volatile preferences. */
|
|
193
|
+
warning: UpdateWarning | null;
|
|
194
|
+
}
|
|
195
|
+
interface ResolveSchemaRuntimeOptions {
|
|
196
|
+
/** Override the candidate order; used by tests and by an embedding host. */
|
|
197
|
+
candidates?: readonly SchemaCandidate[];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve schemastery once, preferring the copy that can actually do the job.
|
|
201
|
+
*
|
|
202
|
+
* Only a total miss throws, and only with the per-candidate reasons attached:
|
|
203
|
+
* that state means the plugin is not running inside a working DSH installation,
|
|
204
|
+
* so there is no schema system to build a settings form with. Every other
|
|
205
|
+
* outcome loads.
|
|
206
|
+
*/
|
|
207
|
+
export declare function resolveSchemaRuntime(options?: ResolveSchemaRuntimeOptions): SchemaRuntime;
|
|
208
|
+
/**
|
|
209
|
+
* The process-wide resolution, computed on first use.
|
|
210
|
+
*
|
|
211
|
+
* The settings schema is built at module scope — the Loader reads the exported
|
|
212
|
+
* `Config` before `apply` runs — so the resolution cannot wait for a Host
|
|
213
|
+
* context. Every input here comes from the process environment or the filesystem.
|
|
214
|
+
*/
|
|
215
|
+
export declare function schemaRuntime(): SchemaRuntime;
|
|
216
|
+
//#endregion
|
|
145
217
|
//#region src/index.d.ts
|
|
146
218
|
export declare const name = "dsh-update-status";
|
|
147
219
|
/** Connection supplies the authenticated transport; settings remains optional. */
|
|
@@ -149,4 +221,4 @@ export declare const inject: string[];
|
|
|
149
221
|
export type Config = UpdateStatusConfig;
|
|
150
222
|
export declare function apply(ctx: Context, config?: Config): void;
|
|
151
223
|
//#endregion
|
|
152
|
-
export type { InstallKind, UpdateStatus };
|
|
224
|
+
export type { InstallKind, SchemaRuntime, UpdateStatus };
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
-
import { dirname, join, normalize, sep } from "node:path";
|
|
4
|
-
import
|
|
3
|
+
import { basename, dirname, join, normalize, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { homedir } from "node:os";
|
|
5
6
|
//#region src/shared/types.ts
|
|
6
7
|
const PACKAGE_NAME = "@deepseek-ai/dsh";
|
|
7
8
|
/** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */
|
|
@@ -12,7 +13,11 @@ const UPDATE_STATUS_CHANNEL = "/api";
|
|
|
12
13
|
* release the registry reports stays `unverified` — the plugin never claims a
|
|
13
14
|
* compatibility nobody checked.
|
|
14
15
|
*/
|
|
15
|
-
const VERIFIED_DSH_VERSIONS = [
|
|
16
|
+
const VERIFIED_DSH_VERSIONS = [
|
|
17
|
+
"0.1.7-alpha.2",
|
|
18
|
+
"0.1.7-rc.1",
|
|
19
|
+
"0.1.7-rc.2"
|
|
20
|
+
];
|
|
16
21
|
const RELEASE_CHANNELS = [
|
|
17
22
|
"latest",
|
|
18
23
|
"next",
|
|
@@ -47,7 +52,7 @@ function manifestAt(path) {
|
|
|
47
52
|
return;
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
|
-
function realPath(path) {
|
|
55
|
+
function realPath$1(path) {
|
|
51
56
|
try {
|
|
52
57
|
return realpathSync(path);
|
|
53
58
|
} catch {
|
|
@@ -56,14 +61,14 @@ function realPath(path) {
|
|
|
56
61
|
}
|
|
57
62
|
/** Ascend from an entry file until the owning DSH package manifest is found. */
|
|
58
63
|
function manifestFromEntry(entryPath) {
|
|
59
|
-
let directory = dirname(realPath(entryPath));
|
|
64
|
+
let directory = dirname(realPath$1(entryPath));
|
|
60
65
|
for (let depth = 0; depth < 10; depth += 1) {
|
|
61
66
|
const manifest = join(directory, "package.json");
|
|
62
67
|
if (existsSync(manifest)) {
|
|
63
68
|
const found = manifestAt(manifest);
|
|
64
69
|
if (found !== void 0) return {
|
|
65
70
|
...found,
|
|
66
|
-
root: realPath(found.root)
|
|
71
|
+
root: realPath$1(found.root)
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
74
|
const parent = dirname(directory);
|
|
@@ -76,7 +81,7 @@ function manifestFromResolver(resolve) {
|
|
|
76
81
|
const found = manifestAt(resolve(PACKAGE_NAME + "/package.json"));
|
|
77
82
|
if (found !== void 0) return {
|
|
78
83
|
...found,
|
|
79
|
-
root: realPath(found.root)
|
|
84
|
+
root: realPath$1(found.root)
|
|
80
85
|
};
|
|
81
86
|
} catch {}
|
|
82
87
|
try {
|
|
@@ -110,7 +115,7 @@ function ownResolver(selfUrl) {
|
|
|
110
115
|
*/
|
|
111
116
|
function classifyInstallRoot(packageRoot, nodePrefix = process.execPath) {
|
|
112
117
|
if (packageRoot === void 0) return "unknown";
|
|
113
|
-
const root = normalize(realPath(packageRoot)).split(sep).join("/");
|
|
118
|
+
const root = normalize(realPath$1(packageRoot)).split(sep).join("/");
|
|
114
119
|
const prefix = nodePrefix === void 0 ? "" : normalize(dirname(dirname(nodePrefix))).split(sep).join("/");
|
|
115
120
|
if (!root.includes("/node_modules/") && /\/apps\/cli(?:\/|$)/.test(root)) return "source-checkout";
|
|
116
121
|
if (root.includes("/.pnpm/") || /\/pnpm\/global(?:\/|$)/.test(root)) return "pnpm-global";
|
|
@@ -253,6 +258,278 @@ function installUpdateStatusRpc(ctx, service) {
|
|
|
253
258
|
});
|
|
254
259
|
}
|
|
255
260
|
//#endregion
|
|
261
|
+
//#region src/host/schemastery.ts
|
|
262
|
+
/**
|
|
263
|
+
* dsh-update-status — shadow-proof resolution of the platform's schemastery.
|
|
264
|
+
*
|
|
265
|
+
* `@deepseek-ai/schemastery` is a peer dependency: DSH ships it, and DSH's copy
|
|
266
|
+
* is the one carrying `volatile()` — the Loader's volatile-field projection this
|
|
267
|
+
* plugin's settings form is built on. A plain
|
|
268
|
+
* `import z from '@deepseek-ai/schemastery'` is resolved by Node's own directory
|
|
269
|
+
* walk *from the importing file*, so any copy sitting next to the plugin wins
|
|
270
|
+
* over the platform's copy. That is not hypothetical: an unmanaged tree left
|
|
271
|
+
* inside the install directory — a dev `node_modules` copied in by a
|
|
272
|
+
* local-directory install, which pnpm never removes — made `lib/index.js` throw
|
|
273
|
+
* `TypeError: …volatile is not a function` while it was being imported, so the
|
|
274
|
+
* whole host half disappeared before one line of plugin code ran.
|
|
275
|
+
*
|
|
276
|
+
* Nothing inside the plugin can repair that state: the failure precedes every
|
|
277
|
+
* plugin entry point. The resolution itself is made immune instead, trying
|
|
278
|
+
* platform copies before anything else:
|
|
279
|
+
*
|
|
280
|
+
* 1. the DSH installation running this process,
|
|
281
|
+
* 2. the profile peer farm DSH maintains (`$DSH_HOME/profiles/node_modules`),
|
|
282
|
+
* 3. the active profile's own tree,
|
|
283
|
+
* 4. whatever Node's walk finds next to this file — last resort.
|
|
284
|
+
*
|
|
285
|
+
* The first candidate that actually exposes `volatile()` wins, and the
|
|
286
|
+
* capability is verified rather than assumed. When no candidate provides it, the
|
|
287
|
+
* plugin still loads on the best copy it could load and reports exactly what it
|
|
288
|
+
* resolved: the missing capability becomes a `stale-schemastery` warning the
|
|
289
|
+
* panel renders instead of a dead plugin.
|
|
290
|
+
*/
|
|
291
|
+
const SCHEMASTERY_NAME = "@deepseek-ai/schemastery";
|
|
292
|
+
/** Anchor file name; it never has to exist, only to give `createRequire` a base. */
|
|
293
|
+
const PROBE_FILE = "dsh-update-status-peer-probe.cjs";
|
|
294
|
+
const MAX_ASCENT = 10;
|
|
295
|
+
function realPath(path) {
|
|
296
|
+
try {
|
|
297
|
+
return realpathSync(path);
|
|
298
|
+
} catch {
|
|
299
|
+
return path;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function boundedMessage$1(error) {
|
|
303
|
+
const trimmed = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
304
|
+
return trimmed === "" ? "unknown error" : trimmed.slice(0, 220);
|
|
305
|
+
}
|
|
306
|
+
function environmentDirectory(name) {
|
|
307
|
+
const value = process.env[name];
|
|
308
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
309
|
+
}
|
|
310
|
+
/** The DSH home directory, from the environment first and `~/.dsh` as a fallback. */
|
|
311
|
+
function dshHomeDirectory() {
|
|
312
|
+
const configured = environmentDirectory("DSH_HOME");
|
|
313
|
+
if (configured !== null) return configured;
|
|
314
|
+
try {
|
|
315
|
+
return join(homedir(), ".dsh");
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** The active profile directory, from `$DSH_PROFILE_DIR` or `$DSH_HOME/profiles/<profile>`. */
|
|
321
|
+
function profileDirectory() {
|
|
322
|
+
const configured = environmentDirectory("DSH_PROFILE_DIR");
|
|
323
|
+
if (configured !== null) return configured;
|
|
324
|
+
const home = dshHomeDirectory();
|
|
325
|
+
const profile = environmentDirectory("DSH_PROFILE");
|
|
326
|
+
return home === null || profile === null ? null : join(home, "profiles", profile);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Anchor on the DSH package that owns the running entry point.
|
|
330
|
+
*
|
|
331
|
+
* The entry is the CLI file Node was started with, so ascending from it reaches
|
|
332
|
+
* the DSH installation whose `node_modules` holds the platform's schemastery.
|
|
333
|
+
* Reusing `manifestFromEntry` keeps this probe identical to the installation
|
|
334
|
+
* detection the status read already performs.
|
|
335
|
+
*/
|
|
336
|
+
function dshInstallAnchor() {
|
|
337
|
+
const entry = process.argv[1];
|
|
338
|
+
if (typeof entry !== "string" || entry === "") return null;
|
|
339
|
+
const found = manifestFromEntry(entry);
|
|
340
|
+
return found === void 0 || found.root === "" ? null : join(found.root, "package.json");
|
|
341
|
+
}
|
|
342
|
+
/** Candidate order: platform copies first, the shadowable walk last. */
|
|
343
|
+
function defaultSchemaCandidates() {
|
|
344
|
+
const candidates = [];
|
|
345
|
+
const install = dshInstallAnchor();
|
|
346
|
+
if (install !== null) candidates.push({
|
|
347
|
+
source: "dsh-install",
|
|
348
|
+
anchor: install
|
|
349
|
+
});
|
|
350
|
+
const home = dshHomeDirectory();
|
|
351
|
+
if (home !== null) candidates.push({
|
|
352
|
+
source: "profile-peers",
|
|
353
|
+
anchor: join(home, "profiles", PROBE_FILE)
|
|
354
|
+
});
|
|
355
|
+
const profile = profileDirectory();
|
|
356
|
+
if (profile !== null) candidates.push({
|
|
357
|
+
source: "profile-local",
|
|
358
|
+
anchor: join(profile, PROBE_FILE)
|
|
359
|
+
});
|
|
360
|
+
candidates.push({
|
|
361
|
+
source: "plugin-local",
|
|
362
|
+
anchor: fileURLToPath(import.meta.url)
|
|
363
|
+
});
|
|
364
|
+
return candidates;
|
|
365
|
+
}
|
|
366
|
+
function manifestVersion(manifestPath) {
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
369
|
+
if (parsed.name !== "@deepseek-ai/schemastery" || typeof parsed.version !== "string") return null;
|
|
370
|
+
const version = parsed.version.trim();
|
|
371
|
+
return version === "" ? null : version;
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Read the version of the schemastery package that owns a resolved entry path. */
|
|
377
|
+
function versionOfResolved(resolvedPath) {
|
|
378
|
+
let directory = dirname(realPath(resolvedPath));
|
|
379
|
+
for (let depth = 0; depth < MAX_ASCENT; depth += 1) {
|
|
380
|
+
const manifestPath = join(directory, "package.json");
|
|
381
|
+
if (existsSync(manifestPath)) {
|
|
382
|
+
const version = manifestVersion(manifestPath);
|
|
383
|
+
if (version !== null) return version;
|
|
384
|
+
}
|
|
385
|
+
const parent = dirname(directory);
|
|
386
|
+
if (parent === directory) break;
|
|
387
|
+
directory = parent;
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
/** The nearest `node_modules` directory above a resolved package entry. */
|
|
392
|
+
function nodeModulesDirectoryOf(resolvedPath) {
|
|
393
|
+
if (resolvedPath === null) return null;
|
|
394
|
+
let directory = dirname(realPath(resolvedPath));
|
|
395
|
+
for (let depth = 0; depth < MAX_ASCENT; depth += 1) {
|
|
396
|
+
if (basename(directory) === "node_modules") return directory;
|
|
397
|
+
const parent = dirname(directory);
|
|
398
|
+
if (parent === directory) break;
|
|
399
|
+
directory = parent;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Does this module expose the Loader's volatile projection?
|
|
405
|
+
*
|
|
406
|
+
* The probe builds a throwaway field because the capability is a prototype
|
|
407
|
+
* method, not a flag: 3.18.2 has no `volatile` at all, 3.18.4 (the copy DSH
|
|
408
|
+
* ships) does. Never throws — an unusable module is simply not a candidate.
|
|
409
|
+
*/
|
|
410
|
+
function supportsVolatile(factory) {
|
|
411
|
+
if (factory === null || typeof factory !== "object" && typeof factory !== "function") return false;
|
|
412
|
+
try {
|
|
413
|
+
const boolean = factory.boolean;
|
|
414
|
+
if (typeof boolean !== "function") return false;
|
|
415
|
+
const field = boolean.call(factory);
|
|
416
|
+
if (field === null || field === void 0 || typeof field.default !== "function") return false;
|
|
417
|
+
const defined = field.default(true);
|
|
418
|
+
return defined !== null && defined !== void 0 && typeof defined.volatile === "function";
|
|
419
|
+
} catch {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** Can this value build a schema? A wrapper object that cannot is never chosen. */
|
|
424
|
+
function isSchemaFactory(value) {
|
|
425
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") return false;
|
|
426
|
+
const record = value;
|
|
427
|
+
return typeof record.object === "function" && typeof record.boolean === "function" && typeof record.number === "function";
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Pick the factory out of the shapes a CommonJS build can expose.
|
|
431
|
+
*
|
|
432
|
+
* The package assigns `module.exports` directly, but a bundler or an interop
|
|
433
|
+
* wrapper can put the same factory behind `default`. A shape that cannot build a
|
|
434
|
+
* schema is never chosen even as a fallback, because the degraded path still has
|
|
435
|
+
* to produce a `Config` — a wrapper object would throw while the entry loads,
|
|
436
|
+
* which is the failure this module exists to prevent.
|
|
437
|
+
*/
|
|
438
|
+
function factoryOf(loaded) {
|
|
439
|
+
const shapes = [loaded, (loaded !== null && (typeof loaded === "object" || typeof loaded === "function") ? loaded : void 0)?.default].filter(isSchemaFactory);
|
|
440
|
+
return shapes.find(supportsVolatile) ?? shapes[0];
|
|
441
|
+
}
|
|
442
|
+
function loadCandidate(candidate) {
|
|
443
|
+
const report = {
|
|
444
|
+
source: candidate.source,
|
|
445
|
+
anchor: candidate.anchor,
|
|
446
|
+
path: null,
|
|
447
|
+
version: null,
|
|
448
|
+
volatile: false,
|
|
449
|
+
error: null
|
|
450
|
+
};
|
|
451
|
+
try {
|
|
452
|
+
const require = createRequire(candidate.anchor);
|
|
453
|
+
const resolved = require.resolve(SCHEMASTERY_NAME);
|
|
454
|
+
const factory = factoryOf(require(resolved));
|
|
455
|
+
if (factory === void 0) throw new Error("module does not export a schemastery factory");
|
|
456
|
+
report.path = realPath(resolved);
|
|
457
|
+
report.version = versionOfResolved(resolved);
|
|
458
|
+
report.volatile = supportsVolatile(factory);
|
|
459
|
+
return {
|
|
460
|
+
report,
|
|
461
|
+
factory
|
|
462
|
+
};
|
|
463
|
+
} catch (error) {
|
|
464
|
+
report.error = boundedMessage$1(error);
|
|
465
|
+
return {
|
|
466
|
+
report,
|
|
467
|
+
factory: void 0
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Resolve schemastery once, preferring the copy that can actually do the job.
|
|
473
|
+
*
|
|
474
|
+
* Only a total miss throws, and only with the per-candidate reasons attached:
|
|
475
|
+
* that state means the plugin is not running inside a working DSH installation,
|
|
476
|
+
* so there is no schema system to build a settings form with. Every other
|
|
477
|
+
* outcome loads.
|
|
478
|
+
*/
|
|
479
|
+
function resolveSchemaRuntime(options = {}) {
|
|
480
|
+
const candidates = options.candidates ?? defaultSchemaCandidates();
|
|
481
|
+
const reports = [];
|
|
482
|
+
let usable;
|
|
483
|
+
let loaded;
|
|
484
|
+
for (const candidate of candidates) {
|
|
485
|
+
const attempt = loadCandidate(candidate);
|
|
486
|
+
reports.push(attempt.report);
|
|
487
|
+
if (attempt.factory === void 0) continue;
|
|
488
|
+
loaded ??= attempt;
|
|
489
|
+
if (attempt.report.volatile) {
|
|
490
|
+
usable = attempt;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const chosen = usable ?? loaded;
|
|
495
|
+
if (chosen === void 0) {
|
|
496
|
+
const tried = reports.map((report) => `${report.source}: ${report.error ?? "not loadable"}`).join("; ");
|
|
497
|
+
throw new Error(`dsh-update-status could not resolve ${SCHEMASTERY_NAME} (${tried})`);
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
z: chosen.factory,
|
|
501
|
+
volatile: chosen.report.volatile,
|
|
502
|
+
source: chosen.report.source,
|
|
503
|
+
path: chosen.report.path,
|
|
504
|
+
version: chosen.report.version,
|
|
505
|
+
candidates: reports,
|
|
506
|
+
warning: chosen.report.volatile ? null : {
|
|
507
|
+
code: "stale-schemastery",
|
|
508
|
+
version: chosen.report.version,
|
|
509
|
+
path: chosen.report.path ?? chosen.report.anchor,
|
|
510
|
+
nodeModulesDir: nodeModulesDirectoryOf(chosen.report.path)
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
let resolved;
|
|
515
|
+
/**
|
|
516
|
+
* The process-wide resolution, computed on first use.
|
|
517
|
+
*
|
|
518
|
+
* The settings schema is built at module scope — the Loader reads the exported
|
|
519
|
+
* `Config` before `apply` runs — so the resolution cannot wait for a Host
|
|
520
|
+
* context. Every input here comes from the process environment or the filesystem.
|
|
521
|
+
*/
|
|
522
|
+
function schemaRuntime() {
|
|
523
|
+
resolved ??= resolveSchemaRuntime();
|
|
524
|
+
return resolved;
|
|
525
|
+
}
|
|
526
|
+
/** English, single-line summary of a resolution, for the Host log. */
|
|
527
|
+
function describeSchemaRuntime(runtime) {
|
|
528
|
+
const where = runtime.path === null ? runtime.source : `${runtime.path} (${runtime.source})`;
|
|
529
|
+
const version = runtime.version === null ? "" : `@${runtime.version}`;
|
|
530
|
+
return `${SCHEMASTERY_NAME}${version} from ${where}; volatile preferences ${runtime.volatile ? "available" : "unavailable"}`;
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
256
533
|
//#region src/shared/semver.ts
|
|
257
534
|
const SEMVER = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
258
535
|
const NUMERIC_IDENTIFIER = /^(0|[1-9]\d*)$/;
|
|
@@ -313,20 +590,34 @@ function boundedMessage(error) {
|
|
|
313
590
|
const trimmed = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
314
591
|
return trimmed === "" ? "unknown error" : trimmed.slice(0, 220);
|
|
315
592
|
}
|
|
316
|
-
|
|
593
|
+
/** English, single-line rendering of one warning; the Host log uses it directly. */
|
|
594
|
+
function describeWarning(warning) {
|
|
317
595
|
switch (warning.code) {
|
|
318
596
|
case "registry-unavailable": return `Unable to check the npm registry: ${warning.detail}`;
|
|
319
597
|
case "channel-unavailable": return `The npm registry does not publish a ${warning.channel} channel.`;
|
|
320
598
|
case "version-incomparable": return `Unable to compare the current version ${warning.currentVersion} with ${warning.channel} channel version ${warning.selectedVersion} using SemVer.`;
|
|
321
599
|
case "preview-unverified": return `${warning.channel} is a preview channel; version ${warning.version} has not been verified as compatible with this plugin.`;
|
|
600
|
+
case "stale-schemastery": {
|
|
601
|
+
const version = warning.version === null ? "" : ` ${warning.version}`;
|
|
602
|
+
const remedy = warning.nodeModulesDir === null ? `Remove the stale ${SCHEMASTERY_NAME} directory there and restart DSH.` : `Remove the stale copy and restart DSH: rm -rf ${warning.nodeModulesDir}`;
|
|
603
|
+
return `This plugin resolved ${SCHEMASTERY_NAME}${version} from ${warning.path} instead of the copy DSH provides, so preference fields cannot be marked volatile. ${remedy}`;
|
|
604
|
+
}
|
|
322
605
|
}
|
|
323
606
|
}
|
|
324
607
|
function warningFallback(warnings) {
|
|
325
|
-
return warnings.length === 0 ? null : warnings.map(
|
|
608
|
+
return warnings.length === 0 ? null : warnings.map(describeWarning).join(" ");
|
|
326
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Advisory codes leave the chip alone; everything else is a failed read.
|
|
612
|
+
*
|
|
613
|
+
* `stale-schemastery` belongs here: the version answer is complete and usable,
|
|
614
|
+
* and only the settings form is degraded, so repainting the brand row would
|
|
615
|
+
* misreport a plugin-runtime fact as a failed update check.
|
|
616
|
+
*/
|
|
617
|
+
const ADVISORY_CODES = ["preview-unverified", "stale-schemastery"];
|
|
327
618
|
function warningKindOf(warnings) {
|
|
328
619
|
if (warnings.length === 0) return null;
|
|
329
|
-
return warnings.every((warning) => warning.code
|
|
620
|
+
return warnings.every((warning) => ADVISORY_CODES.includes(warning.code)) ? "notice" : "failure";
|
|
330
621
|
}
|
|
331
622
|
function dateOrNull(value) {
|
|
332
623
|
if (typeof value !== "string" || value === "") return null;
|
|
@@ -395,6 +686,7 @@ var UpdateStatusService = class {
|
|
|
395
686
|
now;
|
|
396
687
|
ttlMs;
|
|
397
688
|
releaseUrl;
|
|
689
|
+
runtimeWarnings;
|
|
398
690
|
cache;
|
|
399
691
|
inFlight;
|
|
400
692
|
constructor(options) {
|
|
@@ -403,6 +695,7 @@ var UpdateStatusService = class {
|
|
|
403
695
|
this.now = options.now ?? Date.now;
|
|
404
696
|
this.ttlMs = Math.max(1, Math.floor(options.ttlMs ?? DEFAULT_TTL_MS));
|
|
405
697
|
this.releaseUrl = options.releaseUrl ?? "https://github.com/deepseek-ai/deepseek-harness/releases";
|
|
698
|
+
this.runtimeWarnings = options.runtimeWarnings ?? [];
|
|
406
699
|
}
|
|
407
700
|
getStatus(channel = "latest", cacheTtlMinutes) {
|
|
408
701
|
return this.check(false, channel, cacheTtlMinutes);
|
|
@@ -466,6 +759,7 @@ var UpdateStatusService = class {
|
|
|
466
759
|
channel,
|
|
467
760
|
version: selected.version
|
|
468
761
|
});
|
|
762
|
+
warnings.push(...this.runtimeWarnings);
|
|
469
763
|
return {
|
|
470
764
|
currentVersion: this.installation.currentVersion,
|
|
471
765
|
latestVersion: selected.version,
|
|
@@ -487,15 +781,16 @@ var UpdateStatusService = class {
|
|
|
487
781
|
};
|
|
488
782
|
}
|
|
489
783
|
statusWithoutRemoteRelease(channel, warnings) {
|
|
784
|
+
const combined = [...warnings, ...this.runtimeWarnings];
|
|
490
785
|
return {
|
|
491
786
|
currentVersion: this.installation.currentVersion,
|
|
492
787
|
latestVersion: null,
|
|
493
788
|
hasUpdate: false,
|
|
494
789
|
cached: false,
|
|
495
790
|
checkedAt: null,
|
|
496
|
-
warning: warningFallback(
|
|
497
|
-
warningKind: warningKindOf(
|
|
498
|
-
warnings,
|
|
791
|
+
warning: warningFallback(combined),
|
|
792
|
+
warningKind: warningKindOf(combined),
|
|
793
|
+
warnings: combined,
|
|
499
794
|
installKind: this.installation.installKind,
|
|
500
795
|
upgradeCommand: upgradeCommandFor(this.installation.installKind, this.installation.packageName || "@deepseek-ai/dsh", channel),
|
|
501
796
|
releaseUrl: this.releaseUrl,
|
|
@@ -516,25 +811,46 @@ var UpdateStatusService = class {
|
|
|
516
811
|
//#endregion
|
|
517
812
|
//#region src/host/settings.ts
|
|
518
813
|
/**
|
|
814
|
+
* Build the Config schema from a resolved schemastery factory.
|
|
815
|
+
*
|
|
816
|
+
* `volatileAvailable` is the capability the resolver *verified*, not an
|
|
817
|
+
* assumption. On a copy without `volatile()` the three preferences stay ordinary
|
|
818
|
+
* fields — the plugin keeps loading and reports why through
|
|
819
|
+
* `schemaRuntime().warning` instead of throwing while its own entry is imported.
|
|
820
|
+
*
|
|
821
|
+
* @param factory - A schemastery factory, structurally the package's default export.
|
|
822
|
+
* @param volatileAvailable - Whether `factory` exposes the Loader's volatile projection.
|
|
823
|
+
*/
|
|
824
|
+
function buildConfigSchema(factory, volatileAvailable) {
|
|
825
|
+
const volatile = (field) => {
|
|
826
|
+
if (!volatileAvailable) return field;
|
|
827
|
+
const method = field.volatile;
|
|
828
|
+
return typeof method === "function" ? method.call(field) : field;
|
|
829
|
+
};
|
|
830
|
+
return factory.object({
|
|
831
|
+
cacheTtlHours: factory.number().step(1).min(1).max(24).default(6),
|
|
832
|
+
timeoutMs: factory.number().step(1).min(1e3).max(3e4).default(DEFAULT_TIMEOUT_MS),
|
|
833
|
+
autoCheckOnMount: factory.boolean().default(true),
|
|
834
|
+
sidebarEnabled: volatile(factory.boolean().default(true)),
|
|
835
|
+
channel: volatile(factory.union([
|
|
836
|
+
"latest",
|
|
837
|
+
"next",
|
|
838
|
+
"alpha"
|
|
839
|
+
]).default("latest").loose()),
|
|
840
|
+
cacheTtlMinutes: volatile(factory.number().min(30).max(1440).default(360))
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const runtime = schemaRuntime();
|
|
844
|
+
/**
|
|
519
845
|
* The plugin's Config schema; its volatile fields ARE the settings form.
|
|
520
846
|
*
|
|
521
847
|
* The explicit two-argument annotation is load-bearing: a `.volatile()` field's
|
|
522
848
|
* output is a stable reference (`Volatile<T>`) rather than the bare value the
|
|
523
849
|
* input side takes, so the inferred schema type cannot be named by the emitted
|
|
524
|
-
* `.d.ts` (TS2883) without stating the input side here.
|
|
850
|
+
* `.d.ts` (TS2883) without stating the input side here. The builder preserves
|
|
851
|
+
* that contract even on the degraded path, where no field is volatile.
|
|
525
852
|
*/
|
|
526
|
-
const Config = z.
|
|
527
|
-
cacheTtlHours: z.number().step(1).min(1).max(24).default(6),
|
|
528
|
-
timeoutMs: z.number().step(1).min(1e3).max(3e4).default(DEFAULT_TIMEOUT_MS),
|
|
529
|
-
autoCheckOnMount: z.boolean().default(true),
|
|
530
|
-
sidebarEnabled: z.boolean().default(true).volatile(),
|
|
531
|
-
channel: z.union([
|
|
532
|
-
"latest",
|
|
533
|
-
"next",
|
|
534
|
-
"alpha"
|
|
535
|
-
]).default("latest").loose().volatile(),
|
|
536
|
-
cacheTtlMinutes: z.number().min(30).max(1440).default(360).volatile()
|
|
537
|
-
});
|
|
853
|
+
const Config = buildConfigSchema(runtime.z, runtime.volatile);
|
|
538
854
|
/**
|
|
539
855
|
* Suppress the auto-generated settings page for this entry.
|
|
540
856
|
*
|
|
@@ -572,14 +888,17 @@ function boundedNumber(value, fallback, min, max) {
|
|
|
572
888
|
function apply(ctx, config = {}) {
|
|
573
889
|
const ttlHours = boundedNumber(config.cacheTtlHours, 6, 1, 24);
|
|
574
890
|
const timeoutMs = boundedNumber(config.timeoutMs, DEFAULT_TIMEOUT_MS, 1e3, 3e4);
|
|
891
|
+
const schema = schemaRuntime();
|
|
892
|
+
if (schema.warning !== null) console.warn(`[dsh-update-status] ${describeSchemaRuntime(schema)} — ${describeWarning(schema.warning)}`);
|
|
575
893
|
const service = new UpdateStatusService({
|
|
576
894
|
installation: detectInstallation(ctx),
|
|
577
895
|
fetchLatest: createRegistryFetcher(timeoutMs),
|
|
578
|
-
ttlMs: ttlHours * 60 * 60 * 1e3
|
|
896
|
+
ttlMs: ttlHours * 60 * 60 * 1e3,
|
|
897
|
+
runtimeWarnings: schema.warning === null ? [] : [schema.warning]
|
|
579
898
|
});
|
|
580
899
|
installSettings(ctx);
|
|
581
900
|
installUpdateStatusRpc(ctx, service);
|
|
582
901
|
if (config.autoCheckOnMount !== false) service.getStatus();
|
|
583
902
|
}
|
|
584
903
|
//#endregion
|
|
585
|
-
export { Config, UpdateStatusService, apply, inject, name };
|
|
904
|
+
export { Config, UpdateStatusService, apply, inject, name, resolveSchemaRuntime, schemaRuntime };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-update-status",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Read-only DeepSeek Harness Web plugin that shows the running DSH version, discovers latest/next/alpha releases, warns about compatibility, and generates copy-only upgrade commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
"compatibility": {
|
|
64
64
|
"dshReleases": {
|
|
65
65
|
"0.1.7-alpha.2": "compatible",
|
|
66
|
-
"0.1.7-rc.1": "compatible"
|
|
66
|
+
"0.1.7-rc.1": "compatible",
|
|
67
|
+
"0.1.7-rc.2": "compatible"
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
},
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
"test": "pnpm run typecheck && vitest run",
|
|
99
100
|
"test:unit": "vitest run",
|
|
100
101
|
"clean": "rm -rf lib coverage",
|
|
101
|
-
"
|
|
102
|
+
"guard:shadowed-peer": "node tests/guards/shadowed-peer.mjs",
|
|
103
|
+
"verify": "pnpm run test && pnpm run build && pnpm run guard:shadowed-peer && pnpm pack --dry-run"
|
|
102
104
|
}
|
|
103
105
|
}
|