@shings/raven 0.1.13

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/bin/raven.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require("../src/cli");
4
+
5
+ main(process.argv.slice(2))
6
+ .then((code) => {
7
+ if (Number.isInteger(code)) process.exit(code);
8
+ })
9
+ .catch((error) => {
10
+ console.error(error && error.message ? error.message : String(error));
11
+ process.exit(1);
12
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@shings/raven",
3
+ "version": "0.1.13",
4
+ "description": "npx runner for the Raven engine and UI GitHub Release bundle",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/shingyusik/oracle-todo.git",
10
+ "directory": "npm/raven"
11
+ },
12
+ "bin": {
13
+ "raven": "bin/raven.js"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "test": "node --test"
24
+ }
25
+ }
package/src/archive.js ADDED
@@ -0,0 +1,99 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs/promises");
3
+ const path = require("node:path");
4
+ const { spawn } = require("node:child_process");
5
+
6
+ async function downloadFile(url, destination, { fetchImpl = globalThis.fetch } = {}) {
7
+ if (!fetchImpl) throw new Error("Download requires Node.js 18 or newer with global fetch support");
8
+ const response = await fetchImpl(url);
9
+ if (!response.ok) throw new Error(`Download failed with HTTP ${response.status}`);
10
+ await fs.mkdir(path.dirname(destination), { recursive: true });
11
+ const buffer = Buffer.from(await response.arrayBuffer());
12
+ await fs.writeFile(destination, buffer);
13
+ }
14
+
15
+ function run(command, args, options = {}) {
16
+ return new Promise((resolve, reject) => {
17
+ const child = spawn(command, args, { stdio: "ignore", ...options });
18
+ child.on("error", reject);
19
+ child.on("close", (code) => {
20
+ if (code === 0) resolve();
21
+ else reject(new Error(`${command} exited with code ${code}`));
22
+ });
23
+ });
24
+ }
25
+
26
+ async function extractArchive(archivePath, destination, { platform = process.platform } = {}) {
27
+ await fs.mkdir(destination, { recursive: true });
28
+ if (archivePath.endsWith(".tar.gz")) {
29
+ await run("tar", ["-xzf", archivePath, "-C", destination]);
30
+ return;
31
+ }
32
+ if (archivePath.endsWith(".zip")) {
33
+ if (platform === "win32") {
34
+ await run("powershell", ["-NoProfile", "-Command", `Expand-Archive -Force ${JSON.stringify(archivePath)} ${JSON.stringify(destination)}`]);
35
+ return;
36
+ }
37
+ await run("unzip", ["-q", "-o", archivePath, "-d", destination]);
38
+ return;
39
+ }
40
+ throw new Error(`Unsupported archive format: ${archivePath}`);
41
+ }
42
+
43
+ async function verifyChecksum(archivePath, checksumsText, expectedName) {
44
+ const line = checksumsText
45
+ .split(/\r?\n/)
46
+ .map((entry) => entry.trim())
47
+ .find((entry) => entry.endsWith(` ${expectedName}`) || entry.endsWith(` *${expectedName}`));
48
+ if (!line) {
49
+ throw new Error(`Checksum entry not found for ${expectedName}`);
50
+ }
51
+
52
+ const expected = line.split(/\s+/)[0];
53
+ const actual = crypto.createHash("sha256").update(await fs.readFile(archivePath)).digest("hex");
54
+ if (actual !== expected) {
55
+ throw new Error(`Checksum mismatch for ${expectedName}`);
56
+ }
57
+ }
58
+
59
+ async function findExtractedBinary(directory, binaryName) {
60
+ const entries = await fs.readdir(directory, { withFileTypes: true });
61
+ for (const entry of entries) {
62
+ const entryPath = path.join(directory, entry.name);
63
+ if (entry.isFile() && entry.name === binaryName) {
64
+ return entryPath;
65
+ }
66
+ if (entry.isDirectory()) {
67
+ try {
68
+ const found = await findExtractedBinary(entryPath, binaryName);
69
+ if (found) return found;
70
+ } catch (error) {
71
+ if (error.code !== "ENOENT") throw error;
72
+ }
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ async function activateBinary(paths, binaryName) {
79
+ await fs.mkdir(paths.binDir, { recursive: true });
80
+ let source = paths.versionedBinary;
81
+ try {
82
+ await fs.access(source);
83
+ } catch (error) {
84
+ if (error.code !== "ENOENT") throw error;
85
+ source = await findExtractedBinary(paths.versionDir, binaryName);
86
+ if (!source) throw error;
87
+ }
88
+ await fs.copyFile(source, paths.activeBinary);
89
+ if (!binaryName.endsWith(".exe")) {
90
+ await fs.chmod(paths.activeBinary, 0o755);
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ activateBinary,
96
+ downloadFile,
97
+ extractArchive,
98
+ verifyChecksum,
99
+ };
package/src/cache.js ADDED
@@ -0,0 +1,62 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+
4
+ function pathsFor(root, version, binaryName) {
5
+ const binDir = path.join(root, "bin");
6
+ const versionsDir = path.join(root, "versions");
7
+ const versionDir = path.join(versionsDir, version);
8
+ return {
9
+ root,
10
+ binDir,
11
+ versionsDir,
12
+ versionDir,
13
+ metadataPath: path.join(root, "metadata.json"),
14
+ activeBinary: path.join(binDir, binaryName),
15
+ versionedBinary: path.join(versionDir, binaryName),
16
+ };
17
+ }
18
+
19
+ function uiPathsFor(root, version) {
20
+ const uiDir = path.join(root, "ui");
21
+ const uiVersionDir = path.join(uiDir, version);
22
+ return {
23
+ uiDir,
24
+ uiVersionDir,
25
+ uiIndexPath: path.join(uiVersionDir, "index.html"),
26
+ };
27
+ }
28
+
29
+ async function readMetadata(root) {
30
+ try {
31
+ return JSON.parse(await fs.readFile(path.join(root, "metadata.json"), "utf8"));
32
+ } catch (error) {
33
+ if (error.code === "ENOENT") return null;
34
+ throw error;
35
+ }
36
+ }
37
+
38
+ async function writeMetadata(root, metadata) {
39
+ await fs.mkdir(root, { recursive: true });
40
+ await fs.writeFile(path.join(root, "metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
41
+ }
42
+
43
+ async function isUsableFile(filePath, executable = false) {
44
+ if (!filePath) return false;
45
+ try {
46
+ if (!(await fs.stat(filePath)).isFile()) return false;
47
+ const mode = fs.constants.R_OK | (executable && process.platform !== "win32" ? fs.constants.X_OK : 0);
48
+ await fs.access(filePath, mode);
49
+ return true;
50
+ } catch (error) {
51
+ if (["EACCES", "ENOENT", "ENOTDIR"].includes(error.code)) return false;
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ module.exports = {
57
+ isUsableFile,
58
+ pathsFor,
59
+ readMetadata,
60
+ uiPathsFor,
61
+ writeMetadata,
62
+ };
package/src/cli.js ADDED
@@ -0,0 +1,65 @@
1
+ const path = require("node:path");
2
+
3
+ const { isUsableFile, readMetadata } = require("./cache");
4
+ const { topLevelCommandIndex } = require("./command-index");
5
+ const { cacheDir, PACKAGE_NAME } = require("./config");
6
+ const { installBundle, installEngine, updateBundle } = require("./install");
7
+ const { runEngine } = require("./runner");
8
+ const { runUi } = require("./ui-command");
9
+
10
+ async function main(args, options = {}) {
11
+ const env = options.env || process.env;
12
+ const log = options.log || console.log;
13
+ const install = options.installEngine || installEngine;
14
+ const installAll = options.installBundle || installBundle;
15
+ const updateAll = options.updateBundle || updateBundle;
16
+ const run = options.runEngine || runEngine;
17
+ const ui = options.runUi || runUi;
18
+ const command = args[topLevelCommandIndex(args)];
19
+
20
+ if (command === "install") {
21
+ const result = await installAll({ env });
22
+ log(`${PACKAGE_NAME}: ${result.status || "installed"} ${result.installedVersion || ""}`.trim());
23
+ return 0;
24
+ }
25
+
26
+ if (command === "update") {
27
+ const result = await updateAll({ env });
28
+ log(`${PACKAGE_NAME}: ${result.status || "installed"} ${result.installedVersion || ""}`.trim());
29
+ return 0;
30
+ }
31
+
32
+ if (command === "ui") {
33
+ return ui(args, { env, installBundle: installAll, log });
34
+ }
35
+
36
+ if (command === "version") {
37
+ const metadata = await readMetadata(cacheDir(env));
38
+ log(`${PACKAGE_NAME} wrapper`);
39
+ log(`raven ${metadata ? metadata.installedVersion : "not installed"}`);
40
+ log(`raven-ui ${metadata && metadata.uiVersion ? metadata.uiVersion : "not installed"}`);
41
+ return 0;
42
+ }
43
+
44
+ if (command === "doctor") {
45
+ const metadata = await readMetadata(cacheDir(env));
46
+ if (!metadata) throw new Error("raven is not installed; run install first");
47
+ if (!(await isUsableFile(metadata.binaryPath, true))) {
48
+ throw new Error("raven binary is missing or unusable; run install first");
49
+ }
50
+ if (!metadata.uiPath) throw new Error("raven-ui is not installed; run install first");
51
+ if (!(await isUsableFile(path.join(metadata.uiPath, "index.html")))) {
52
+ throw new Error("raven-ui is missing or unusable; run install first");
53
+ }
54
+ log(`cache ok: ${metadata.binaryPath}`);
55
+ log(`ui ok: ${metadata.uiPath}`);
56
+ return 0;
57
+ }
58
+
59
+ const installed = await install({ env });
60
+ const binaryPath = installed.binaryPath;
61
+ const exitCode = await run(args, { binaryPath });
62
+ return exitCode;
63
+ }
64
+
65
+ module.exports = { main };
@@ -0,0 +1,9 @@
1
+ function topLevelCommandIndex(args) {
2
+ let index = 0;
3
+ while (args[index] === "--home" || args[index]?.startsWith("--home=")) {
4
+ index += args[index] === "--home" ? 2 : 1;
5
+ }
6
+ return index;
7
+ }
8
+
9
+ module.exports = { topLevelCommandIndex };
package/src/config.js ADDED
@@ -0,0 +1,21 @@
1
+ const path = require("node:path");
2
+ const os = require("node:os");
3
+
4
+ const PACKAGE_NAME = "@shings/raven";
5
+ const COMMAND_NAME = "raven";
6
+ const ENGINE_BINARY = "raven";
7
+ const GITHUB_REPOSITORY = "shingyusik/oracle-todo";
8
+ const DEFAULT_CACHE_DIR = path.join(os.homedir(), ".local", "share", "raven");
9
+
10
+ function cacheDir(env = process.env) {
11
+ return env.RAVEN_CACHE_DIR || DEFAULT_CACHE_DIR;
12
+ }
13
+
14
+ module.exports = {
15
+ PACKAGE_NAME,
16
+ COMMAND_NAME,
17
+ ENGINE_BINARY,
18
+ GITHUB_REPOSITORY,
19
+ DEFAULT_CACHE_DIR,
20
+ cacheDir,
21
+ };
package/src/github.js ADDED
@@ -0,0 +1,38 @@
1
+ function tagFor(version) {
2
+ if (!version) return null;
3
+ return String(version).startsWith("v") ? String(version) : `v${version}`;
4
+ }
5
+
6
+ async function fetchRelease({ version, repository, token, fetchImpl = globalThis.fetch }) {
7
+ if (!fetchImpl) {
8
+ throw new Error("This command requires Node.js 18 or newer with global fetch support");
9
+ }
10
+
11
+ const endpoint = version
12
+ ? `https://api.github.com/repos/${repository}/releases/tags/${tagFor(version)}`
13
+ : `https://api.github.com/repos/${repository}/releases/latest`;
14
+ const headers = {
15
+ Accept: "application/vnd.github+json",
16
+ "User-Agent": "@shings/raven",
17
+ };
18
+ if (token) headers.Authorization = `Bearer ${token}`;
19
+
20
+ const response = await fetchImpl(endpoint, { headers });
21
+ if (!response.ok) {
22
+ throw new Error(`GitHub release lookup failed with HTTP ${response.status}`);
23
+ }
24
+ return response.json();
25
+ }
26
+
27
+ function selectAsset(release, expectedName) {
28
+ const asset = (release.assets || []).find((candidate) => candidate.name === expectedName);
29
+ if (!asset) {
30
+ throw new Error(`Release asset not found: ${expectedName}`);
31
+ }
32
+ return asset;
33
+ }
34
+
35
+ module.exports = {
36
+ fetchRelease,
37
+ selectAsset,
38
+ };
package/src/install.js ADDED
@@ -0,0 +1,149 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+
4
+ const { downloadFile, extractArchive, activateBinary, verifyChecksum } = require("./archive");
5
+ const { isUsableFile, pathsFor, readMetadata, writeMetadata } = require("./cache");
6
+ const { cacheDir, GITHUB_REPOSITORY } = require("./config");
7
+ const { fetchRelease, selectAsset } = require("./github");
8
+ const { assetName, resolvePlatform } = require("./platform");
9
+ const { installUiArtifact } = require("./ui-artifact");
10
+ const { compareVersions, normalizeVersion } = require("./version");
11
+
12
+ async function fetchRequestedRelease(options, env) {
13
+ return (options.fetchReleaseImpl || fetchRelease)({
14
+ repository: options.repository || GITHUB_REPOSITORY,
15
+ version: env.RAVEN_VERSION,
16
+ token: env.RAVEN_GITHUB_TOKEN,
17
+ fetchImpl: options.fetchImpl,
18
+ });
19
+ }
20
+
21
+ async function installBundle(options = {}) {
22
+ const env = options.env || process.env;
23
+ const cacheRoot = options.cacheRoot || cacheDir(env);
24
+ const platformInfo = options.platformInfo || resolvePlatform();
25
+ const metadata = await readMetadata(cacheRoot);
26
+ const release = await fetchRequestedRelease(options, env);
27
+ const version = normalizeVersion(release.tag_name);
28
+
29
+ if (
30
+ metadata
31
+ && metadata.installedVersion === version
32
+ && metadata.uiVersion === version
33
+ && await isUsableFile(metadata.binaryPath, true)
34
+ && await isUsableFile(metadata.uiPath && path.join(metadata.uiPath, "index.html"))
35
+ ) {
36
+ return { status: "already-installed", ...metadata };
37
+ }
38
+
39
+ const engine = await installRelease({
40
+ ...options,
41
+ cacheRoot,
42
+ platformInfo,
43
+ release,
44
+ version,
45
+ activate: false,
46
+ writeMetadata: false,
47
+ });
48
+ const ui = await installUiArtifact({ ...options, cacheRoot, release, version });
49
+ await activateBinary(pathsFor(cacheRoot, version, platformInfo.binaryName), platformInfo.binaryName);
50
+ const metadataNext = {
51
+ ...engine,
52
+ uiVersion: ui.uiVersion,
53
+ uiAssetName: ui.uiAssetName,
54
+ uiPath: ui.uiPath,
55
+ };
56
+ await writeMetadata(cacheRoot, metadataNext);
57
+ return metadataNext;
58
+ }
59
+
60
+ async function updateBundle(options = {}) {
61
+ return installBundle(options);
62
+ }
63
+
64
+ async function installEngine(options = {}) {
65
+ const env = options.env || process.env;
66
+ const cacheRoot = options.cacheRoot || cacheDir(env);
67
+ const platformInfo = options.platformInfo || resolvePlatform();
68
+ const metadata = await readMetadata(cacheRoot);
69
+ const requestedVersion = env.RAVEN_VERSION;
70
+
71
+ if (metadata && !requestedVersion && await isUsableFile(metadata.binaryPath, true)) {
72
+ return { status: "already-installed", ...metadata };
73
+ }
74
+
75
+ const release = await (options.fetchReleaseImpl || fetchRelease)({
76
+ repository: options.repository || GITHUB_REPOSITORY,
77
+ version: requestedVersion,
78
+ token: env.RAVEN_GITHUB_TOKEN,
79
+ fetchImpl: options.fetchImpl,
80
+ });
81
+
82
+ const version = normalizeVersion(release.tag_name);
83
+ if (metadata && metadata.installedVersion === version && await isUsableFile(metadata.binaryPath, true)) {
84
+ return { status: "already-installed", ...metadata };
85
+ }
86
+
87
+ return installRelease({ ...options, cacheRoot, platformInfo, release, version });
88
+ }
89
+
90
+ async function updateEngine(options = {}) {
91
+ const env = options.env || process.env;
92
+ const cacheRoot = options.cacheRoot || cacheDir(env);
93
+ const platformInfo = options.platformInfo || resolvePlatform();
94
+ const metadata = await readMetadata(cacheRoot);
95
+ const release = await (options.fetchReleaseImpl || fetchRelease)({
96
+ repository: options.repository || GITHUB_REPOSITORY,
97
+ version: env.RAVEN_VERSION,
98
+ token: env.RAVEN_GITHUB_TOKEN,
99
+ fetchImpl: options.fetchImpl,
100
+ });
101
+ const version = normalizeVersion(release.tag_name);
102
+
103
+ if (
104
+ metadata
105
+ && compareVersions(version, metadata.installedVersion) <= 0
106
+ && await isUsableFile(metadata.binaryPath, true)
107
+ ) {
108
+ return { status: "up-to-date", ...metadata };
109
+ }
110
+
111
+ return installRelease({ ...options, cacheRoot, platformInfo, release, version });
112
+ }
113
+
114
+ async function installRelease(options) {
115
+ const expectedAsset = assetName(options.version, options.platformInfo.target);
116
+ const asset = selectAsset(options.release, expectedAsset);
117
+ const checksumAsset = selectAsset(options.release, "SHA256SUMS");
118
+ const paths = pathsFor(options.cacheRoot, options.version, options.platformInfo.binaryName);
119
+ const archivePath = path.join(paths.versionDir, expectedAsset);
120
+ const checksumPath = path.join(paths.versionDir, "SHA256SUMS");
121
+
122
+ await fs.rm(paths.versionDir, { recursive: true, force: true });
123
+ await fs.mkdir(paths.versionDir, { recursive: true });
124
+ await (options.downloadFileImpl || downloadFile)(asset.browser_download_url, archivePath, { fetchImpl: options.fetchImpl });
125
+ await (options.downloadFileImpl || downloadFile)(checksumAsset.browser_download_url, checksumPath, { fetchImpl: options.fetchImpl });
126
+ await verifyChecksum(archivePath, await fs.readFile(checksumPath, "utf8"), expectedAsset);
127
+ await (options.extractArchiveImpl || extractArchive)(archivePath, paths.versionDir, { platform: process.platform });
128
+ if (options.activate !== false) {
129
+ await activateBinary(paths, options.platformInfo.binaryName);
130
+ }
131
+
132
+ const metadata = {
133
+ installedVersion: options.version,
134
+ assetName: expectedAsset,
135
+ binaryPath: paths.activeBinary,
136
+ installedAt: (options.now || (() => new Date()))().toISOString(),
137
+ };
138
+ if (options.writeMetadata !== false) {
139
+ await writeMetadata(options.cacheRoot, metadata);
140
+ }
141
+ return { status: "installed", ...metadata };
142
+ }
143
+
144
+ module.exports = {
145
+ installBundle,
146
+ installEngine,
147
+ updateBundle,
148
+ updateEngine,
149
+ };
@@ -0,0 +1,37 @@
1
+ const SUPPORTED_TARGETS = [
2
+ "aarch64-apple-darwin",
3
+ "x86_64-apple-darwin",
4
+ "x86_64-unknown-linux-gnu",
5
+ "x86_64-pc-windows-msvc",
6
+ ];
7
+
8
+ const TARGETS = {
9
+ "darwin/arm64": { target: "aarch64-apple-darwin", extension: ".tar.gz", binaryName: "raven" },
10
+ "darwin/x64": { target: "x86_64-apple-darwin", extension: ".tar.gz", binaryName: "raven" },
11
+ "linux/x64": { target: "x86_64-unknown-linux-gnu", extension: ".tar.gz", binaryName: "raven" },
12
+ "win32/x64": { target: "x86_64-pc-windows-msvc", extension: ".zip", binaryName: "raven.exe" },
13
+ };
14
+
15
+ function normalizeVersion(version) {
16
+ return String(version).replace(/^v/, "");
17
+ }
18
+
19
+ function resolvePlatform({ platform = process.platform, arch = process.arch } = {}) {
20
+ const key = `${platform}/${arch}`;
21
+ const resolved = TARGETS[key];
22
+ if (!resolved) {
23
+ throw new Error(`Unsupported platform ${key}. Supported targets: ${SUPPORTED_TARGETS.join(", ")}`);
24
+ }
25
+ return { platform, arch, ...resolved };
26
+ }
27
+
28
+ function assetName(version, target) {
29
+ const extension = target === "x86_64-pc-windows-msvc" ? ".zip" : ".tar.gz";
30
+ return `raven-${normalizeVersion(version)}-${target}${extension}`;
31
+ }
32
+
33
+ module.exports = {
34
+ SUPPORTED_TARGETS,
35
+ assetName,
36
+ resolvePlatform,
37
+ };
package/src/runner.js ADDED
@@ -0,0 +1,11 @@
1
+ const { spawn } = require("node:child_process");
2
+
3
+ function runEngine(args, { binaryPath, stdio = "inherit" }) {
4
+ return new Promise((resolve, reject) => {
5
+ const child = spawn(binaryPath, args, { stdio });
6
+ child.on("error", reject);
7
+ child.on("close", (code) => resolve(code || 0));
8
+ });
9
+ }
10
+
11
+ module.exports = { runEngine };
@@ -0,0 +1,67 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+
4
+ const { downloadFile, extractArchive, verifyChecksum } = require("./archive");
5
+ const { uiPathsFor } = require("./cache");
6
+ const { selectAsset } = require("./github");
7
+
8
+ function uiAssetName(version) {
9
+ return `raven-ui-${version}.tar.gz`;
10
+ }
11
+
12
+ async function findIndexRoot(directory) {
13
+ const indexPath = path.join(directory, "index.html");
14
+ try {
15
+ await fs.access(indexPath);
16
+ return directory;
17
+ } catch (error) {
18
+ if (error.code !== "ENOENT") throw error;
19
+ }
20
+
21
+ const entries = await fs.readdir(directory, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory()) continue;
24
+ const found = await findIndexRoot(path.join(directory, entry.name));
25
+ if (found) return found;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ async function installUiArtifact(options) {
31
+ const expectedAsset = uiAssetName(options.version);
32
+ const asset = selectAsset(options.release, expectedAsset);
33
+ const checksumAsset = selectAsset(options.release, "SHA256SUMS");
34
+ const paths = uiPathsFor(options.cacheRoot, options.version);
35
+ const archivePath = path.join(paths.uiVersionDir, expectedAsset);
36
+ const checksumPath = path.join(paths.uiVersionDir, "SHA256SUMS");
37
+
38
+ await fs.rm(paths.uiVersionDir, { recursive: true, force: true });
39
+ await fs.mkdir(paths.uiVersionDir, { recursive: true });
40
+ await (options.downloadFileImpl || downloadFile)(asset.browser_download_url, archivePath, { fetchImpl: options.fetchImpl });
41
+ await (options.downloadFileImpl || downloadFile)(checksumAsset.browser_download_url, checksumPath, { fetchImpl: options.fetchImpl });
42
+ await verifyChecksum(archivePath, await fs.readFile(checksumPath, "utf8"), expectedAsset);
43
+ await (options.extractArchiveImpl || extractArchive)(archivePath, paths.uiVersionDir, { platform: process.platform });
44
+
45
+ const root = await findIndexRoot(paths.uiVersionDir);
46
+ if (!root) {
47
+ throw new Error(`UI artifact is missing index.html: ${expectedAsset}`);
48
+ }
49
+ if (root !== paths.uiVersionDir) {
50
+ const stagingDir = `${paths.uiVersionDir}.staging`;
51
+ await fs.rm(stagingDir, { recursive: true, force: true });
52
+ await fs.rename(root, stagingDir);
53
+ await fs.rm(paths.uiVersionDir, { recursive: true, force: true });
54
+ await fs.rename(stagingDir, paths.uiVersionDir);
55
+ }
56
+
57
+ return {
58
+ uiVersion: options.version,
59
+ uiAssetName: expectedAsset,
60
+ uiPath: paths.uiVersionDir,
61
+ };
62
+ }
63
+
64
+ module.exports = {
65
+ installUiArtifact,
66
+ uiAssetName,
67
+ };
@@ -0,0 +1,17 @@
1
+ const { runEngine } = require("./runner");
2
+ const { topLevelCommandIndex } = require("./command-index");
3
+
4
+ async function runUi(args, options = {}) {
5
+ const uiIndex = topLevelCommandIndex(args);
6
+ if (args[uiIndex] !== "ui") throw new Error("Expected native raven ui command");
7
+
8
+ const installed = await options.installBundle({ env: options.env || process.env });
9
+ const uiArgs = args.slice(uiIndex + 1);
10
+ const hasUiPath = uiArgs.some((arg) => arg === "--ui-path" || arg.startsWith("--ui-path="));
11
+ const nativeArgs = hasUiPath
12
+ ? args
13
+ : [...args.slice(0, uiIndex + 1), "--ui-path", installed.uiPath, ...uiArgs];
14
+ return (options.runEngine || runEngine)(nativeArgs, { binaryPath: installed.binaryPath });
15
+ }
16
+
17
+ module.exports = { runUi };
package/src/version.js ADDED
@@ -0,0 +1,19 @@
1
+ function normalizeVersion(version) {
2
+ return String(version).trim().replace(/^v/, "");
3
+ }
4
+
5
+ function compareVersions(left, right) {
6
+ const a = normalizeVersion(left).split(".").map((part) => Number.parseInt(part, 10));
7
+ const b = normalizeVersion(right).split(".").map((part) => Number.parseInt(part, 10));
8
+ for (let index = 0; index < 3; index += 1) {
9
+ const delta = (a[index] || 0) - (b[index] || 0);
10
+ if (delta > 0) return 1;
11
+ if (delta < 0) return -1;
12
+ }
13
+ return 0;
14
+ }
15
+
16
+ module.exports = {
17
+ compareVersions,
18
+ normalizeVersion,
19
+ };