@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.
- package/dist/index.mjs +369 -214
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
function invalid(reason) {
|
|
100
|
+
return {
|
|
101
|
+
status: "invalid",
|
|
102
|
+
reason
|
|
103
|
+
};
|
|
113
104
|
}
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
153
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
195
|
-
|
|
221
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
222
|
+
throw new Error(`tar extraction failed: ${msg}`);
|
|
196
223
|
}
|
|
197
224
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
rmSync(tempDir, {
|
|
259
|
+
recursive: true,
|
|
260
|
+
force: true
|
|
261
|
+
});
|
|
260
262
|
}
|
|
261
263
|
}
|
|
262
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
442
|
+
detectFramework(),
|
|
279
443
|
"--pm",
|
|
280
|
-
|
|
444
|
+
detectPackageManager(),
|
|
281
445
|
"--runtime",
|
|
282
|
-
|
|
446
|
+
detectRuntime()
|
|
283
447
|
];
|
|
284
448
|
const child = spawn(enginePath, engineArgs, {
|
|
285
449
|
stdio: "inherit",
|
|
286
|
-
cwd:
|
|
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(
|
|
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 ${
|
|
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
|
|
318
|
-
console.log(" --version, -V
|
|
319
|
-
console.log(" --dry-run
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
process.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
38
|
+
"@jsorm/core": "1.2.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^26.1.1",
|