@jsorm/cli 0.1.1 → 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.
Files changed (2) hide show
  1. package/dist/index.mjs +369 -214
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,28 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { execSync, spawn } from "node:child_process";
3
- import { createHash, createPublicKey, verify } from "node:crypto";
4
- import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync } from "node:fs";
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
4
  import { dirname, join } from "node:path";
6
5
  import { homedir } from "node:os";
7
6
  import { fileURLToPath } from "node:url";
8
- //#region src/index.ts
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const VERSION = "0.1.1";
12
- const ENGINES_DIR = join(homedir(), ".jsorm", "engines");
13
- const DOWNLOAD_BASE = "https://download.jsorm.dev";
14
- const STABLE_MANIFEST_URL = `${DOWNLOAD_BASE}/engine/stable/manifest`;
15
- const STABLE_DOWNLOAD_URL = `${DOWNLOAD_BASE}/engine/download`;
16
- const EMBEDDED_PUBLIC_KEY_B64 = "pFJ+WOzwsMPbZyvF/bMPB0UNCvuosqFN7Hvjd/qmP78=";
17
- function getSpkiPublicKey() {
18
- const rawKey = Buffer.from(EMBEDDED_PUBLIC_KEY_B64, "base64");
19
- const spkiDer = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), rawKey]);
20
- return createPublicKey({
21
- key: spkiDer,
22
- format: "der",
23
- type: "spki"
24
- });
25
- }
7
+ import { createHash, createPublicKey, verify } from "node:crypto";
8
+ import { Readable } from "node:stream";
9
+ import { pipeline } from "node:stream/promises";
10
+ //#region src/platform.ts
26
11
  function detectPlatform() {
27
12
  switch (process.platform) {
28
13
  case "linux": return "linux";
@@ -38,6 +23,10 @@ function detectArch() {
38
23
  default: throw new Error(`Unsupported architecture: ${process.arch}`);
39
24
  }
40
25
  }
26
+ function getEngineBinaryName() {
27
+ const platform = detectPlatform();
28
+ return `jsorm-${platform}-${detectArch()}${platform === "windows" ? ".exe" : ""}`;
29
+ }
41
30
  function detectPackageManager() {
42
31
  let dir = process.cwd();
43
32
  for (let i = 0; i < 10; i++) {
@@ -95,37 +84,78 @@ function detectFramework() {
95
84
  }
96
85
  return "none";
97
86
  }
98
- function getEngineBinaryName() {
99
- const platform = detectPlatform();
100
- return `jsorm-${platform}-${detectArch()}${platform === "windows" ? ".exe" : ""}`;
101
- }
102
- function getLocalDevEnginePath() {
103
- const debug = join(__dirname, "..", "..", "..", "engine", "target", "debug", "jsorm");
104
- if (existsSync(debug)) return debug;
105
- const release = join(__dirname, "..", "..", "..", "engine", "target", "release", "jsorm");
106
- if (existsSync(release)) return release;
107
- return null;
87
+ //#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
+ });
108
98
  }
109
- function getInstalledEnginePath(binaryName) {
110
- const version = detectInstalledVersion(binaryName);
111
- if (!version) return null;
112
- return join(getVersionDir(version), binaryName);
99
+ function invalid(reason) {
100
+ return {
101
+ status: "invalid",
102
+ reason
103
+ };
113
104
  }
114
- function verifySignature(data, signature) {
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;
115
119
  try {
116
- const publicKey = getSpkiPublicKey();
117
- return verify(null, data, publicKey, signature);
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"));
118
126
  } catch {
119
- return false;
127
+ return invalid("Signed manifest is not valid JSON");
120
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
+ };
121
134
  }
122
- function verifyManifest(signedPayload, signature) {
123
- if (signature.length !== 64) {
124
- console.log(` ✖ Invalid signature length: ${signature.length} (expected 64)`);
125
- return false;
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
+ };
126
144
  }
127
- return verifySignature(signedPayload, signature);
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);
128
156
  }
157
+ //#endregion
158
+ //#region src/version.ts
129
159
  function compareVersions(a, b) {
130
160
  const pa = a.split(".").map(Number);
131
161
  const pb = b.split(".").map(Number);
@@ -137,153 +167,287 @@ function compareVersions(a, b) {
137
167
  }
138
168
  return 0;
139
169
  }
140
- function detectInstalledVersion(target) {
141
- if (!existsSync(ENGINES_DIR)) return null;
142
- const dirs = readdirSync(ENGINES_DIR).filter((d) => d.startsWith("v")).sort((a, b) => {
143
- const va = a.slice(1);
144
- return compareVersions(b.slice(1), va);
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
181
+ function getVersionDir(enginesDir, version) {
182
+ return join(enginesDir, `v${version}`);
183
+ }
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
145
199
  });
146
- for (const dir of dirs) {
147
- const binPath = join(ENGINES_DIR, dir, target);
148
- if (existsSync(binPath)) return dir.slice(1);
149
- }
150
- return null;
200
+ return removed;
151
201
  }
152
- function getVersionDir(version) {
153
- return join(ENGINES_DIR, `v${version}`);
202
+ async function sha256File(filePath) {
203
+ const hash = createHash("sha256");
204
+ await pipeline(createReadStream(filePath), hash);
205
+ return hash.digest("hex");
154
206
  }
155
- async function downloadManifest() {
207
+ async function downloadToFile(url, destPath) {
208
+ const res = await fetch(url, { signal: AbortSignal.timeout(6e4) });
209
+ if (!res.ok || !res.body) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
210
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
211
+ }
212
+ function extractTarGz(tarPath, destDir) {
156
213
  try {
157
- console.log(` Fetching manifest from ${STABLE_MANIFEST_URL}...`);
158
- const manifestRes = await fetch(STABLE_MANIFEST_URL, { signal: AbortSignal.timeout(1e4) });
159
- if (!manifestRes.ok) {
160
- console.log(` ✖ Manifest fetch failed: ${manifestRes.status} ${manifestRes.statusText}`);
161
- return null;
162
- }
163
- const envelope = await manifestRes.json();
164
- if (!envelope.success || !envelope.manifest) {
165
- console.log(` ✖ Invalid manifest response`);
166
- return null;
167
- }
168
- const manifest = envelope.manifest;
169
- if (!manifest.targets || typeof manifest.targets !== "object") {
170
- console.log(` ✖ Invalid manifest format`);
171
- return null;
172
- }
173
- if (envelope.signature && envelope.signed_payload) {
174
- const signedBytes = Buffer.from(envelope.signed_payload, "base64");
175
- const sigBytes = Buffer.from(envelope.signature, "base64");
176
- console.log(` Verifying manifest signature...`);
177
- if (!verifyManifest(signedBytes, sigBytes)) {
178
- console.error(` ✖ Manifest signature verification failed.`);
179
- console.error(` The manifest may have been tampered with.`);
180
- process.exit(1);
181
- }
182
- console.log(` ✓ Manifest signature verified`);
183
- } else {
184
- console.error(` ✖ Manifest signature or signed payload missing.`);
185
- console.error(` Refusing to continue without signature verification.`);
186
- return null;
187
- }
188
- console.log(` ✓ Manifest v${manifest.version} (${Object.keys(manifest.targets).length} targets)`);
189
- return {
190
- manifest,
191
- stableVersion: manifest.version
192
- };
214
+ execFileSync("tar", [
215
+ "-xzf",
216
+ tarPath,
217
+ "-C",
218
+ destDir
219
+ ], { stdio: "pipe" });
193
220
  } catch (err) {
194
- console.log(` ✖ Manifest fetch error: ${err instanceof Error ? err.message : err}`);
195
- return null;
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ throw new Error(`tar extraction failed: ${msg}`);
196
223
  }
197
224
  }
198
- async function sha256File(filePath) {
199
- return new Promise((resolve, reject) => {
200
- const hash = createHash("sha256");
201
- const stream = createReadStream(filePath);
202
- stream.on("data", (data) => hash.update(data));
203
- stream.on("end", () => resolve(hash.digest("hex")));
204
- stream.on("error", reject);
205
- });
206
- }
207
- async function downloadAndExtractEngine(url, destDir, binaryName, expectedSha256) {
208
- const tempDir = join(ENGINES_DIR, ".tmp");
209
- mkdirSync(tempDir, { recursive: true });
210
- const tarPath = join(tempDir, `${binaryName}.tar.gz`);
225
+ /**
226
+ * Downloads, verifies and extracts the engine inside a private temp directory,
227
+ * and only then moves the binary into `<enginesDir>/v<version>/<binaryName>`.
228
+ * A failed or interrupted install never leaves a binary at the final path.
229
+ * Returns the installed binary path.
230
+ */
231
+ async function installEngine(options) {
232
+ const { url, enginesDir, version, binaryName, expectedSha256, log = () => {} } = options;
233
+ mkdirSync(enginesDir, { recursive: true });
234
+ const tempDir = mkdtempSync(join(enginesDir, ".tmp-"));
211
235
  try {
212
- console.log(` Downloading from ${url}...`);
213
- const res = await fetch(url, { signal: AbortSignal.timeout(6e4) });
214
- if (!res.ok) {
215
- console.log(` ✖ Download failed: ${res.status} ${res.statusText}`);
216
- return false;
217
- }
218
- const fileStream = createWriteStream(tarPath);
219
- if (res.body) {
220
- const reader = res.body.getReader();
221
- while (true) {
222
- const { done, value } = await reader.read();
223
- if (done) break;
224
- fileStream.write(value);
225
- }
226
- }
227
- fileStream.end();
228
- await new Promise((resolve, reject) => {
229
- fileStream.on("finish", resolve);
230
- fileStream.on("error", reject);
231
- });
232
- const tarSize = statSync(tarPath).size;
233
- console.log(` ✓ Downloaded ${(tarSize / 1024).toFixed(1)} KB`);
234
- console.log(` Verifying checksum...`);
236
+ const tarPath = join(tempDir, "engine.tar.gz");
237
+ log(` Downloading from ${url}...`);
238
+ await downloadToFile(url, tarPath);
239
+ log(` ✓ Downloaded ${(statSync(tarPath).size / 1024).toFixed(1)} KB`);
240
+ log(` Verifying checksum...`);
235
241
  const hash = await sha256File(tarPath);
236
- if (hash !== expectedSha256) {
237
- console.error(` ✖ Checksum mismatch!`);
238
- console.error(` Expected: ${expectedSha256}`);
239
- console.error(` Got: ${hash}`);
240
- unlinkSync(tarPath);
241
- return false;
242
- }
243
- console.log(` ✓ Checksum verified`);
244
- console.log(` Extracting...`);
245
- mkdirSync(destDir, { recursive: true });
246
- await extractTarGz(tarPath, destDir);
247
- const extractedPath = join(destDir, "jsorm");
242
+ if (hash !== expectedSha256) throw new Error(`Checksum mismatch!\n Expected: ${expectedSha256}\n Got: ${hash}`);
243
+ log(` ✓ Checksum verified`);
244
+ log(` Extracting...`);
245
+ const extractDir = join(tempDir, "out");
246
+ mkdirSync(extractDir);
247
+ extractTarGz(tarPath, extractDir);
248
+ const extractedPath = join(extractDir, "jsorm");
249
+ if (!existsSync(extractedPath)) throw new Error(`Archive does not contain the "jsorm" binary`);
250
+ if (process.platform !== "win32") chmodSync(extractedPath, 493);
251
+ const destDir = getVersionDir(enginesDir, version);
248
252
  const destPath = join(destDir, binaryName);
249
- if (existsSync(extractedPath) && extractedPath !== destPath) renameSync(extractedPath, destPath);
250
- if (process.platform !== "win32") chmodSync(destPath, 493);
251
- console.log(` ✓ Engine installed: ${destPath}`);
252
- return true;
253
- } catch (err) {
254
- console.log(` ✖ Extract error: ${err instanceof Error ? err.message : err}`);
255
- return false;
253
+ mkdirSync(destDir, { recursive: true });
254
+ renameSync(extractedPath, destPath);
255
+ log(` ✓ Engine installed: ${destPath}`);
256
+ return destPath;
256
257
  } finally {
257
- try {
258
- unlinkSync(tarPath);
259
- } catch {}
258
+ rmSync(tempDir, {
259
+ recursive: true,
260
+ force: true
261
+ });
260
262
  }
261
263
  }
262
- async function extractTarGz(tarPath, destDir) {
264
+ //#endregion
265
+ //#region src/resolve.ts
266
+ const DAY_MS = 864e5;
267
+ const LAST_CHECK_FILE = ".last-check";
268
+ function readLastCheck(enginesDir) {
263
269
  try {
264
- execSync(`tar -xzf "${tarPath}" -C "${destDir}"`, { stdio: "pipe" });
265
- } catch (err) {
266
- const msg = err instanceof Error ? err.message : String(err);
267
- throw new Error(`tar extraction failed: ${msg}`);
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
+ }
295
+ /**
296
+ * Returns the path of an engine binary that is safe to run: the installed one
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.
301
+ */
302
+ async function resolveEngine(options) {
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);
306
+ const installedPath = installedVer ? join(getVersionDir(enginesDir, installedVer), binaryName) : null;
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}`);
317
+ log(` ⚠ Using installed engine v${installedVer} without checking for updates`);
318
+ return installedPath;
319
+ }
320
+ throw new Error(`${manifest.unreachable}\n ✖ Failed to fetch manifest.`);
321
+ }
322
+ writeLastCheck(enginesDir, now());
323
+ const stableVersion = manifest.version;
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
+ }
332
+ const target = manifest.targets[binaryName];
333
+ if (!target) throw new Error(`No binary available for ${binaryName}\n Available: ${Object.keys(manifest.targets).join(", ")}`);
334
+ if (installedPath && installedVer && !force && compareVersions(stableVersion, installedVer) <= 0) {
335
+ log(` ✓ Engine v${installedVer} is up to date (stable: v${stableVersion})`);
336
+ return installedPath;
337
+ }
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}...`);
341
+ log(` Requesting download URL...`);
342
+ const dlRes = await fetch(`${downloadUrl}/${binaryName}`, { signal: AbortSignal.timeout(1e4) });
343
+ if (!dlRes.ok) throw new Error(`Download request failed: ${dlRes.status} ${dlRes.statusText}`);
344
+ const dl = await dlRes.json();
345
+ if (dl.version !== stableVersion) throw new Error(`Version mismatch: server returned v${dl.version}, manifest says v${stableVersion}`);
346
+ if (dl.sha256 !== target.sha256) throw new Error(`SHA-256 mismatch: server returned ${String(dl.sha256).slice(0, 16)}, manifest says ${target.sha256.slice(0, 16)}`);
347
+ return installEngine({
348
+ url: dl.url,
349
+ enginesDir,
350
+ version: stableVersion,
351
+ binaryName,
352
+ expectedSha256: target.sha256,
353
+ log
354
+ });
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
+ }
416
+ //#endregion
417
+ //#region src/index.ts
418
+ const __filename = fileURLToPath(import.meta.url);
419
+ const __dirname = dirname(__filename);
420
+ const ENGINES_DIR = join(homedir(), ".jsorm", "engines");
421
+ const DOWNLOAD_BASE = "https://download.jsorm.dev";
422
+ const STABLE_MANIFEST_URL = `${DOWNLOAD_BASE}/engine/stable/manifest`;
423
+ const STABLE_DOWNLOAD_URL = `${DOWNLOAD_BASE}/engine/download`;
424
+ function readVersion() {
425
+ try {
426
+ return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
427
+ } catch {
428
+ return "unknown";
268
429
  }
269
430
  }
431
+ function getLocalDevEnginePath() {
432
+ const debug = join(__dirname, "..", "..", "..", "engine", "target", "debug", "jsorm");
433
+ if (existsSync(debug)) return debug;
434
+ const release = join(__dirname, "..", "..", "..", "engine", "target", "release", "jsorm");
435
+ if (existsSync(release)) return release;
436
+ return null;
437
+ }
270
438
  function executeEngine(enginePath, args) {
271
- const projectDir = process.cwd();
272
- const framework = detectFramework();
273
- const pm = detectPackageManager();
274
- const runtime = detectRuntime();
275
439
  const engineArgs = [
276
440
  ...args,
277
441
  "--framework",
278
- framework,
442
+ detectFramework(),
279
443
  "--pm",
280
- pm,
444
+ detectPackageManager(),
281
445
  "--runtime",
282
- runtime
446
+ detectRuntime()
283
447
  ];
284
448
  const child = spawn(enginePath, engineArgs, {
285
449
  stdio: "inherit",
286
- cwd: projectDir
450
+ cwd: process.cwd()
287
451
  });
288
452
  child.on("error", (err) => {
289
453
  console.error(` ✖ Failed to start engine: ${err.message}`);
@@ -295,8 +459,8 @@ function executeEngine(enginePath, args) {
295
459
  }
296
460
  async function main() {
297
461
  const args = process.argv.slice(2);
298
- if (args.includes("--version") || args.includes("-v")) {
299
- console.log(VERSION);
462
+ if (args.includes("--version") || args.includes("-v") || args.includes("-V")) {
463
+ console.log(readVersion());
300
464
  return;
301
465
  }
302
466
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -305,64 +469,55 @@ async function main() {
305
469
  executeEngine(localEnginePath, args);
306
470
  return;
307
471
  }
308
- console.log(`jsorm ${VERSION}`);
472
+ console.log(`jsorm ${readVersion()}`);
309
473
  console.log("JSORM CLI — TypeScript ORM code generation");
310
474
  console.log();
311
475
  console.log("Commands:");
312
476
  console.log(" configure Interactive project setup wizard");
313
477
  console.log(" gen Generate types, model types, and runtime files");
314
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)");
315
481
  console.log();
316
482
  console.log("Options:");
317
- console.log(" --help, -h Print help");
318
- console.log(" --version, -V Print version");
319
- console.log(" --dry-run Show what would be generated without writing files");
320
- return;
321
- }
322
- const localEnginePath = getLocalDevEnginePath();
323
- if (localEnginePath) {
324
- executeEngine(localEnginePath, args);
325
- return;
326
- }
327
- const binaryName = getEngineBinaryName();
328
- const result = await downloadManifest();
329
- if (!result) {
330
- console.error(` ✖ Failed to fetch manifest.`);
331
- process.exit(1);
332
- }
333
- const { manifest, stableVersion } = result;
334
- const target = manifest.targets[binaryName];
335
- if (!target) {
336
- console.error(` ✖ No binary available for ${binaryName}`);
337
- console.error(` Available: ${Object.keys(manifest.targets).join(", ")}`);
338
- process.exit(1);
339
- }
340
- const installedPath = getInstalledEnginePath(binaryName);
341
- const installedVer = installedPath ? detectInstalledVersion(binaryName) : null;
342
- if (installedVer && compareVersions(stableVersion, installedVer) <= 0) {
343
- console.log(` ✓ Engine v${installedVer} is up to date (stable: v${stableVersion})`);
344
- executeEngine(installedPath, args);
483
+ console.log(" --help, -h Print help");
484
+ console.log(" --version, -v, -V Print version");
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");
345
489
  return;
346
490
  }
347
- if (installedVer) console.log(` Update available: v${installedVer} → v${stableVersion}`);
348
- else console.log(` Engine not found. Downloading v${stableVersion}...`);
349
- console.log(` Requesting download URL...`);
350
- const dlRes = await fetch(`${STABLE_DOWNLOAD_URL}/${binaryName}`, { signal: AbortSignal.timeout(1e4) });
351
- if (!dlRes.ok) {
352
- console.error(` ✖ Download request failed: ${dlRes.status} ${dlRes.statusText}`);
353
- process.exit(1);
354
- }
355
- const dl = await dlRes.json();
356
- if (dl.version !== stableVersion) {
357
- console.error(` ✖ Version mismatch: server returned v${dl.version}, manifest says v${stableVersion}`);
358
- process.exit(1);
359
- }
360
- if (dl.sha256 !== target.sha256) {
361
- console.error(` ✖ SHA-256 mismatch: server returned ${dl.sha256.slice(0, 16)}, manifest says ${target.sha256.slice(0, 16)}`);
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
+ };
500
+ try {
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);
517
+ } catch (err) {
518
+ console.error(` ✖ ${err instanceof Error ? err.message : err}`);
362
519
  process.exit(1);
363
520
  }
364
- const destDir = getVersionDir(stableVersion);
365
- if (await downloadAndExtractEngine(dl.url, destDir, binaryName, target.sha256)) executeEngine(join(destDir, binaryName), args);
366
521
  }
367
522
  main();
368
523
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jsorm/cli",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "description": "CLI de JSORM",
6
6
  "license": "MPL-2.0",
7
7
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "main": "./dist/index.mjs",
21
21
  "types": "./dist/index.d.mts",
22
22
  "bin": {
23
- "@jsorm/cli": "./dist/index.mjs"
23
+ "jsorm": "./dist/index.mjs"
24
24
  },
25
25
  "exports": {
26
26
  ".": {
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@jsorm/core": "1.0.2"
38
+ "@jsorm/core": "1.2.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^26.1.1",