@jsorm/cli 0.1.3 → 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/dist/index.mjs +180 -39
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
-
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -155,7 +155,7 @@ async function fetchManifest(url, publicKey = getSpkiPublicKey()) {
|
|
|
155
155
|
return parseSignedEnvelope(envelope, publicKey);
|
|
156
156
|
}
|
|
157
157
|
//#endregion
|
|
158
|
-
//#region src/
|
|
158
|
+
//#region src/version.ts
|
|
159
159
|
function compareVersions(a, b) {
|
|
160
160
|
const pa = a.split(".").map(Number);
|
|
161
161
|
const pb = b.split(".").map(Number);
|
|
@@ -167,15 +167,37 @@ function compareVersions(a, b) {
|
|
|
167
167
|
}
|
|
168
168
|
return 0;
|
|
169
169
|
}
|
|
170
|
+
/** Release line of a version: "0.2" while the major is 0, "1" from 1.0 on. */
|
|
171
|
+
function releaseLine(version) {
|
|
172
|
+
const [major = "0", minor = "0"] = version.split(".");
|
|
173
|
+
return major === "0" ? `0.${minor}` : major;
|
|
174
|
+
}
|
|
175
|
+
/** The CLI only runs engines of its own release line. */
|
|
176
|
+
function isCompatibleEngine(cliVersion, engineVersion) {
|
|
177
|
+
return releaseLine(cliVersion) === releaseLine(engineVersion);
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/install.ts
|
|
170
181
|
function getVersionDir(enginesDir, version) {
|
|
171
182
|
return join(enginesDir, `v${version}`);
|
|
172
183
|
}
|
|
173
|
-
/**
|
|
174
|
-
function
|
|
175
|
-
if (!existsSync(enginesDir)) return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
/** Installed versions that hold `binaryName`, newest first, e.g. ["0.2.1", "0.2.0"]. */
|
|
185
|
+
function listInstalledVersions(enginesDir, binaryName) {
|
|
186
|
+
if (!existsSync(enginesDir)) return [];
|
|
187
|
+
return readdirSync(enginesDir).filter((d) => d.startsWith("v") && existsSync(join(enginesDir, d, binaryName))).map((d) => d.slice(1)).sort((a, b) => compareVersions(b, a));
|
|
188
|
+
}
|
|
189
|
+
/** Newest installed version accepted by `accept`, e.g. "0.2.1". */
|
|
190
|
+
function detectInstalledVersion(enginesDir, binaryName, accept = () => true) {
|
|
191
|
+
return listInstalledVersions(enginesDir, binaryName).find(accept) ?? null;
|
|
192
|
+
}
|
|
193
|
+
/** Removes every installed version directory except `keepVersion`. Returns the removed versions. */
|
|
194
|
+
function pruneInstalledVersions(enginesDir, binaryName, keepVersion) {
|
|
195
|
+
const removed = listInstalledVersions(enginesDir, binaryName).filter((v) => v !== keepVersion);
|
|
196
|
+
for (const version of removed) rmSync(getVersionDir(enginesDir, version), {
|
|
197
|
+
recursive: true,
|
|
198
|
+
force: true
|
|
199
|
+
});
|
|
200
|
+
return removed;
|
|
179
201
|
}
|
|
180
202
|
async function sha256File(filePath) {
|
|
181
203
|
const hash = createHash("sha256");
|
|
@@ -241,38 +263,81 @@ async function installEngine(options) {
|
|
|
241
263
|
}
|
|
242
264
|
//#endregion
|
|
243
265
|
//#region src/resolve.ts
|
|
266
|
+
const DAY_MS = 864e5;
|
|
267
|
+
const LAST_CHECK_FILE = ".last-check";
|
|
268
|
+
function readLastCheck(enginesDir) {
|
|
269
|
+
try {
|
|
270
|
+
const value = Number(readFileSync(join(enginesDir, LAST_CHECK_FILE), "utf-8").trim());
|
|
271
|
+
return Number.isFinite(value) ? value : null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function writeLastCheck(enginesDir, time) {
|
|
277
|
+
try {
|
|
278
|
+
mkdirSync(enginesDir, { recursive: true });
|
|
279
|
+
writeFileSync(join(enginesDir, LAST_CHECK_FILE), String(time));
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
function incompatibleMessage(cliVersion, engineVersion) {
|
|
283
|
+
return `The latest engine v${engineVersion} needs a newer @jsorm/cli (this is v${cliVersion}, release line ${releaseLine(cliVersion)}).\n Upgrade it: npm install -D @jsorm/cli@latest`;
|
|
284
|
+
}
|
|
285
|
+
async function fetchTrustedManifest(options) {
|
|
286
|
+
const { manifestUrl, publicKey = getSpkiPublicKey(), log = () => {} } = options;
|
|
287
|
+
log(` Fetching manifest from ${manifestUrl}...`);
|
|
288
|
+
const result = await fetchManifest(manifestUrl, publicKey);
|
|
289
|
+
if (result.status === "unreachable") return { unreachable: result.reason };
|
|
290
|
+
if (result.status === "invalid") throw new Error(result.reason);
|
|
291
|
+
log(` ✓ Manifest signature verified`);
|
|
292
|
+
log(` ✓ Manifest v${result.manifest.version} (${Object.keys(result.manifest.targets).length} targets)`);
|
|
293
|
+
return result.manifest;
|
|
294
|
+
}
|
|
244
295
|
/**
|
|
245
296
|
* Returns the path of an engine binary that is safe to run: the installed one
|
|
246
|
-
* when it is current, or a freshly installed stable release.
|
|
247
|
-
*
|
|
248
|
-
*
|
|
297
|
+
* when it is current, or a freshly installed stable release. Only engines of
|
|
298
|
+
* the CLI's release line are used. When the server cannot be reached, an
|
|
299
|
+
* installed engine is used as is. Throws when no engine can be provided or
|
|
300
|
+
* when the server's answer cannot be trusted.
|
|
249
301
|
*/
|
|
250
302
|
async function resolveEngine(options) {
|
|
251
|
-
const { enginesDir, binaryName,
|
|
252
|
-
const
|
|
303
|
+
const { enginesDir, binaryName, downloadUrl, cliVersion, log = () => {}, checkIntervalMs = DAY_MS, skipUpdateCheck = false, force = false, requireLiveCheck = false, now = Date.now } = options;
|
|
304
|
+
const compatible = (version) => isCompatibleEngine(cliVersion, version);
|
|
305
|
+
const installedVer = detectInstalledVersion(enginesDir, binaryName, compatible);
|
|
253
306
|
const installedPath = installedVer ? join(getVersionDir(enginesDir, installedVer), binaryName) : null;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
307
|
+
if (installedPath && !force && !requireLiveCheck) {
|
|
308
|
+
if (skipUpdateCheck) return installedPath;
|
|
309
|
+
const lastCheck = readLastCheck(enginesDir);
|
|
310
|
+
const elapsed = lastCheck === null ? Infinity : now() - lastCheck;
|
|
311
|
+
if (checkIntervalMs > 0 && elapsed >= 0 && elapsed < checkIntervalMs) return installedPath;
|
|
312
|
+
}
|
|
313
|
+
const manifest = await fetchTrustedManifest(options);
|
|
314
|
+
if ("unreachable" in manifest) {
|
|
315
|
+
if (installedPath && !requireLiveCheck) {
|
|
316
|
+
log(` ⚠ ${manifest.unreachable}`);
|
|
259
317
|
log(` ⚠ Using installed engine v${installedVer} without checking for updates`);
|
|
260
318
|
return installedPath;
|
|
261
319
|
}
|
|
262
|
-
throw new Error(`${
|
|
320
|
+
throw new Error(`${manifest.unreachable}\n ✖ Failed to fetch manifest.`);
|
|
263
321
|
}
|
|
264
|
-
|
|
265
|
-
const { manifest } = result;
|
|
322
|
+
writeLastCheck(enginesDir, now());
|
|
266
323
|
const stableVersion = manifest.version;
|
|
267
|
-
|
|
268
|
-
|
|
324
|
+
if (!compatible(stableVersion)) {
|
|
325
|
+
if (installedPath && !requireLiveCheck) {
|
|
326
|
+
log(` ⚠ ${incompatibleMessage(cliVersion, stableVersion)}`);
|
|
327
|
+
log(` ⚠ Using installed engine v${installedVer}`);
|
|
328
|
+
return installedPath;
|
|
329
|
+
}
|
|
330
|
+
throw new Error(incompatibleMessage(cliVersion, stableVersion));
|
|
331
|
+
}
|
|
269
332
|
const target = manifest.targets[binaryName];
|
|
270
333
|
if (!target) throw new Error(`No binary available for ${binaryName}\n Available: ${Object.keys(manifest.targets).join(", ")}`);
|
|
271
|
-
if (installedPath && installedVer && compareVersions(stableVersion, installedVer) <= 0) {
|
|
334
|
+
if (installedPath && installedVer && !force && compareVersions(stableVersion, installedVer) <= 0) {
|
|
272
335
|
log(` ✓ Engine v${installedVer} is up to date (stable: v${stableVersion})`);
|
|
273
336
|
return installedPath;
|
|
274
337
|
}
|
|
275
|
-
|
|
338
|
+
if (!installedVer) log(` Engine not found. Downloading v${stableVersion}...`);
|
|
339
|
+
else if (compareVersions(stableVersion, installedVer) > 0) log(` Update available: v${installedVer} → v${stableVersion}`);
|
|
340
|
+
else log(` Reinstalling engine v${stableVersion}...`);
|
|
276
341
|
log(` Requesting download URL...`);
|
|
277
342
|
const dlRes = await fetch(`${downloadUrl}/${binaryName}`, { signal: AbortSignal.timeout(1e4) });
|
|
278
343
|
if (!dlRes.ok) throw new Error(`Download request failed: ${dlRes.status} ${dlRes.statusText}`);
|
|
@@ -288,6 +353,66 @@ async function resolveEngine(options) {
|
|
|
288
353
|
log
|
|
289
354
|
});
|
|
290
355
|
}
|
|
356
|
+
/** Reads the live manifest and compares it with the installed engine, without installing. */
|
|
357
|
+
async function checkForUpdate(options) {
|
|
358
|
+
const { enginesDir, binaryName, cliVersion, now = Date.now } = options;
|
|
359
|
+
const compatible = (version) => isCompatibleEngine(cliVersion, version);
|
|
360
|
+
const installed = detectInstalledVersion(enginesDir, binaryName, compatible);
|
|
361
|
+
const manifest = await fetchTrustedManifest(options);
|
|
362
|
+
if ("unreachable" in manifest) throw new Error(manifest.unreachable);
|
|
363
|
+
writeLastCheck(enginesDir, now());
|
|
364
|
+
const latestCompatible = compatible(manifest.version);
|
|
365
|
+
return {
|
|
366
|
+
installed,
|
|
367
|
+
latest: manifest.version,
|
|
368
|
+
compatible: latestCompatible,
|
|
369
|
+
updateAvailable: latestCompatible && (installed === null || compareVersions(manifest.version, installed) > 0)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/update.ts
|
|
374
|
+
/**
|
|
375
|
+
* `jsorm update`: checks the live manifest now, installs the newest
|
|
376
|
+
* compatible engine (again when `force`), and removes every other installed
|
|
377
|
+
* version. Fails, removing nothing, when the live manifest cannot be reached
|
|
378
|
+
* or its engine belongs to another release line.
|
|
379
|
+
*/
|
|
380
|
+
async function updateEngine(options) {
|
|
381
|
+
const { enginesDir, binaryName, cliVersion, log = () => {} } = options;
|
|
382
|
+
await resolveEngine({
|
|
383
|
+
...options,
|
|
384
|
+
checkIntervalMs: 0,
|
|
385
|
+
skipUpdateCheck: false,
|
|
386
|
+
requireLiveCheck: true
|
|
387
|
+
});
|
|
388
|
+
const version = detectInstalledVersion(enginesDir, binaryName, (v) => isCompatibleEngine(cliVersion, v));
|
|
389
|
+
if (!version) throw new Error("No compatible engine is installed after the update");
|
|
390
|
+
const removed = pruneInstalledVersions(enginesDir, binaryName, version);
|
|
391
|
+
for (const old of removed) log(` ✓ Removed engine v${old}`);
|
|
392
|
+
log(` ✓ Engine v${version} is installed`);
|
|
393
|
+
return {
|
|
394
|
+
version,
|
|
395
|
+
removed
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const UPDATE_FLAGS = ["--check", "--force"];
|
|
399
|
+
/** Validates `jsorm update` flags; returns an error message, or null when valid. */
|
|
400
|
+
function validateUpdateArgs(args) {
|
|
401
|
+
const unknown = args.filter((arg) => !UPDATE_FLAGS.includes(arg));
|
|
402
|
+
if (unknown.length > 0) return `Unknown option for jsorm update: ${unknown.join(" ")} (use --check or --force)`;
|
|
403
|
+
if (args.includes("--check") && args.includes("--force")) return "jsorm update: --check and --force cannot be combined";
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
/** `jsorm update --check`: reports the installed and latest engine without installing. */
|
|
407
|
+
async function reportUpdate(options) {
|
|
408
|
+
const { log = () => {} } = options;
|
|
409
|
+
const status = await checkForUpdate(options);
|
|
410
|
+
log(` Installed engine: ${status.installed ? `v${status.installed}` : "none"}`);
|
|
411
|
+
log(` Latest engine: v${status.latest}`);
|
|
412
|
+
if (!status.compatible) log(` ⚠ v${status.latest} needs a newer @jsorm/cli: npm install -D @jsorm/cli@latest`);
|
|
413
|
+
else if (status.updateAvailable) log(` Update available. Run: jsorm update`);
|
|
414
|
+
else log(` ✓ Up to date`);
|
|
415
|
+
}
|
|
291
416
|
//#endregion
|
|
292
417
|
//#region src/index.ts
|
|
293
418
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -351,32 +476,48 @@ async function main() {
|
|
|
351
476
|
console.log(" configure Interactive project setup wizard");
|
|
352
477
|
console.log(" gen Generate types, model types, and runtime files");
|
|
353
478
|
console.log(" watch Watch model files and regenerate on changes");
|
|
479
|
+
console.log(" update Install the latest compatible engine and remove old ones");
|
|
480
|
+
console.log(" (--check: only report, --force: reinstall)");
|
|
354
481
|
console.log();
|
|
355
482
|
console.log("Options:");
|
|
356
483
|
console.log(" --help, -h Print help");
|
|
357
484
|
console.log(" --version, -v, -V Print version");
|
|
358
485
|
console.log(" --dry-run Show what would be generated without writing files");
|
|
486
|
+
console.log();
|
|
487
|
+
console.log("Environment:");
|
|
488
|
+
console.log(" JSORM_NO_UPDATE_CHECK=1 Skip engine update checks while an engine is installed");
|
|
359
489
|
return;
|
|
360
490
|
}
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
491
|
+
const resolveOptions = {
|
|
492
|
+
enginesDir: ENGINES_DIR,
|
|
493
|
+
binaryName: getEngineBinaryName(),
|
|
494
|
+
manifestUrl: STABLE_MANIFEST_URL,
|
|
495
|
+
downloadUrl: STABLE_DOWNLOAD_URL,
|
|
496
|
+
cliVersion: readVersion(),
|
|
497
|
+
skipUpdateCheck: process.env.JSORM_NO_UPDATE_CHECK === "1",
|
|
498
|
+
log: console.log
|
|
499
|
+
};
|
|
367
500
|
try {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
501
|
+
if (args[0] === "update") {
|
|
502
|
+
const invalid = validateUpdateArgs(args.slice(1));
|
|
503
|
+
if (invalid) throw new Error(invalid);
|
|
504
|
+
if (args.includes("--check")) await reportUpdate(resolveOptions);
|
|
505
|
+
else await updateEngine({
|
|
506
|
+
...resolveOptions,
|
|
507
|
+
force: args.includes("--force")
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const localEnginePath = getLocalDevEnginePath();
|
|
512
|
+
if (localEnginePath) {
|
|
513
|
+
executeEngine(localEnginePath, args);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
executeEngine(await resolveEngine(resolveOptions), args);
|
|
375
517
|
} catch (err) {
|
|
376
518
|
console.error(` ✖ ${err instanceof Error ? err.message : err}`);
|
|
377
519
|
process.exit(1);
|
|
378
520
|
}
|
|
379
|
-
executeEngine(enginePath, args);
|
|
380
521
|
}
|
|
381
522
|
main();
|
|
382
523
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsorm/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
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.0
|
|
38
|
+
"@jsorm/core": "1.2.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^26.1.1",
|