@jsorm/cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/index.mjs +202 -87
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -60,8 +60,25 @@ jsorm watch
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## jsorm update
|
|
64
|
+
|
|
65
|
+
Installs the latest engine compatible with this CLI and removes older installed versions. It checks the download server now and fails, removing nothing, when the server cannot be reached or the latest engine belongs to a newer release line.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
jsorm update # install the latest compatible engine
|
|
69
|
+
jsorm update --check # report installed and latest, install nothing
|
|
70
|
+
jsorm update --force # reinstall the latest compatible engine
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The engine lives in `~/.jsorm/engines/v<version>/` (owner-only). Before every run the CLI checks the binary against the SHA-256 signed in the release manifest; a modified engine is reinstalled, never run. Other commands check for updates at most once every 24 hours; `JSORM_NO_UPDATE_CHECK=1` skips the check while an engine is installed.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
63
77
|
## Requirements
|
|
64
78
|
|
|
65
79
|
- Node.js 22+
|
|
80
|
+
- macOS or Linux, x64 or arm64 (Windows is not supported yet)
|
|
66
81
|
- `jsorm.config.ts` at project root
|
|
67
|
-
- Models in configured path
|
|
82
|
+
- Models in configured path
|
|
83
|
+
|
|
84
|
+
Run commands through your package manager: `pnpm jsorm`, `npx jsorm`, `yarn jsorm` or `bunx jsorm`. A local install does not put `jsorm` on `PATH`.
|
package/dist/index.mjs
CHANGED
|
@@ -43,6 +43,15 @@ function detectPackageManager() {
|
|
|
43
43
|
if (process.env.npm_config_user_agent?.includes("yarn")) return "yarn";
|
|
44
44
|
return "npm";
|
|
45
45
|
}
|
|
46
|
+
/** Command that runs a locally installed `jsorm` with this package manager. */
|
|
47
|
+
function cliRunner(pm) {
|
|
48
|
+
switch (pm) {
|
|
49
|
+
case "npm": return "npx jsorm";
|
|
50
|
+
case "yarn": return "yarn jsorm";
|
|
51
|
+
case "bun": return "bunx jsorm";
|
|
52
|
+
default: return "pnpm jsorm";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
46
55
|
function detectRuntime() {
|
|
47
56
|
if (typeof process !== "undefined" && process.versions?.bun) return "bun";
|
|
48
57
|
if (typeof process !== "undefined" && process.versions?.deno) return "deno";
|
|
@@ -85,76 +94,6 @@ function detectFramework() {
|
|
|
85
94
|
return "none";
|
|
86
95
|
}
|
|
87
96
|
//#endregion
|
|
88
|
-
//#region src/manifest.ts
|
|
89
|
-
const EMBEDDED_PUBLIC_KEY_B64 = "6AUQMvF7jBV+S8yv9K8hnnoYzIznQw4nf5x5LAuEsfM=";
|
|
90
|
-
function getSpkiPublicKey(rawKeyB64 = EMBEDDED_PUBLIC_KEY_B64) {
|
|
91
|
-
const rawKey = Buffer.from(rawKeyB64, "base64");
|
|
92
|
-
const spkiDer = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), rawKey]);
|
|
93
|
-
return createPublicKey({
|
|
94
|
-
key: spkiDer,
|
|
95
|
-
format: "der",
|
|
96
|
-
type: "spki"
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
function invalid(reason) {
|
|
100
|
-
return {
|
|
101
|
-
status: "invalid",
|
|
102
|
-
reason
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Verifies the envelope `{ success, signature, signed_payload }` and returns
|
|
107
|
-
* the manifest decoded from `signed_payload`, the exact bytes that were signed.
|
|
108
|
-
* The unsigned `envelope.manifest` copy is never trusted.
|
|
109
|
-
*/
|
|
110
|
-
function parseSignedEnvelope(envelope, publicKey) {
|
|
111
|
-
if (!envelope || typeof envelope !== "object") return invalid("Invalid manifest response");
|
|
112
|
-
const { success, signature, signed_payload } = envelope;
|
|
113
|
-
if (success !== true) return invalid("Invalid manifest response");
|
|
114
|
-
if (typeof signature !== "string" || typeof signed_payload !== "string") return invalid("Manifest signature or signed payload missing. Refusing to continue without signature verification.");
|
|
115
|
-
const sigBytes = Buffer.from(signature, "base64");
|
|
116
|
-
if (sigBytes.length !== 64) return invalid(`Invalid signature length: ${sigBytes.length} (expected 64)`);
|
|
117
|
-
const signedBytes = Buffer.from(signed_payload, "base64");
|
|
118
|
-
let valid = false;
|
|
119
|
-
try {
|
|
120
|
-
valid = verify(null, signedBytes, publicKey, sigBytes);
|
|
121
|
-
} catch {}
|
|
122
|
-
if (!valid) return invalid("Manifest signature verification failed. The manifest may have been tampered with.");
|
|
123
|
-
let manifest;
|
|
124
|
-
try {
|
|
125
|
-
manifest = JSON.parse(signedBytes.toString("utf-8"));
|
|
126
|
-
} catch {
|
|
127
|
-
return invalid("Signed manifest is not valid JSON");
|
|
128
|
-
}
|
|
129
|
-
if (typeof manifest?.version !== "string" || !manifest.targets || typeof manifest.targets !== "object") return invalid("Invalid manifest format");
|
|
130
|
-
return {
|
|
131
|
-
status: "ok",
|
|
132
|
-
manifest
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
async function fetchManifest(url, publicKey = getSpkiPublicKey()) {
|
|
136
|
-
let res;
|
|
137
|
-
try {
|
|
138
|
-
res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
139
|
-
} catch (err) {
|
|
140
|
-
return {
|
|
141
|
-
status: "unreachable",
|
|
142
|
-
reason: `Manifest fetch error: ${err instanceof Error ? err.message : err}`
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
if (!res.ok) return {
|
|
146
|
-
status: "unreachable",
|
|
147
|
-
reason: `Manifest fetch failed: ${res.status} ${res.statusText}`
|
|
148
|
-
};
|
|
149
|
-
let envelope;
|
|
150
|
-
try {
|
|
151
|
-
envelope = await res.json();
|
|
152
|
-
} catch {
|
|
153
|
-
return invalid("Invalid manifest response");
|
|
154
|
-
}
|
|
155
|
-
return parseSignedEnvelope(envelope, publicKey);
|
|
156
|
-
}
|
|
157
|
-
//#endregion
|
|
158
97
|
//#region src/version.ts
|
|
159
98
|
function compareVersions(a, b) {
|
|
160
99
|
const pa = a.split(".").map(Number);
|
|
@@ -181,6 +120,22 @@ function isCompatibleEngine(cliVersion, engineVersion) {
|
|
|
181
120
|
function getVersionDir(enginesDir, version) {
|
|
182
121
|
return join(enginesDir, `v${version}`);
|
|
183
122
|
}
|
|
123
|
+
/** Signed envelope stored next to an installed binary. */
|
|
124
|
+
function getEnvelopePath(enginesDir, version, binaryName) {
|
|
125
|
+
return join(getVersionDir(enginesDir, version), `${binaryName}.envelope.json`);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Creates `dir` (and missing parents) readable and writable by its owner
|
|
129
|
+
* only, and tightens it when it already exists: nobody else may replace an
|
|
130
|
+
* engine the CLI is going to run.
|
|
131
|
+
*/
|
|
132
|
+
function ensureOwnerOnlyDir(dir) {
|
|
133
|
+
mkdirSync(dir, {
|
|
134
|
+
recursive: true,
|
|
135
|
+
mode: 448
|
|
136
|
+
});
|
|
137
|
+
if (process.platform !== "win32") chmodSync(dir, 448);
|
|
138
|
+
}
|
|
184
139
|
/** Installed versions that hold `binaryName`, newest first, e.g. ["0.2.1", "0.2.0"]. */
|
|
185
140
|
function listInstalledVersions(enginesDir, binaryName) {
|
|
186
141
|
if (!existsSync(enginesDir)) return [];
|
|
@@ -229,8 +184,8 @@ function extractTarGz(tarPath, destDir) {
|
|
|
229
184
|
* Returns the installed binary path.
|
|
230
185
|
*/
|
|
231
186
|
async function installEngine(options) {
|
|
232
|
-
const { url, enginesDir, version, binaryName, expectedSha256, log = () => {} } = options;
|
|
233
|
-
|
|
187
|
+
const { url, enginesDir, version, binaryName, expectedSha256, expectedBinarySha256, envelope, log = () => {} } = options;
|
|
188
|
+
ensureOwnerOnlyDir(enginesDir);
|
|
234
189
|
const tempDir = mkdtempSync(join(enginesDir, ".tmp-"));
|
|
235
190
|
try {
|
|
236
191
|
const tarPath = join(tempDir, "engine.tar.gz");
|
|
@@ -247,10 +202,15 @@ async function installEngine(options) {
|
|
|
247
202
|
extractTarGz(tarPath, extractDir);
|
|
248
203
|
const extractedPath = join(extractDir, "jsorm");
|
|
249
204
|
if (!existsSync(extractedPath)) throw new Error(`Archive does not contain the "jsorm" binary`);
|
|
250
|
-
|
|
205
|
+
const binaryHash = await sha256File(extractedPath);
|
|
206
|
+
if (binaryHash !== expectedBinarySha256) throw new Error(`Binary checksum mismatch!\n Expected: ${expectedBinarySha256}\n Got: ${binaryHash}`);
|
|
207
|
+
if (process.platform !== "win32") chmodSync(extractedPath, 448);
|
|
208
|
+
const envelopeTemp = join(tempDir, "envelope.json");
|
|
209
|
+
writeFileSync(envelopeTemp, JSON.stringify(envelope), { mode: 384 });
|
|
251
210
|
const destDir = getVersionDir(enginesDir, version);
|
|
252
211
|
const destPath = join(destDir, binaryName);
|
|
253
|
-
|
|
212
|
+
ensureOwnerOnlyDir(destDir);
|
|
213
|
+
renameSync(envelopeTemp, getEnvelopePath(enginesDir, version, binaryName));
|
|
254
214
|
renameSync(extractedPath, destPath);
|
|
255
215
|
log(` ✓ Engine installed: ${destPath}`);
|
|
256
216
|
return destPath;
|
|
@@ -262,6 +222,124 @@ async function installEngine(options) {
|
|
|
262
222
|
}
|
|
263
223
|
}
|
|
264
224
|
//#endregion
|
|
225
|
+
//#region src/manifest.ts
|
|
226
|
+
const EMBEDDED_PUBLIC_KEY_B64 = "6AUQMvF7jBV+S8yv9K8hnnoYzIznQw4nf5x5LAuEsfM=";
|
|
227
|
+
function getSpkiPublicKey(rawKeyB64 = EMBEDDED_PUBLIC_KEY_B64) {
|
|
228
|
+
const rawKey = Buffer.from(rawKeyB64, "base64");
|
|
229
|
+
const spkiDer = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), rawKey]);
|
|
230
|
+
return createPublicKey({
|
|
231
|
+
key: spkiDer,
|
|
232
|
+
format: "der",
|
|
233
|
+
type: "spki"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function invalid(reason) {
|
|
237
|
+
return {
|
|
238
|
+
status: "invalid",
|
|
239
|
+
reason
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Verifies the envelope `{ success, signature, signed_payload }` and returns
|
|
244
|
+
* the manifest decoded from `signed_payload`, the exact bytes that were signed.
|
|
245
|
+
* The unsigned `envelope.manifest` copy is never trusted.
|
|
246
|
+
*/
|
|
247
|
+
function parseSignedEnvelope(envelope, publicKey) {
|
|
248
|
+
if (!envelope || typeof envelope !== "object") return invalid("Invalid manifest response");
|
|
249
|
+
const { success, signature, signed_payload } = envelope;
|
|
250
|
+
if (success !== true) return invalid("Invalid manifest response");
|
|
251
|
+
if (typeof signature !== "string" || typeof signed_payload !== "string") return invalid("Manifest signature or signed payload missing. Refusing to continue without signature verification.");
|
|
252
|
+
const sigBytes = Buffer.from(signature, "base64");
|
|
253
|
+
if (sigBytes.length !== 64) return invalid(`Invalid signature length: ${sigBytes.length} (expected 64)`);
|
|
254
|
+
const signedBytes = Buffer.from(signed_payload, "base64");
|
|
255
|
+
let valid = false;
|
|
256
|
+
try {
|
|
257
|
+
valid = verify(null, signedBytes, publicKey, sigBytes);
|
|
258
|
+
} catch {}
|
|
259
|
+
if (!valid) return invalid("Manifest signature verification failed. The manifest may have been tampered with.");
|
|
260
|
+
let manifest;
|
|
261
|
+
try {
|
|
262
|
+
manifest = JSON.parse(signedBytes.toString("utf-8"));
|
|
263
|
+
} catch {
|
|
264
|
+
return invalid("Signed manifest is not valid JSON");
|
|
265
|
+
}
|
|
266
|
+
if (typeof manifest?.version !== "string" || !manifest.targets || typeof manifest.targets !== "object") return invalid("Invalid manifest format");
|
|
267
|
+
return {
|
|
268
|
+
status: "ok",
|
|
269
|
+
manifest,
|
|
270
|
+
envelope: {
|
|
271
|
+
success: true,
|
|
272
|
+
signature,
|
|
273
|
+
signed_payload
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function fetchManifest(url, publicKey = getSpkiPublicKey()) {
|
|
278
|
+
let res;
|
|
279
|
+
try {
|
|
280
|
+
res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return {
|
|
283
|
+
status: "unreachable",
|
|
284
|
+
reason: `Manifest fetch error: ${err instanceof Error ? err.message : err}`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (!res.ok) return {
|
|
288
|
+
status: "unreachable",
|
|
289
|
+
reason: `Manifest fetch failed: ${res.status} ${res.statusText}`
|
|
290
|
+
};
|
|
291
|
+
let envelope;
|
|
292
|
+
try {
|
|
293
|
+
envelope = await res.json();
|
|
294
|
+
} catch {
|
|
295
|
+
return invalid("Invalid manifest response");
|
|
296
|
+
}
|
|
297
|
+
return parseSignedEnvelope(envelope, publicKey);
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/verify.ts
|
|
301
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
302
|
+
/**
|
|
303
|
+
* Checks an installed engine before it runs: its stored envelope must be
|
|
304
|
+
* signed with `publicKey`, declare this version and target, and carry the
|
|
305
|
+
* SHA-256 of the binary on disk.
|
|
306
|
+
*/
|
|
307
|
+
async function verifyInstalledEngine(enginesDir, version, binaryName, publicKey) {
|
|
308
|
+
const envelopePath = getEnvelopePath(enginesDir, version, binaryName);
|
|
309
|
+
if (!existsSync(envelopePath)) return {
|
|
310
|
+
ok: false,
|
|
311
|
+
reason: "no signed envelope next to the binary"
|
|
312
|
+
};
|
|
313
|
+
let envelope;
|
|
314
|
+
try {
|
|
315
|
+
envelope = JSON.parse(readFileSync(envelopePath, "utf-8"));
|
|
316
|
+
} catch {
|
|
317
|
+
return {
|
|
318
|
+
ok: false,
|
|
319
|
+
reason: "the stored envelope is not valid JSON"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const result = parseSignedEnvelope(envelope, publicKey);
|
|
323
|
+
if (result.status !== "ok") return {
|
|
324
|
+
ok: false,
|
|
325
|
+
reason: result.reason
|
|
326
|
+
};
|
|
327
|
+
if (result.manifest.version !== version) return {
|
|
328
|
+
ok: false,
|
|
329
|
+
reason: `the stored envelope is for v${result.manifest.version}`
|
|
330
|
+
};
|
|
331
|
+
const expected = result.manifest.targets[binaryName]?.binary_sha256;
|
|
332
|
+
if (typeof expected !== "string" || !SHA256_HEX.test(expected)) return {
|
|
333
|
+
ok: false,
|
|
334
|
+
reason: `the stored envelope has no binary_sha256 for ${binaryName}`
|
|
335
|
+
};
|
|
336
|
+
if (await sha256File(join(getVersionDir(enginesDir, version), binaryName)) !== expected) return {
|
|
337
|
+
ok: false,
|
|
338
|
+
reason: "the binary does not match its signed SHA-256"
|
|
339
|
+
};
|
|
340
|
+
return { ok: true };
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
265
343
|
//#region src/resolve.ts
|
|
266
344
|
const DAY_MS = 864e5;
|
|
267
345
|
const LAST_CHECK_FILE = ".last-check";
|
|
@@ -290,19 +368,40 @@ async function fetchTrustedManifest(options) {
|
|
|
290
368
|
if (result.status === "invalid") throw new Error(result.reason);
|
|
291
369
|
log(` ✓ Manifest signature verified`);
|
|
292
370
|
log(` ✓ Manifest v${result.manifest.version} (${Object.keys(result.manifest.targets).length} targets)`);
|
|
293
|
-
return
|
|
371
|
+
return {
|
|
372
|
+
manifest: result.manifest,
|
|
373
|
+
envelope: result.envelope
|
|
374
|
+
};
|
|
294
375
|
}
|
|
295
376
|
/**
|
|
296
377
|
* Returns the path of an engine binary that is safe to run: the installed one
|
|
297
378
|
* when it is current, or a freshly installed stable release. Only engines of
|
|
298
|
-
* the CLI's release line are used
|
|
299
|
-
*
|
|
300
|
-
*
|
|
379
|
+
* the CLI's release line are used, and an installed engine only runs after
|
|
380
|
+
* its binary matches the signed SHA-256 stored next to it. When the server
|
|
381
|
+
* cannot be reached, a verified installed engine is used as is. Throws when
|
|
382
|
+
* no engine can be provided or when the server's answer cannot be trusted.
|
|
301
383
|
*/
|
|
302
384
|
async function resolveEngine(options) {
|
|
303
|
-
const { enginesDir, binaryName, downloadUrl, cliVersion, log = () => {}, checkIntervalMs = DAY_MS, skipUpdateCheck = false, force = false, requireLiveCheck = false, now = Date.now } = options;
|
|
385
|
+
const { enginesDir, binaryName, downloadUrl, cliVersion, publicKey = getSpkiPublicKey(), log = () => {}, updateCommand = "jsorm update", checkIntervalMs = DAY_MS, skipUpdateCheck = false, force = false, requireLiveCheck = false, now = Date.now } = options;
|
|
304
386
|
const compatible = (version) => isCompatibleEngine(cliVersion, version);
|
|
305
|
-
|
|
387
|
+
let installedVer = detectInstalledVersion(enginesDir, binaryName, compatible);
|
|
388
|
+
let rejectedVer = null;
|
|
389
|
+
if (installedVer) {
|
|
390
|
+
const check = await verifyInstalledEngine(enginesDir, installedVer, binaryName, publicKey);
|
|
391
|
+
if (!check.ok) {
|
|
392
|
+
log(` ⚠ Installed engine v${installedVer} failed verification: ${check.reason}`);
|
|
393
|
+
try {
|
|
394
|
+
rmSync(getVersionDir(enginesDir, installedVer), {
|
|
395
|
+
recursive: true,
|
|
396
|
+
force: true
|
|
397
|
+
});
|
|
398
|
+
} catch (err) {
|
|
399
|
+
log(` ⚠ Could not remove engine v${installedVer}: ${err instanceof Error ? err.message : err}`);
|
|
400
|
+
}
|
|
401
|
+
rejectedVer = installedVer;
|
|
402
|
+
installedVer = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
306
405
|
const installedPath = installedVer ? join(getVersionDir(enginesDir, installedVer), binaryName) : null;
|
|
307
406
|
if (installedPath && !force && !requireLiveCheck) {
|
|
308
407
|
if (skipUpdateCheck) return installedPath;
|
|
@@ -310,15 +409,20 @@ async function resolveEngine(options) {
|
|
|
310
409
|
const elapsed = lastCheck === null ? Infinity : now() - lastCheck;
|
|
311
410
|
if (checkIntervalMs > 0 && elapsed >= 0 && elapsed < checkIntervalMs) return installedPath;
|
|
312
411
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
412
|
+
const trusted = await fetchTrustedManifest({
|
|
413
|
+
...options,
|
|
414
|
+
publicKey
|
|
415
|
+
});
|
|
416
|
+
if ("unreachable" in trusted) {
|
|
315
417
|
if (installedPath && !requireLiveCheck) {
|
|
316
|
-
log(` ⚠ ${
|
|
418
|
+
log(` ⚠ ${trusted.unreachable}`);
|
|
317
419
|
log(` ⚠ Using installed engine v${installedVer} without checking for updates`);
|
|
318
420
|
return installedPath;
|
|
319
421
|
}
|
|
320
|
-
throw new Error(`${
|
|
422
|
+
if (rejectedVer) throw new Error(`${trusted.unreachable}\n ✖ Installed engine v${rejectedVer} failed verification and cannot be reinstalled offline.\n Run "${updateCommand}" once the download server is reachable.`);
|
|
423
|
+
throw new Error(`${trusted.unreachable}\n ✖ Failed to fetch manifest.`);
|
|
321
424
|
}
|
|
425
|
+
const { manifest, envelope } = trusted;
|
|
322
426
|
writeLastCheck(enginesDir, now());
|
|
323
427
|
const stableVersion = manifest.version;
|
|
324
428
|
if (!compatible(stableVersion)) {
|
|
@@ -331,11 +435,13 @@ async function resolveEngine(options) {
|
|
|
331
435
|
}
|
|
332
436
|
const target = manifest.targets[binaryName];
|
|
333
437
|
if (!target) throw new Error(`No binary available for ${binaryName}\n Available: ${Object.keys(manifest.targets).join(", ")}`);
|
|
438
|
+
if (!/^[0-9a-f]{64}$/.test(target.binary_sha256 ?? "")) throw new Error(`Engine v${stableVersion} has no signed binary hash for ${binaryName}; it cannot be verified after install.`);
|
|
334
439
|
if (installedPath && installedVer && !force && compareVersions(stableVersion, installedVer) <= 0) {
|
|
335
440
|
log(` ✓ Engine v${installedVer} is up to date (stable: v${stableVersion})`);
|
|
336
441
|
return installedPath;
|
|
337
442
|
}
|
|
338
|
-
if (
|
|
443
|
+
if (rejectedVer) log(` Reinstalling engine v${stableVersion}...`);
|
|
444
|
+
else if (!installedVer) log(` Engine not found. Downloading v${stableVersion}...`);
|
|
339
445
|
else if (compareVersions(stableVersion, installedVer) > 0) log(` Update available: v${installedVer} → v${stableVersion}`);
|
|
340
446
|
else log(` Reinstalling engine v${stableVersion}...`);
|
|
341
447
|
log(` Requesting download URL...`);
|
|
@@ -350,6 +456,8 @@ async function resolveEngine(options) {
|
|
|
350
456
|
version: stableVersion,
|
|
351
457
|
binaryName,
|
|
352
458
|
expectedSha256: target.sha256,
|
|
459
|
+
expectedBinarySha256: target.binary_sha256,
|
|
460
|
+
envelope,
|
|
353
461
|
log
|
|
354
462
|
});
|
|
355
463
|
}
|
|
@@ -358,8 +466,9 @@ async function checkForUpdate(options) {
|
|
|
358
466
|
const { enginesDir, binaryName, cliVersion, now = Date.now } = options;
|
|
359
467
|
const compatible = (version) => isCompatibleEngine(cliVersion, version);
|
|
360
468
|
const installed = detectInstalledVersion(enginesDir, binaryName, compatible);
|
|
361
|
-
const
|
|
362
|
-
if ("unreachable" in
|
|
469
|
+
const trusted = await fetchTrustedManifest(options);
|
|
470
|
+
if ("unreachable" in trusted) throw new Error(trusted.unreachable);
|
|
471
|
+
const { manifest } = trusted;
|
|
363
472
|
writeLastCheck(enginesDir, now());
|
|
364
473
|
const latestCompatible = compatible(manifest.version);
|
|
365
474
|
return {
|
|
@@ -488,6 +597,11 @@ async function main() {
|
|
|
488
597
|
console.log(" JSORM_NO_UPDATE_CHECK=1 Skip engine update checks while an engine is installed");
|
|
489
598
|
return;
|
|
490
599
|
}
|
|
600
|
+
try {
|
|
601
|
+
ensureOwnerOnlyDir(dirname(ENGINES_DIR));
|
|
602
|
+
} catch (err) {
|
|
603
|
+
console.error(` ⚠ Could not restrict ${dirname(ENGINES_DIR)}: ${err instanceof Error ? err.message : err}`);
|
|
604
|
+
}
|
|
491
605
|
const resolveOptions = {
|
|
492
606
|
enginesDir: ENGINES_DIR,
|
|
493
607
|
binaryName: getEngineBinaryName(),
|
|
@@ -495,6 +609,7 @@ async function main() {
|
|
|
495
609
|
downloadUrl: STABLE_DOWNLOAD_URL,
|
|
496
610
|
cliVersion: readVersion(),
|
|
497
611
|
skipUpdateCheck: process.env.JSORM_NO_UPDATE_CHECK === "1",
|
|
612
|
+
updateCommand: `${cliRunner(detectPackageManager())} update`,
|
|
498
613
|
log: console.log
|
|
499
614
|
};
|
|
500
615
|
try {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsorm/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "CLI de JSORM",
|
|
6
6
|
"license": "MPL-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jsorm/core": "1.2.
|
|
38
|
+
"@jsorm/core": "1.2.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^26.1.1",
|