dsh-windows-mcp 0.1.0 → 0.2.0
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 +43 -1
- package/LICENSE +0 -6
- package/README.md +38 -2
- package/README.zh.md +37 -2
- package/lib/client.js +135 -11
- package/lib/dependency.js +84 -18
- package/lib/index.js +154 -27
- package/lib/installs.js +196 -0
- package/lib/mirrors.js +236 -0
- package/lib/pypi.js +171 -0
- package/lib/updater.js +496 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -33,12 +33,12 @@ import {
|
|
|
33
33
|
BRIDGE_ROUTE,
|
|
34
34
|
PREPARE_CANCEL_ROUTE,
|
|
35
35
|
PREPARE_ROUTE,
|
|
36
|
-
createPrepare,
|
|
37
36
|
dependencySnapshot,
|
|
38
37
|
windowsMcpExecutable,
|
|
39
38
|
windowsMcpVersionPin,
|
|
40
39
|
writeBridgeOverride,
|
|
41
40
|
} from "./dependency.js";
|
|
41
|
+
import { UPDATE_ACTIONS, UPDATE_ROUTE, createUpdater } from "./updater.js";
|
|
42
42
|
|
|
43
43
|
const PACKAGE_ID = "dsh-windows-mcp";
|
|
44
44
|
const BRIDGE_ROW_ID = "windows-mcp-bridge";
|
|
@@ -67,8 +67,8 @@ const MAX_STATE_BODY_BYTES = 1024;
|
|
|
67
67
|
* Desktop control is opt-in: a fresh install starts OFF. */
|
|
68
68
|
const computerUse = { enabled: false };
|
|
69
69
|
|
|
70
|
-
/** The
|
|
71
|
-
let
|
|
70
|
+
/** The Windows-MCP version manager, created in apply() once the logger exists. */
|
|
71
|
+
let updater = null;
|
|
72
72
|
|
|
73
73
|
/* ------------------------------------------------------------------ *
|
|
74
74
|
* small, throw-proof helpers
|
|
@@ -311,6 +311,25 @@ function buildReport(tools) {
|
|
|
311
311
|
}`,
|
|
312
312
|
);
|
|
313
313
|
|
|
314
|
+
const update = updater === null ? null : updater.view();
|
|
315
|
+
if (update !== null) {
|
|
316
|
+
const managed = update.versions;
|
|
317
|
+
lines.push(
|
|
318
|
+
`managed install : ${
|
|
319
|
+
managed.active === null
|
|
320
|
+
? "none yet — the global `uv tool` installation is used (installing or updating creates one)"
|
|
321
|
+
: `v${managed.active} under ${managed.toolchainRoot}`
|
|
322
|
+
}`,
|
|
323
|
+
);
|
|
324
|
+
if (managed.previous !== null) lines.push(` rollback to : v${managed.previous}`);
|
|
325
|
+
if (managed.available.length > 0) lines.push(` installed : ${managed.available.map((version) => `v${version}`).join(", ")}`);
|
|
326
|
+
if (update.check.latest === null) {
|
|
327
|
+
lines.push(" latest : not checked yet — press Check for updates in Settings → Plugins");
|
|
328
|
+
} else {
|
|
329
|
+
lines.push(` latest : v${update.check.latest}${managed.updateAvailable ? " — an update is available" : " — up to date"}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
314
333
|
if (uv === null && uvx === null) {
|
|
315
334
|
advice.push(
|
|
316
335
|
"Install uv (https://docs.astral.sh/uv/) and make sure `uvx` is on the PATH of the process that runs DSH — without it the bridge cannot start Windows-MCP.",
|
|
@@ -490,13 +509,34 @@ function registerStateRoute(ctx, warn) {
|
|
|
490
509
|
}
|
|
491
510
|
};
|
|
492
511
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
512
|
+
/** Map the version manager's install progress onto the prepare shape the page polls. */
|
|
513
|
+
const prepareSnapshot = () => {
|
|
514
|
+
if (updater === null) return null;
|
|
515
|
+
const install = updater.view().install;
|
|
516
|
+
return {
|
|
517
|
+
phase: install.phase === "installing" ? "running" : install.phase,
|
|
518
|
+
startedAt: install.startedAt,
|
|
519
|
+
finishedAt: install.finishedAt,
|
|
520
|
+
error: install.error,
|
|
521
|
+
log: install.log,
|
|
522
|
+
bridge: install.bridge,
|
|
523
|
+
version: install.version,
|
|
524
|
+
mirror: install.mirror,
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const snapshot = () => {
|
|
529
|
+
const legacy = dependencySnapshot(which, windowsMcpVersionPin(readText(patchPath)));
|
|
530
|
+
const update = updater === null ? null : updater.view();
|
|
531
|
+
return {
|
|
532
|
+
enabled: computerUse.enabled,
|
|
533
|
+
bridged: bridgedToolCount(scoped.get?.("tools")),
|
|
534
|
+
serverName: "windows",
|
|
535
|
+
dependency: updater === null ? legacy : updater.dependencyView(legacy),
|
|
536
|
+
prepare: prepareSnapshot(),
|
|
537
|
+
update,
|
|
538
|
+
};
|
|
539
|
+
};
|
|
500
540
|
|
|
501
541
|
const handler = (request, response) => {
|
|
502
542
|
if (request.method === "GET") {
|
|
@@ -536,7 +576,7 @@ function registerStateRoute(ctx, warn) {
|
|
|
536
576
|
request.on("error", () => respond(response, 400, { error: "request failed" }));
|
|
537
577
|
};
|
|
538
578
|
|
|
539
|
-
/** Start the
|
|
579
|
+
/** Start installing the pinned (or newest known) Windows-MCP into its own tool root. */
|
|
540
580
|
const prepareHandler = (request, response) => {
|
|
541
581
|
if (request.method === "GET") {
|
|
542
582
|
respond(response, 200, snapshot());
|
|
@@ -546,17 +586,22 @@ function registerStateRoute(ctx, warn) {
|
|
|
546
586
|
respond(response, 405, { error: "method not allowed" });
|
|
547
587
|
return;
|
|
548
588
|
}
|
|
549
|
-
if (
|
|
550
|
-
respond(response, 503, { error: "the
|
|
589
|
+
if (updater === null) {
|
|
590
|
+
respond(response, 503, { error: "the version manager is not ready" });
|
|
551
591
|
return;
|
|
552
592
|
}
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
593
|
+
const view = updater.view();
|
|
594
|
+
const target = view.versions.pinned ?? view.check.latest;
|
|
595
|
+
if (target === null) {
|
|
596
|
+
respond(response, 409, { started: false, reason: "no Windows-MCP version is known yet", prepare: prepareSnapshot() });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (view.install.phase === "installing") {
|
|
600
|
+
respond(response, 409, { started: false, reason: "an install is already running", prepare: prepareSnapshot() });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
void updater.installVersion(target);
|
|
604
|
+
respond(response, 202, { started: true, spec: `windows-mcp@${target}`, prepare: prepareSnapshot() });
|
|
560
605
|
};
|
|
561
606
|
|
|
562
607
|
/** Stop a running install. */
|
|
@@ -565,31 +610,105 @@ function registerStateRoute(ctx, warn) {
|
|
|
565
610
|
respond(response, 405, { error: "method not allowed" });
|
|
566
611
|
return;
|
|
567
612
|
}
|
|
568
|
-
const
|
|
569
|
-
respond(response, 200, { cancelled
|
|
613
|
+
const result = updater === null ? { cancelled: false } : updater.cancel();
|
|
614
|
+
respond(response, 200, { cancelled: result.cancelled === true, prepare: prepareSnapshot() });
|
|
570
615
|
};
|
|
571
616
|
|
|
572
|
-
/** Point the bridge at the
|
|
617
|
+
/** Point the bridge at the active executable (managed install first). */
|
|
573
618
|
const bridgeHandler = (request, response) => {
|
|
574
619
|
if (request.method !== "POST") {
|
|
575
620
|
respond(response, 405, { error: "method not allowed" });
|
|
576
621
|
return;
|
|
577
622
|
}
|
|
578
|
-
const
|
|
623
|
+
const view = updater === null ? null : updater.view();
|
|
624
|
+
const executable = view?.versions?.activeExecutable ?? windowsMcpExecutable();
|
|
579
625
|
if (executable === null) {
|
|
580
626
|
respond(response, 409, {
|
|
581
627
|
ok: false,
|
|
582
|
-
error: "Windows-MCP is not installed
|
|
628
|
+
error: "Windows-MCP is not installed yet, so there is nothing to point the bridge at.",
|
|
583
629
|
});
|
|
584
630
|
return;
|
|
585
631
|
}
|
|
586
632
|
respond(response, 200, writeBridgeOverride(executable, { warn, version: selfVersion() }));
|
|
587
633
|
};
|
|
588
634
|
|
|
635
|
+
/** Check for, install, activate and roll back Windows-MCP versions. */
|
|
636
|
+
const updateHandler = (request, response) => {
|
|
637
|
+
if (request.method === "GET") {
|
|
638
|
+
respond(response, 200, updater === null ? { error: "the version manager is not ready" } : updater.view());
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (request.method !== "POST") {
|
|
642
|
+
respond(response, 405, { error: "method not allowed" });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (updater === null) {
|
|
646
|
+
respond(response, 503, { error: "the version manager is not ready" });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const chunks = [];
|
|
650
|
+
let size = 0;
|
|
651
|
+
request.on("data", (chunk) => {
|
|
652
|
+
size += chunk.length;
|
|
653
|
+
if (size > MAX_STATE_BODY_BYTES) {
|
|
654
|
+
request.destroy();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
chunks.push(chunk);
|
|
658
|
+
});
|
|
659
|
+
request.on("end", () => {
|
|
660
|
+
let body;
|
|
661
|
+
try {
|
|
662
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
663
|
+
} catch {
|
|
664
|
+
respond(response, 400, { error: "invalid JSON body" });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const action = typeof body?.action === "string" ? body.action : "";
|
|
668
|
+
const version = typeof body?.version === "string" && body.version !== "" ? body.version : null;
|
|
669
|
+
const allowed = [...UPDATE_ACTIONS, "cancel"];
|
|
670
|
+
if (!allowed.includes(action)) {
|
|
671
|
+
respond(response, 400, { error: `expected { action } in ${allowed.join(", ")}` });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (action === "check") {
|
|
675
|
+
void updater.check({ force: true });
|
|
676
|
+
respond(response, 202, { started: true, action });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (action === "install") {
|
|
680
|
+
const view = updater.view();
|
|
681
|
+
const target = version ?? view.check.latest ?? view.versions.pinned;
|
|
682
|
+
if (target === null) {
|
|
683
|
+
respond(response, 409, { ok: false, error: "no Windows-MCP version is known yet — check for updates first" });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
void updater.installVersion(target);
|
|
687
|
+
respond(response, 202, { started: true, action, version: target });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (action === "cancel") {
|
|
691
|
+
respond(response, 200, updater.cancel());
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (action === "activate") {
|
|
695
|
+
if (version === null) {
|
|
696
|
+
respond(response, 400, { error: "activate needs { version }" });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
respond(response, 200, updater.activateInstalled(version));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
respond(response, 200, updater.rollback());
|
|
703
|
+
});
|
|
704
|
+
request.on("error", () => respond(response, 400, { error: "request failed" }));
|
|
705
|
+
};
|
|
706
|
+
|
|
589
707
|
scoped.webServer.register({ kind: "exact", path: STATE_ROUTE, handler });
|
|
590
708
|
scoped.webServer.register({ kind: "exact", path: PREPARE_ROUTE, handler: prepareHandler });
|
|
591
709
|
scoped.webServer.register({ kind: "exact", path: PREPARE_CANCEL_ROUTE, handler: cancelHandler });
|
|
592
710
|
scoped.webServer.register({ kind: "exact", path: BRIDGE_ROUTE, handler: bridgeHandler });
|
|
711
|
+
scoped.webServer.register({ kind: "exact", path: UPDATE_ROUTE, handler: updateHandler });
|
|
593
712
|
} catch (error) {
|
|
594
713
|
warn(`could not register the Computer Use state route: ${String(error)}`);
|
|
595
714
|
}
|
|
@@ -607,16 +726,24 @@ export function apply(ctx) {
|
|
|
607
726
|
try {
|
|
608
727
|
const warn = makeLogger(ctx);
|
|
609
728
|
computerUse.enabled = readComputerUse().enabled;
|
|
610
|
-
|
|
729
|
+
updater = createUpdater({
|
|
611
730
|
warn,
|
|
612
731
|
resolver: which,
|
|
613
732
|
pinned: windowsMcpVersionPin(readText(patchPath)),
|
|
614
|
-
|
|
733
|
+
writeBridge: (executable) => writeBridgeOverride(executable, { warn, version: selfVersion() }),
|
|
615
734
|
});
|
|
616
735
|
auditStartup(warn);
|
|
617
736
|
registerDoctor(ctx, warn);
|
|
618
737
|
registerStateRoute(ctx, warn);
|
|
619
738
|
registerToolGate(ctx, warn);
|
|
739
|
+
// Check for a newer Windows-MCP build in the background (cheap: a ranged
|
|
740
|
+
// read per mirror, cached for hours). The page shows the result; the boot
|
|
741
|
+
// never waits for it, and a failure only costs the last known answer.
|
|
742
|
+
try {
|
|
743
|
+
void updater.check({ force: false }).catch(() => {});
|
|
744
|
+
} catch {
|
|
745
|
+
/* a failed check is not a boot problem */
|
|
746
|
+
}
|
|
620
747
|
} catch {
|
|
621
748
|
/* apply() must not throw, whatever happens */
|
|
622
749
|
}
|
package/lib/installs.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-windows-mcp — managed installs (host side).
|
|
3
|
+
*
|
|
4
|
+
* Every Windows-MCP version this plugin installs lives in its own `uv` tool
|
|
5
|
+
* root:
|
|
6
|
+
*
|
|
7
|
+
* <home>/dsh-windows-mcp/toolchain/<version>/ ← UV_TOOL_DIR
|
|
8
|
+
* <home>/dsh-windows-mcp/toolchain/<version>/windows-mcp/Scripts/windows-mcp.exe
|
|
9
|
+
* <home>/dsh-windows-mcp/toolchain/<version>/manifest.json
|
|
10
|
+
*
|
|
11
|
+
* Isolating by version is what makes "update" safe: the previous environment is
|
|
12
|
+
* never overwritten, so rolling back is a pointer move plus a bridge rewrite —
|
|
13
|
+
* no download, no reinstall. `<home>/dsh-windows-mcp.versions.json` remembers
|
|
14
|
+
* which version is active, which one it replaced, and what the last update
|
|
15
|
+
* check saw.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { dshHome, toolVersionIn } from "./dependency.js";
|
|
20
|
+
|
|
21
|
+
const VERSION_PATTERN = /^\d[0-9A-Za-z.+_-]*$/u;
|
|
22
|
+
const STATE_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
/** Root that holds one tool root per Windows-MCP version. */
|
|
25
|
+
export function managedToolchainRoot(home = dshHome()) {
|
|
26
|
+
return join(home, "dsh-windows-mcp", "toolchain");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `…/<version>` — the directory handed to `UV_TOOL_DIR`. */
|
|
30
|
+
export function versionToolRoot(root, version) {
|
|
31
|
+
if (typeof root !== "string" || root === "") throw new TypeError("root must be a non-empty string");
|
|
32
|
+
if (typeof version !== "string" || !VERSION_PATTERN.test(version)) throw new TypeError(`invalid Windows-MCP version: ${String(version)}`);
|
|
33
|
+
return join(root, version);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `…/<version>/windows-mcp` — the tool environment inside the version root. */
|
|
37
|
+
export function toolDirectoryFor(root, version) {
|
|
38
|
+
return join(versionToolRoot(root, version), "windows-mcp");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Executable path for a tool directory, or null when the tool is not there. */
|
|
42
|
+
export function executableIn(toolDirectory) {
|
|
43
|
+
if (typeof toolDirectory !== "string" || toolDirectory === "") return null;
|
|
44
|
+
for (const candidate of [join(toolDirectory, "Scripts", "windows-mcp.exe"), join(toolDirectory, "bin", "windows-mcp")]) {
|
|
45
|
+
if (existsSync(candidate)) return candidate;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Executable of a managed version, or null when it is not installed. */
|
|
51
|
+
export function managedExecutable(root, version) {
|
|
52
|
+
return executableIn(toolDirectoryFor(root, version));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `…/<version>/manifest.json` */
|
|
56
|
+
export function manifestPath(root, version) {
|
|
57
|
+
return join(versionToolRoot(root, version), "manifest.json");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read the install manifest of one managed version (null when absent). */
|
|
61
|
+
export function readManifest(root, version) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(readFileSync(manifestPath(root, version), "utf8"));
|
|
64
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Write the install manifest of one managed version. */
|
|
71
|
+
export function writeManifest(root, version, payload = {}) {
|
|
72
|
+
const target = manifestPath(root, version);
|
|
73
|
+
mkdirSync(versionToolRoot(root, version), { recursive: true });
|
|
74
|
+
const manifest = {
|
|
75
|
+
version,
|
|
76
|
+
installedAt: new Date().toISOString(),
|
|
77
|
+
executable: managedExecutable(root, version),
|
|
78
|
+
...payload,
|
|
79
|
+
};
|
|
80
|
+
writeFileSync(target, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
81
|
+
return manifest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Every managed version, newest first. */
|
|
85
|
+
export function listManagedVersions(root = managedToolchainRoot()) {
|
|
86
|
+
let names = [];
|
|
87
|
+
try {
|
|
88
|
+
names = readdirSync(root, { withFileTypes: true })
|
|
89
|
+
.filter((entry) => entry.isDirectory() && VERSION_PATTERN.test(entry.name))
|
|
90
|
+
.map((entry) => entry.name);
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const versions = names.map((version) => {
|
|
95
|
+
const toolDirectory = toolDirectoryFor(root, version);
|
|
96
|
+
const manifest = readManifest(root, version);
|
|
97
|
+
const executable = executableIn(toolDirectory);
|
|
98
|
+
return {
|
|
99
|
+
version,
|
|
100
|
+
toolDirectory,
|
|
101
|
+
executable,
|
|
102
|
+
installedVersion: toolVersionIn(toolDirectory),
|
|
103
|
+
manifest,
|
|
104
|
+
installedAt: typeof manifest?.installedAt === "string" ? manifest.installedAt : null,
|
|
105
|
+
mirror: typeof manifest?.mirror === "string" ? manifest.mirror : null,
|
|
106
|
+
ready: executable !== null,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
return versions
|
|
110
|
+
.filter((entry) => entry.ready)
|
|
111
|
+
.sort((left, right) => compareVersionsDescending(left.version, right.version));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Local sort helper — newest first, numeric segments. */
|
|
115
|
+
function compareVersionsDescending(left, right) {
|
|
116
|
+
const leftParts = left.split(/[.+-]/u).map((part) => (/^\d+$/u.test(part) ? Number(part) : part));
|
|
117
|
+
const rightParts = right.split(/[.+-]/u).map((part) => (/^\d+$/u.test(part) ? Number(part) : part));
|
|
118
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
119
|
+
const leftPart = leftParts[index] ?? 0;
|
|
120
|
+
const rightPart = rightParts[index] ?? 0;
|
|
121
|
+
if (leftPart === rightPart) continue;
|
|
122
|
+
if (typeof leftPart === "number" && typeof rightPart === "number") return rightPart - leftPart;
|
|
123
|
+
return String(rightPart).localeCompare(String(leftPart));
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `<home>/dsh-windows-mcp.versions.json` */
|
|
129
|
+
export function versionsStatePath(home = dshHome()) {
|
|
130
|
+
return join(home, "dsh-windows-mcp.versions.json");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A fresh, empty versions state. */
|
|
134
|
+
export function emptyVersionsState() {
|
|
135
|
+
return { version: STATE_VERSION, active: null, previous: null, history: [], lastCheck: null };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Normalise anything read from disk into a usable state object. */
|
|
139
|
+
function normalizeVersionsState(parsed) {
|
|
140
|
+
const state = emptyVersionsState();
|
|
141
|
+
if (parsed === null || typeof parsed !== "object") return state;
|
|
142
|
+
const version = (value) => (typeof value === "string" && VERSION_PATTERN.test(value) ? value : null);
|
|
143
|
+
state.active = version(parsed.active);
|
|
144
|
+
state.previous = version(parsed.previous);
|
|
145
|
+
if (Array.isArray(parsed.history)) {
|
|
146
|
+
state.history = parsed.history
|
|
147
|
+
.filter((entry) => entry !== null && typeof entry === "object" && version(entry.version) !== null)
|
|
148
|
+
.slice(-20)
|
|
149
|
+
.map((entry) => ({ version: version(entry.version), activatedAt: typeof entry.activatedAt === "string" ? entry.activatedAt : null }));
|
|
150
|
+
}
|
|
151
|
+
if (parsed.lastCheck !== null && typeof parsed.lastCheck === "object" && typeof parsed.lastCheck.at === "string") {
|
|
152
|
+
state.lastCheck = {
|
|
153
|
+
at: parsed.lastCheck.at,
|
|
154
|
+
latest: version(parsed.lastCheck.latest),
|
|
155
|
+
best: typeof parsed.lastCheck.best === "string" ? parsed.lastCheck.best : null,
|
|
156
|
+
mirrors: Array.isArray(parsed.lastCheck.mirrors) ? parsed.lastCheck.mirrors : [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Read the versions state, tolerating a missing or damaged file. */
|
|
163
|
+
export function readVersionsState(home = dshHome()) {
|
|
164
|
+
try {
|
|
165
|
+
return normalizeVersionsState(JSON.parse(readFileSync(versionsStatePath(home), "utf8")));
|
|
166
|
+
} catch {
|
|
167
|
+
return emptyVersionsState();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Write the versions state atomically (write-then-rename). */
|
|
172
|
+
export function writeVersionsState(state, home = dshHome()) {
|
|
173
|
+
const target = versionsStatePath(home);
|
|
174
|
+
const temporary = `${target}.tmp`;
|
|
175
|
+
const payload = { ...emptyVersionsState(), ...state, version: STATE_VERSION, history: state.history?.slice(-20) ?? [] };
|
|
176
|
+
writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`);
|
|
177
|
+
renameSync(temporary, target);
|
|
178
|
+
return payload;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Record `version` as active and the previous active as its rollback target. */
|
|
182
|
+
export function activateVersion(state, version) {
|
|
183
|
+
if (typeof version !== "string" || !VERSION_PATTERN.test(version)) throw new TypeError(`invalid Windows-MCP version: ${String(version)}`);
|
|
184
|
+
const previous = state.active !== null && state.active !== version ? state.active : null;
|
|
185
|
+
const history = [...(state.history ?? []), { version, activatedAt: new Date().toISOString() }].slice(-20);
|
|
186
|
+
return { ...state, active: version, previous, history };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Where the bridge should point: the active managed version's executable. */
|
|
190
|
+
export function activeInstall(state, home = dshHome()) {
|
|
191
|
+
const version = state?.active ?? null;
|
|
192
|
+
if (version === null) return null;
|
|
193
|
+
const executable = managedExecutable(managedToolchainRoot(home), version);
|
|
194
|
+
if (executable === null) return null;
|
|
195
|
+
return { version, executable, toolDirectory: toolDirectoryFor(managedToolchainRoot(home), version) };
|
|
196
|
+
}
|