@moldesk/core 0.0.1 → 0.0.2
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/dist/compatibility.d.ts +31 -0
- package/dist/compatibility.js +377 -0
- package/dist/compatibility.test.d.ts +1 -0
- package/dist/compatibility.test.js +362 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +16 -0
- package/dist/hash.d.ts +4 -0
- package/dist/hash.js +13 -0
- package/dist/index.d.ts +16 -5
- package/dist/index.js +9 -3
- package/dist/install-state.d.ts +67 -0
- package/dist/install-state.js +137 -0
- package/dist/installation.d.ts +74 -0
- package/dist/installation.js +310 -0
- package/dist/installation.test.d.ts +1 -0
- package/dist/installation.test.js +268 -0
- package/dist/run.d.ts +89 -0
- package/dist/run.dynamic-defaults.test.d.ts +1 -0
- package/dist/run.dynamic-defaults.test.js +124 -0
- package/dist/run.js +222 -0
- package/dist/run.test.d.ts +1 -0
- package/dist/run.test.js +210 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +8 -5
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { evaluateCompatibility, shortReason } from "./compatibility.js";
|
|
3
|
+
const GB = 1024 ** 3;
|
|
4
|
+
function baseReport(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
capturedAt: "2026-01-01T00:00:00.000Z",
|
|
7
|
+
platform: "linux",
|
|
8
|
+
arch: "x64",
|
|
9
|
+
cpu: { model: "Test CPU", logicalCores: 8 },
|
|
10
|
+
memory: { totalBytes: 32 * GB },
|
|
11
|
+
disk: { availableBytes: 200 * GB, path: "/tmp/moldesk-test" },
|
|
12
|
+
appleSilicon: false,
|
|
13
|
+
hostPython: { available: false, error: "no python" },
|
|
14
|
+
docker: { available: true, running: true, version: "24.0.7", gpuAccess: "available" },
|
|
15
|
+
nvidia: {
|
|
16
|
+
available: true,
|
|
17
|
+
driverVersion: "550.54.15",
|
|
18
|
+
driverCudaVersion: "12.4",
|
|
19
|
+
gpus: [{ index: 0, name: "NVIDIA A100", totalVramBytes: 80 * GB }],
|
|
20
|
+
},
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function baseManifest(overrides = {}) {
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
name: "proteinmpnn",
|
|
28
|
+
displayName: "ProteinMPNN",
|
|
29
|
+
modelVersion: "v_48_020",
|
|
30
|
+
adapterVersion: "0.1.0",
|
|
31
|
+
description: "test",
|
|
32
|
+
category: "sequence-design",
|
|
33
|
+
status: "available",
|
|
34
|
+
runtimes: [{ kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch", version: "2.2.1" }] }],
|
|
35
|
+
hardware: { platforms: ["darwin-arm64", "linux-x64"] },
|
|
36
|
+
input: { formats: [".pdb"], required: true },
|
|
37
|
+
outputs: [{ id: "sequences", glob: "*.fa", required: true }],
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
describe("evaluateCompatibility", () => {
|
|
42
|
+
it("macOS ARM64 CPU: python model compatible without host python or GPU", () => {
|
|
43
|
+
const report = baseReport({
|
|
44
|
+
platform: "darwin",
|
|
45
|
+
arch: "arm64",
|
|
46
|
+
appleSilicon: true,
|
|
47
|
+
nvidia: { available: false, gpus: [], error: "no gpu" },
|
|
48
|
+
docker: { available: false, running: false, gpuAccess: "unknown" },
|
|
49
|
+
});
|
|
50
|
+
const result = evaluateCompatibility(baseManifest(), report);
|
|
51
|
+
expect(result.status).toBe("compatible");
|
|
52
|
+
expect(result.selectedRuntime).toBe("python");
|
|
53
|
+
});
|
|
54
|
+
it("Linux CPU-only: CUDA-recommended model yields warning, not unsupported", () => {
|
|
55
|
+
const manifest = baseManifest({
|
|
56
|
+
hardware: { platforms: ["linux-x64"], nvidiaGpu: "recommended", cuda: { level: "recommended" } },
|
|
57
|
+
});
|
|
58
|
+
const report = baseReport({
|
|
59
|
+
platform: "linux",
|
|
60
|
+
arch: "x64",
|
|
61
|
+
nvidia: { available: false, gpus: [], error: "no gpu" },
|
|
62
|
+
});
|
|
63
|
+
const result = evaluateCompatibility(manifest, report);
|
|
64
|
+
expect(result.status).toBe("warning");
|
|
65
|
+
expect(result.selectedRuntime).toBe("python");
|
|
66
|
+
expect(result.reasons.some((r) => r.code === "CUDA_NOT_DETECTED")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it("Linux NVIDIA CUDA: CUDA-required models compatible", () => {
|
|
69
|
+
const manifest = baseManifest({
|
|
70
|
+
hardware: {
|
|
71
|
+
platforms: ["linux-x64"],
|
|
72
|
+
nvidiaGpu: "required",
|
|
73
|
+
minVramGb: 16,
|
|
74
|
+
cuda: { level: "required" },
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
78
|
+
expect(result.status).toBe("compatible");
|
|
79
|
+
});
|
|
80
|
+
it("insufficient VRAM is unsupported with INSUFFICIENT_VRAM", () => {
|
|
81
|
+
const manifest = baseManifest({
|
|
82
|
+
hardware: { platforms: ["linux-x64"], nvidiaGpu: "required", minVramGb: 160 },
|
|
83
|
+
});
|
|
84
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
85
|
+
expect(result.status).toBe("unsupported");
|
|
86
|
+
expect(result.reasons.some((r) => r.code === "INSUFFICIENT_VRAM")).toBe(true);
|
|
87
|
+
expect(shortReason(result)).toMatch(/INSUFFICIENT_VRAM/);
|
|
88
|
+
});
|
|
89
|
+
it("missing Docker: docker-only model unsupported with DOCKER_NOT_INSTALLED", () => {
|
|
90
|
+
const manifest = baseManifest({
|
|
91
|
+
runtimes: [{ kind: "docker", image: "example/model:latest", digest: `sha256:${"a".repeat(64)}`, gpu: "none" }],
|
|
92
|
+
});
|
|
93
|
+
const report = baseReport({ docker: { available: false, running: false, gpuAccess: "unknown" } });
|
|
94
|
+
const result = evaluateCompatibility(manifest, report);
|
|
95
|
+
expect(result.status).toBe("unsupported");
|
|
96
|
+
expect(result.reasons.some((r) => r.code === "DOCKER_NOT_INSTALLED")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it("stopped Docker daemon: DOCKER_NOT_RUNNING", () => {
|
|
99
|
+
const manifest = baseManifest({
|
|
100
|
+
runtimes: [{ kind: "docker", image: "example/model:latest", digest: `sha256:${"a".repeat(64)}`, gpu: "none" }],
|
|
101
|
+
});
|
|
102
|
+
const report = baseReport({
|
|
103
|
+
docker: { available: true, running: false, gpuAccess: "unknown", error: "daemon not running" },
|
|
104
|
+
});
|
|
105
|
+
const result = evaluateCompatibility(manifest, report);
|
|
106
|
+
expect(result.status).toBe("unsupported");
|
|
107
|
+
expect(result.reasons.some((r) => r.code === "DOCKER_NOT_RUNNING")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it("insufficient RAM/disk flagged with stable codes", () => {
|
|
110
|
+
const manifest = baseManifest({
|
|
111
|
+
hardware: { platforms: ["linux-x64"], minRamGb: 64, minDiskGb: 500 },
|
|
112
|
+
});
|
|
113
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
114
|
+
expect(result.status).toBe("unsupported");
|
|
115
|
+
const codes = result.reasons.map((r) => r.code);
|
|
116
|
+
expect(codes).toContain("INSUFFICIENT_RAM");
|
|
117
|
+
expect(codes).toContain("DISK_SPACE_LOW");
|
|
118
|
+
});
|
|
119
|
+
it("unknown probe output never counts as present", () => {
|
|
120
|
+
const manifest = baseManifest({
|
|
121
|
+
hardware: { platforms: ["linux-x64"], nvidiaGpu: "required", cuda: { level: "required" }, minVramGb: 8 },
|
|
122
|
+
});
|
|
123
|
+
const report = baseReport({
|
|
124
|
+
platform: "unknown",
|
|
125
|
+
arch: "unknown",
|
|
126
|
+
nvidia: { available: false, gpus: [], error: "probe failed" },
|
|
127
|
+
});
|
|
128
|
+
const result = evaluateCompatibility(manifest, report);
|
|
129
|
+
expect(result.status).toBe("unsupported");
|
|
130
|
+
expect(result.reasons.some((r) => r.code === "CUDA_NOT_DETECTED")).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it("planned models cannot be installed even on compatible hardware", () => {
|
|
133
|
+
const result = evaluateCompatibility(baseManifest({ status: "planned" }), baseReport());
|
|
134
|
+
expect(result.status).toBe("unsupported");
|
|
135
|
+
expect(result.selectedRuntime).toBeUndefined();
|
|
136
|
+
expect(result.reasons[0]?.code).toBe("MODEL_PLANNED");
|
|
137
|
+
});
|
|
138
|
+
it("requestedRuntime constrains selection without silent fallback", () => {
|
|
139
|
+
const manifest = baseManifest({
|
|
140
|
+
runtimes: [
|
|
141
|
+
{ kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch", version: "1.0" }] },
|
|
142
|
+
{ kind: "docker", image: "example/m:latest", digest: `sha256:${"b".repeat(64)}`, gpu: "none" },
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
const report = baseReport({ docker: { available: false, running: false, gpuAccess: "unknown" } });
|
|
146
|
+
const dockerOnly = evaluateCompatibility(manifest, report, { requestedRuntime: "docker" });
|
|
147
|
+
expect(dockerOnly.status).toBe("unsupported");
|
|
148
|
+
expect(dockerOnly.selectedRuntime).toBeUndefined();
|
|
149
|
+
const pythonOnly = evaluateCompatibility(manifest, report, { requestedRuntime: "python" });
|
|
150
|
+
expect(pythonOnly.status).toBe("compatible");
|
|
151
|
+
expect(pythonOnly.selectedRuntime).toBe("python");
|
|
152
|
+
});
|
|
153
|
+
it("does not reject managed python when host python missing", () => {
|
|
154
|
+
const report = baseReport({ hostPython: { available: false, error: "absent" } });
|
|
155
|
+
const result = evaluateCompatibility(baseManifest(), report);
|
|
156
|
+
expect(result.status).toBe("compatible");
|
|
157
|
+
});
|
|
158
|
+
it("managed python unsupported on unknown platform", () => {
|
|
159
|
+
const report = baseReport({ platform: "unknown", arch: "mips" });
|
|
160
|
+
const result = evaluateCompatibility(baseManifest(), report);
|
|
161
|
+
expect(result.status).toBe("unsupported");
|
|
162
|
+
expect(result.reasons.some((r) => r.code === "MANAGED_PYTHON_PLATFORM_UNSUPPORTED")).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
it("prefers fully compatible runtime over earlier warning", () => {
|
|
165
|
+
const manifest = baseManifest({
|
|
166
|
+
runtimes: [
|
|
167
|
+
{ kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch", version: "1.0" }] },
|
|
168
|
+
{ kind: "docker", image: "example/m:latest", digest: `sha256:${"c".repeat(64)}`, gpu: "none" },
|
|
169
|
+
],
|
|
170
|
+
hardware: { platforms: ["linux-x64"], nvidiaGpu: "recommended" },
|
|
171
|
+
});
|
|
172
|
+
// No GPU: python warns, docker... also warns via optional? make docker gpu none -> clean except shared warning.
|
|
173
|
+
// Both warn; first warning wins deterministically.
|
|
174
|
+
const report = baseReport({
|
|
175
|
+
nvidia: { available: false, gpus: [], error: "no gpu" },
|
|
176
|
+
docker: { available: true, running: true, gpuAccess: "available" },
|
|
177
|
+
});
|
|
178
|
+
const result = evaluateCompatibility(manifest, report);
|
|
179
|
+
expect(result.status).toBe("warning");
|
|
180
|
+
expect(result.selectedRuntime).toBe("python");
|
|
181
|
+
});
|
|
182
|
+
it("minVramGb downgrades to a warning when nvidiaGpu is merely recommended (CPU fallback preserved)", () => {
|
|
183
|
+
const manifest = baseManifest({
|
|
184
|
+
hardware: { platforms: ["linux-x64"], nvidiaGpu: "recommended", minVramGb: 160 },
|
|
185
|
+
});
|
|
186
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
187
|
+
expect(result.status).toBe("warning");
|
|
188
|
+
expect(result.reasons.some((r) => r.code === "INSUFFICIENT_VRAM" && r.severity === "warning")).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it("minVramGb stays a hard error when nvidiaGpu is required", () => {
|
|
191
|
+
const manifest = baseManifest({
|
|
192
|
+
hardware: { platforms: ["linux-x64"], nvidiaGpu: "required", minVramGb: 160 },
|
|
193
|
+
});
|
|
194
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
195
|
+
expect(result.status).toBe("unsupported");
|
|
196
|
+
expect(result.reasons.some((r) => r.code === "INSUFFICIENT_VRAM" && r.severity === "error")).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
it("rejects an insufficient cuda.minDriverCudaVersion with CUDA_VERSION_UNSUPPORTED", () => {
|
|
199
|
+
const manifest = baseManifest({
|
|
200
|
+
hardware: {
|
|
201
|
+
platforms: ["linux-x64"],
|
|
202
|
+
nvidiaGpu: "required",
|
|
203
|
+
cuda: { level: "required", minDriverCudaVersion: "12.6" },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const result = evaluateCompatibility(manifest, baseReport()); // report has driverCudaVersion "12.4"
|
|
207
|
+
expect(result.status).toBe("unsupported");
|
|
208
|
+
expect(result.reasons.some((r) => r.code === "CUDA_VERSION_UNSUPPORTED")).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
it("accepts a sufficient cuda.minDriverCudaVersion", () => {
|
|
211
|
+
const manifest = baseManifest({
|
|
212
|
+
hardware: {
|
|
213
|
+
platforms: ["linux-x64"],
|
|
214
|
+
nvidiaGpu: "required",
|
|
215
|
+
cuda: { level: "required", minDriverCudaVersion: "12.1" },
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
219
|
+
expect(result.status).toBe("compatible");
|
|
220
|
+
});
|
|
221
|
+
it("rejects an unsupported managed Python version", () => {
|
|
222
|
+
const manifest = baseManifest({
|
|
223
|
+
runtimes: [{ kind: "python", python: "3.8", installer: "uv", requirements: [{ name: "torch", version: "2.2.1" }] }],
|
|
224
|
+
});
|
|
225
|
+
const result = evaluateCompatibility(manifest, baseReport());
|
|
226
|
+
expect(result.status).toBe("unsupported");
|
|
227
|
+
expect(result.reasons.some((r) => r.code === "MANAGED_PYTHON_VERSION_UNSUPPORTED")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
it("docker GPU required + never verified (Tier 1 'unknown') yields DOCKER_GPU_UNVERIFIED, not UNAVAILABLE", () => {
|
|
230
|
+
const manifest = baseManifest({
|
|
231
|
+
runtimes: [{ kind: "docker", image: "example/m:latest", digest: `sha256:${"d".repeat(64)}`, gpu: "required" }],
|
|
232
|
+
});
|
|
233
|
+
const report = baseReport({ docker: { available: true, running: true, gpuAccess: "unknown" } });
|
|
234
|
+
const result = evaluateCompatibility(manifest, report);
|
|
235
|
+
expect(result.status).toBe("unsupported");
|
|
236
|
+
expect(result.reasons.some((r) => r.code === "DOCKER_GPU_UNVERIFIED")).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
it("docker GPU required + Tier 1/2 confirmed unavailable yields DOCKER_GPU_UNAVAILABLE", () => {
|
|
239
|
+
const manifest = baseManifest({
|
|
240
|
+
runtimes: [{ kind: "docker", image: "example/m:latest", digest: `sha256:${"e".repeat(64)}`, gpu: "required" }],
|
|
241
|
+
});
|
|
242
|
+
const report = baseReport({ docker: { available: true, running: true, gpuAccess: "unavailable" } });
|
|
243
|
+
const result = evaluateCompatibility(manifest, report);
|
|
244
|
+
expect(result.status).toBe("unsupported");
|
|
245
|
+
expect(result.reasons.some((r) => r.code === "DOCKER_GPU_UNAVAILABLE")).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
it("docker GPU access does not consult local nvidia.available — a remote daemon with GPU access stays compatible", () => {
|
|
248
|
+
const manifest = baseManifest({
|
|
249
|
+
runtimes: [{ kind: "docker", image: "example/m:latest", digest: `sha256:${"f".repeat(64)}`, gpu: "required" }],
|
|
250
|
+
});
|
|
251
|
+
const report = baseReport({
|
|
252
|
+
nvidia: { available: false, gpus: [], error: "no local gpu" },
|
|
253
|
+
docker: { available: true, running: true, gpuAccess: "available" },
|
|
254
|
+
});
|
|
255
|
+
const result = evaluateCompatibility(manifest, report);
|
|
256
|
+
expect(result.status).toBe("compatible");
|
|
257
|
+
expect(result.selectedRuntime).toBe("docker");
|
|
258
|
+
});
|
|
259
|
+
it("populates a per-runtime breakdown (not just candidate 0) when every candidate is unsupported for different reasons", () => {
|
|
260
|
+
const manifest = baseManifest({
|
|
261
|
+
runtimes: [
|
|
262
|
+
{ kind: "python", python: "3.8", installer: "uv", requirements: [{ name: "torch", version: "1.0" }] },
|
|
263
|
+
{ kind: "docker", image: "example/m:latest", digest: `sha256:${"1".repeat(64)}`, gpu: "required" },
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
const report = baseReport({ docker: { available: true, running: true, gpuAccess: "unavailable" } });
|
|
267
|
+
const result = evaluateCompatibility(manifest, report);
|
|
268
|
+
expect(result.status).toBe("unsupported");
|
|
269
|
+
expect(result.runtimes).toHaveLength(2);
|
|
270
|
+
expect(result.runtimes.find((r) => r.kind === "python")?.reasons.some((r) => r.code === "MANAGED_PYTHON_VERSION_UNSUPPORTED")).toBe(true);
|
|
271
|
+
expect(result.runtimes.find((r) => r.kind === "docker")?.reasons.some((r) => r.code === "DOCKER_GPU_UNAVAILABLE")).toBe(true);
|
|
272
|
+
const codes = result.reasons.map((r) => r.code);
|
|
273
|
+
expect(codes).toContain("MANAGED_PYTHON_VERSION_UNSUPPORTED");
|
|
274
|
+
expect(codes).toContain("DOCKER_GPU_UNAVAILABLE");
|
|
275
|
+
});
|
|
276
|
+
it("every registry-style model returns exactly one result", () => {
|
|
277
|
+
for (const status of ["available", "beta", "planned"]) {
|
|
278
|
+
const result = evaluateCompatibility(baseManifest({ status }), baseReport());
|
|
279
|
+
expect(["compatible", "warning", "unsupported"]).toContain(result.status);
|
|
280
|
+
expect(Array.isArray(result.reasons)).toBe(true);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe("per-platform accelerator resolution", () => {
|
|
285
|
+
/** A single manifest whose two python runtime entries declare genuinely different
|
|
286
|
+
* accelerator requirements: CUDA-required on linux-x64 (the "official" runtime),
|
|
287
|
+
* no CUDA/GPU requirement on darwin-arm64 (an MPS-style runtime) — mirroring a
|
|
288
|
+
* future Boltz-style dual-runtime manifest. */
|
|
289
|
+
function dualAcceleratorManifest() {
|
|
290
|
+
return baseManifest({
|
|
291
|
+
hardware: { platforms: ["darwin-arm64", "linux-x64"] }, // no manifest-wide accelerator signal
|
|
292
|
+
runtimes: [
|
|
293
|
+
{
|
|
294
|
+
kind: "python",
|
|
295
|
+
python: "3.11",
|
|
296
|
+
installer: "uv",
|
|
297
|
+
platforms: ["darwin-arm64"],
|
|
298
|
+
nvidiaGpu: "unsupported",
|
|
299
|
+
requirements: [{ name: "torch", version: "2.6.0" }],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
kind: "python",
|
|
303
|
+
python: "3.11",
|
|
304
|
+
installer: "uv",
|
|
305
|
+
platforms: ["linux-x64"],
|
|
306
|
+
nvidiaGpu: "required",
|
|
307
|
+
cuda: { level: "required", minDriverCudaVersion: "12.1" },
|
|
308
|
+
minVramGb: 16,
|
|
309
|
+
requirements: [{ name: "torch", version: "2.2.1" }],
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
it("reports MPS-style (no CUDA requirement) compatibility on a simulated darwin-arm64 host, even with no GPU", () => {
|
|
315
|
+
const manifest = dualAcceleratorManifest();
|
|
316
|
+
const report = baseReport({
|
|
317
|
+
platform: "darwin",
|
|
318
|
+
arch: "arm64",
|
|
319
|
+
appleSilicon: true,
|
|
320
|
+
nvidia: { available: false, gpus: [], error: "no gpu" },
|
|
321
|
+
docker: { available: false, running: false, gpuAccess: "unknown" },
|
|
322
|
+
});
|
|
323
|
+
const result = evaluateCompatibility(manifest, report);
|
|
324
|
+
expect(result.status).toBe("compatible");
|
|
325
|
+
expect(result.reasons.some((r) => r.code === "CUDA_NOT_DETECTED")).toBe(false);
|
|
326
|
+
expect(result.reasons.some((r) => r.code === "NVIDIA_GPU_REQUIRED")).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
it("reports CUDA-required compatibility (or its absence as an error) on a simulated linux-x64 host, for the same manifest", () => {
|
|
329
|
+
const manifest = dualAcceleratorManifest();
|
|
330
|
+
const cudaReport = baseReport({ platform: "linux", arch: "x64" }); // has CUDA + 80GB VRAM per baseReport()
|
|
331
|
+
const compatible = evaluateCompatibility(manifest, cudaReport);
|
|
332
|
+
expect(compatible.status).toBe("compatible");
|
|
333
|
+
const noGpuReport = baseReport({
|
|
334
|
+
platform: "linux",
|
|
335
|
+
arch: "x64",
|
|
336
|
+
nvidia: { available: false, gpus: [], error: "no gpu" },
|
|
337
|
+
});
|
|
338
|
+
const unsupported = evaluateCompatibility(manifest, noGpuReport);
|
|
339
|
+
expect(unsupported.status).toBe("unsupported");
|
|
340
|
+
const codes = unsupported.reasons.map((r) => r.code);
|
|
341
|
+
expect(codes).toContain("NVIDIA_GPU_REQUIRED");
|
|
342
|
+
expect(codes).toContain("CUDA_NOT_DETECTED");
|
|
343
|
+
});
|
|
344
|
+
it("differs correctly between darwin-arm64 and linux-x64 for the identical manifest object (proves per-platform-entry resolution, not manifest-wide)", () => {
|
|
345
|
+
const manifest = dualAcceleratorManifest();
|
|
346
|
+
const darwinReport = baseReport({
|
|
347
|
+
platform: "darwin",
|
|
348
|
+
arch: "arm64",
|
|
349
|
+
appleSilicon: true,
|
|
350
|
+
nvidia: { available: false, gpus: [], error: "no gpu" },
|
|
351
|
+
docker: { available: false, running: false, gpuAccess: "unknown" },
|
|
352
|
+
});
|
|
353
|
+
const linuxReport = baseReport({ platform: "linux", arch: "x64" });
|
|
354
|
+
const darwinResult = evaluateCompatibility(manifest, darwinReport);
|
|
355
|
+
const linuxResult = evaluateCompatibility(manifest, linuxReport);
|
|
356
|
+
expect(darwinResult.status).toBe("compatible");
|
|
357
|
+
expect(linuxResult.status).toBe("compatible");
|
|
358
|
+
// Neither host's evaluation should carry the other platform's accelerator
|
|
359
|
+
// reasons — each host only ever evaluates its own matching runtime entry.
|
|
360
|
+
expect(darwinResult.reasons.some((r) => r.code.includes("CUDA"))).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
});
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MoldeskError } from "@moldesk/registry";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { MoldeskError } from "./errors.js";
|
|
3
|
+
describe("MoldeskError", () => {
|
|
4
|
+
it("carries code, message, remediation, and details", () => {
|
|
5
|
+
const error = new MoldeskError({
|
|
6
|
+
code: "DEMO_CODE",
|
|
7
|
+
message: "demo failure",
|
|
8
|
+
remediation: "do the thing",
|
|
9
|
+
details: { hello: "world" },
|
|
10
|
+
});
|
|
11
|
+
expect(error).toBeInstanceOf(Error);
|
|
12
|
+
expect(error.code).toBe("DEMO_CODE");
|
|
13
|
+
expect(error.remediation).toBe("do the thing");
|
|
14
|
+
expect(error.details).toEqual({ hello: "world" });
|
|
15
|
+
});
|
|
16
|
+
});
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Stable sha256 digest of a JSON-serializable value. Shared by installation and run provenance. */
|
|
2
|
+
export declare function digest(value: unknown): string;
|
|
3
|
+
/** sha256 of a file's contents, streamed rather than loaded fully into memory. */
|
|
4
|
+
export declare function sha256File(filePath: string): Promise<string>;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
/** Stable sha256 digest of a JSON-serializable value. Shared by installation and run provenance. */
|
|
4
|
+
export function digest(value) {
|
|
5
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
/** sha256 of a file's contents, streamed rather than loaded fully into memory. */
|
|
8
|
+
export async function sha256File(filePath) {
|
|
9
|
+
const hash = createHash("sha256");
|
|
10
|
+
for await (const chunk of fs.createReadStream(filePath))
|
|
11
|
+
hash.update(chunk);
|
|
12
|
+
return hash.digest("hex");
|
|
13
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
export { getSystemReport } from "@moldesk/runtime";
|
|
2
|
-
export type { SystemReport } from "@moldesk/runtime";
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
5
|
-
export
|
|
1
|
+
export { collectSystemReport, getSystemReport, isImageCachedLocally, pullDockerImage, probeDockerGpuAccess, } from "@moldesk/runtime";
|
|
2
|
+
export type { DiskInfo, DockerCapability, NvidiaCapability, NvidiaGpu, SystemReport, SystemReportDeps, ToolCapability, } from "@moldesk/runtime";
|
|
3
|
+
export { listAvailableModels } from "@moldesk/registry";
|
|
4
|
+
export type { AssetSpec, CommandSpec, HardwareRequirements, InputSpec, ModelManifestV1, ModelStatus, OutputSpec, PlatformId, PostInstallHook, PythonRequirement, PythonRuntimeSpec, RuntimeKind, RuntimeSpec, SourceSpec, } from "@moldesk/registry";
|
|
5
|
+
export { detectHostPlatformId, selectPythonRuntime } from "@moldesk/registry";
|
|
6
|
+
export { MoldeskError } from "./errors.js";
|
|
7
|
+
export type { MoldeskErrorData } from "./errors.js";
|
|
8
|
+
export { evaluateCompatibility, shortReason } from "./compatibility.js";
|
|
9
|
+
export type { CompatibilityOptions, CompatibilityReason, CompatibilityResult, CompatibilityStatus, RuntimeCompatibility, } from "./compatibility.js";
|
|
10
|
+
export { listInstalledModels, } from "./install-state.js";
|
|
11
|
+
export type { InstalledModel, InstalledPostInstallResult } from "./install-state.js";
|
|
12
|
+
export { createInstallationPlan, installModel, INSTALL_CONFIRMATION_DISK_BYTES, INSTALL_CONFIRMATION_DOWNLOAD_BYTES, requiresInstallConfirmation, runtimeFingerprint, uninstallModel, } from "./installation.js";
|
|
13
|
+
export type { InstallationPlanResult, InstallCostEstimate, InstallModelOptions, InstallModelResult, UninstallModelOptions, } from "./installation.js";
|
|
14
|
+
export { runModel } from "./run.js";
|
|
15
|
+
export type { RunModelOptions, RunModelResult, RunRecord } from "./run.js";
|
|
16
|
+
export { VERSION } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
export { getSystemReport } from "@moldesk/runtime";
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
1
|
+
export { collectSystemReport, getSystemReport, isImageCachedLocally, pullDockerImage, probeDockerGpuAccess, } from "@moldesk/runtime";
|
|
2
|
+
export { listAvailableModels } from "@moldesk/registry";
|
|
3
|
+
export { detectHostPlatformId, selectPythonRuntime } from "@moldesk/registry";
|
|
4
|
+
export { MoldeskError } from "./errors.js";
|
|
5
|
+
export { evaluateCompatibility, shortReason } from "./compatibility.js";
|
|
6
|
+
export { listInstalledModels, } from "./install-state.js";
|
|
7
|
+
export { createInstallationPlan, installModel, INSTALL_CONFIRMATION_DISK_BYTES, INSTALL_CONFIRMATION_DOWNLOAD_BYTES, requiresInstallConfirmation, runtimeFingerprint, uninstallModel, } from "./installation.js";
|
|
8
|
+
export { runModel } from "./run.js";
|
|
9
|
+
export { VERSION } from "./version.js";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type MoldeskPaths } from "@moldesk/runtime";
|
|
2
|
+
import type { PlatformId, PostInstallHook, RuntimeKind } from "@moldesk/registry";
|
|
3
|
+
export interface InstalledPostInstallResult {
|
|
4
|
+
hook: PostInstallHook | string;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InstalledModel {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
installationId: string;
|
|
11
|
+
model: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
modelVersion: string;
|
|
14
|
+
adapterVersion: string;
|
|
15
|
+
runtime: {
|
|
16
|
+
kind: RuntimeKind;
|
|
17
|
+
fingerprint: string;
|
|
18
|
+
python?: {
|
|
19
|
+
version: string;
|
|
20
|
+
executable: string;
|
|
21
|
+
lockSha256: string;
|
|
22
|
+
/** The host platform the selected runtime entry was resolved for, when
|
|
23
|
+
* determinable (`undefined` on a host outside the supported PlatformId set). */
|
|
24
|
+
platform?: PlatformId;
|
|
25
|
+
/** True when the manifest declared a `platforms` field on the selected entry
|
|
26
|
+
* (a genuine per-platform selection happened) vs. a legacy entry with none. */
|
|
27
|
+
platformSelected: boolean;
|
|
28
|
+
/** One entry per `postInstall` hook that ran, in declared order; empty when
|
|
29
|
+
* the selected runtime entry declared none. */
|
|
30
|
+
postInstall: InstalledPostInstallResult[];
|
|
31
|
+
};
|
|
32
|
+
docker?: {
|
|
33
|
+
image: string;
|
|
34
|
+
digest: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
environmentFingerprint: string;
|
|
38
|
+
installDir: string;
|
|
39
|
+
source?: {
|
|
40
|
+
repository: string;
|
|
41
|
+
revision: string;
|
|
42
|
+
};
|
|
43
|
+
assets: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
path: string;
|
|
46
|
+
sha256: string;
|
|
47
|
+
sizeBytes: number;
|
|
48
|
+
}>;
|
|
49
|
+
manifestSha256: string;
|
|
50
|
+
verification: {
|
|
51
|
+
passed: boolean;
|
|
52
|
+
output?: string;
|
|
53
|
+
};
|
|
54
|
+
dependencyLock: string[];
|
|
55
|
+
installedAt: string;
|
|
56
|
+
verifiedAt: string;
|
|
57
|
+
status: "ready";
|
|
58
|
+
}
|
|
59
|
+
interface InstallStateFile {
|
|
60
|
+
schemaVersion: 1;
|
|
61
|
+
installations: InstalledModel[];
|
|
62
|
+
}
|
|
63
|
+
export declare function rebuildInstallState(paths: MoldeskPaths): InstallStateFile;
|
|
64
|
+
export declare function readInstallState(paths?: MoldeskPaths): InstallStateFile;
|
|
65
|
+
export declare function writeInstallState(state: InstallStateFile, paths?: MoldeskPaths): void;
|
|
66
|
+
export declare function listInstalledModels(paths?: MoldeskPaths): InstalledModel[];
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getMoldeskPaths, modelInstallDir } from "@moldesk/runtime";
|
|
5
|
+
function statePath(paths) {
|
|
6
|
+
return path.join(paths.state, "installed-models.json");
|
|
7
|
+
}
|
|
8
|
+
function isInstalledModel(value) {
|
|
9
|
+
if (!value || typeof value !== "object")
|
|
10
|
+
return false;
|
|
11
|
+
const item = value;
|
|
12
|
+
return item["schemaVersion"] === 1 &&
|
|
13
|
+
typeof item["installationId"] === "string" &&
|
|
14
|
+
typeof item["model"] === "string" &&
|
|
15
|
+
typeof item["displayName"] === "string" &&
|
|
16
|
+
typeof item["modelVersion"] === "string" &&
|
|
17
|
+
typeof item["adapterVersion"] === "string" &&
|
|
18
|
+
!!item["runtime"] && typeof item["runtime"] === "object" &&
|
|
19
|
+
(item["runtime"]["kind"] === "python" || item["runtime"]["kind"] === "docker") &&
|
|
20
|
+
typeof item["runtime"]["fingerprint"] === "string" &&
|
|
21
|
+
typeof item["environmentFingerprint"] === "string" &&
|
|
22
|
+
typeof item["installDir"] === "string" &&
|
|
23
|
+
Array.isArray(item["dependencyLock"]) &&
|
|
24
|
+
item["dependencyLock"].every((entry) => typeof entry === "string") &&
|
|
25
|
+
typeof item["installedAt"] === "string" &&
|
|
26
|
+
typeof item["verifiedAt"] === "string" &&
|
|
27
|
+
item["status"] === "ready" &&
|
|
28
|
+
Array.isArray(item["assets"]) && item["assets"].every((asset) => {
|
|
29
|
+
if (!asset || typeof asset !== "object")
|
|
30
|
+
return false;
|
|
31
|
+
const record = asset;
|
|
32
|
+
return typeof record["id"] === "string" && typeof record["path"] === "string" &&
|
|
33
|
+
typeof record["sha256"] === "string" && typeof record["sizeBytes"] === "number";
|
|
34
|
+
}) &&
|
|
35
|
+
typeof item["manifestSha256"] === "string" &&
|
|
36
|
+
!!item["verification"] && typeof item["verification"] === "object" &&
|
|
37
|
+
item["verification"]["passed"] === true;
|
|
38
|
+
}
|
|
39
|
+
function validManagedRecord(record, recordDir, paths) {
|
|
40
|
+
try {
|
|
41
|
+
const expected = modelInstallDir(record.model, record.modelVersion, record.runtime.fingerprint, { MOLDESK_HOME: paths.home });
|
|
42
|
+
return path.resolve(record.installDir) === path.resolve(expected) && path.resolve(recordDir) === path.resolve(expected);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function scanInstallationRecords(paths) {
|
|
49
|
+
if (!fs.existsSync(paths.models))
|
|
50
|
+
return [];
|
|
51
|
+
const records = [];
|
|
52
|
+
for (const model of fs.readdirSync(paths.models, { withFileTypes: true })) {
|
|
53
|
+
if (!model.isDirectory())
|
|
54
|
+
continue;
|
|
55
|
+
const modelDir = path.join(paths.models, model.name);
|
|
56
|
+
for (const version of fs.readdirSync(modelDir, { withFileTypes: true })) {
|
|
57
|
+
if (!version.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
const versionDir = path.join(modelDir, version.name);
|
|
60
|
+
for (const runtime of fs.readdirSync(versionDir, { withFileTypes: true })) {
|
|
61
|
+
if (!runtime.isDirectory() || runtime.name.includes(".partial-") || runtime.name.includes(".backup-"))
|
|
62
|
+
continue;
|
|
63
|
+
const recordDir = path.join(versionDir, runtime.name);
|
|
64
|
+
const file = path.join(recordDir, "installation.json");
|
|
65
|
+
if (!fs.existsSync(file))
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const record = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
69
|
+
if (isInstalledModel(record) && validManagedRecord(record, recordDir, paths))
|
|
70
|
+
records.push(record);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// A directory alone never counts as installed; skip invalid records.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return records.sort((a, b) => a.installationId.localeCompare(b.installationId));
|
|
79
|
+
}
|
|
80
|
+
export function rebuildInstallState(paths) {
|
|
81
|
+
const rebuilt = { schemaVersion: 1, installations: scanInstallationRecords(paths) };
|
|
82
|
+
writeInstallState(rebuilt, paths);
|
|
83
|
+
return rebuilt;
|
|
84
|
+
}
|
|
85
|
+
export function readInstallState(paths = getMoldeskPaths()) {
|
|
86
|
+
const file = statePath(paths);
|
|
87
|
+
if (!fs.existsSync(file)) {
|
|
88
|
+
if (!fs.existsSync(paths.models))
|
|
89
|
+
return { schemaVersion: 1, installations: [] };
|
|
90
|
+
return rebuildInstallState(paths);
|
|
91
|
+
}
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return rebuildInstallState(paths);
|
|
98
|
+
}
|
|
99
|
+
if (!raw || typeof raw !== "object" || raw["schemaVersion"] !== 1) {
|
|
100
|
+
return rebuildInstallState(paths);
|
|
101
|
+
}
|
|
102
|
+
const installations = raw["installations"];
|
|
103
|
+
if (!Array.isArray(installations) || !installations.every(isInstalledModel)) {
|
|
104
|
+
return rebuildInstallState(paths);
|
|
105
|
+
}
|
|
106
|
+
for (const record of installations) {
|
|
107
|
+
const expected = modelInstallDir(record.model, record.modelVersion, record.runtime.fingerprint, { MOLDESK_HOME: paths.home });
|
|
108
|
+
if (path.resolve(record.installDir) !== path.resolve(expected)) {
|
|
109
|
+
return rebuildInstallState(paths);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { schemaVersion: 1, installations };
|
|
113
|
+
}
|
|
114
|
+
export function writeInstallState(state, paths = getMoldeskPaths()) {
|
|
115
|
+
fs.mkdirSync(paths.state, { recursive: true });
|
|
116
|
+
const file = statePath(paths);
|
|
117
|
+
const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
|
|
118
|
+
fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
119
|
+
fs.renameSync(temporary, file);
|
|
120
|
+
}
|
|
121
|
+
export function listInstalledModels(paths = getMoldeskPaths()) {
|
|
122
|
+
const state = readInstallState(paths);
|
|
123
|
+
const valid = state.installations.filter((record) => {
|
|
124
|
+
const file = path.join(record.installDir, "installation.json");
|
|
125
|
+
if (!fs.existsSync(file))
|
|
126
|
+
return false;
|
|
127
|
+
try {
|
|
128
|
+
const onDisk = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
129
|
+
return isInstalledModel(onDisk) && onDisk.installationId === record.installationId && validManagedRecord(onDisk, record.installDir, paths);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const installations = valid.length === state.installations.length ? valid : rebuildInstallState(paths).installations;
|
|
136
|
+
return installations.sort((a, b) => a.model.localeCompare(b.model));
|
|
137
|
+
}
|