inferay 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/cli.js +9 -0
- package/package.json +10 -2
- package/src/cli.js +135 -0
- package/src/config.js +39 -0
- package/src/doctor.js +84 -0
- package/src/install.js +76 -0
- package/src/launch.js +39 -0
- package/src/platform.js +37 -0
- package/src/releases.js +91 -0
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
1
|
# inferay
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line installer and launcher for Inferay.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx inferay
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package is intentionally small. It does not contain the desktop app. It resolves the right release asset, opens the installer, launches an installed app, and runs setup checks.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
inferay # install or launch Inferay
|
|
15
|
+
inferay . # open the current folder
|
|
16
|
+
inferay install # download/open the latest release installer
|
|
17
|
+
inferay install --local ./build/stable-macos-arm64/inferay.app
|
|
18
|
+
inferay launch ~/code # open a workspace
|
|
19
|
+
inferay doctor # check user setup
|
|
20
|
+
inferay doctor --dev # check contributor setup
|
|
21
|
+
inferay update # show latest release asset
|
|
22
|
+
inferay channel nightly # switch release channel
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
Contributors should work from the source repo:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bun install
|
|
31
|
+
bun run dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Users should not need Bun or a source checkout.
|
package/cli.js
ADDED
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inferay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Command-line installer and launcher for Inferay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"exports": "./index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"inferay": "cli.js"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"index.js",
|
|
13
|
+
"cli.js",
|
|
14
|
+
"src",
|
|
10
15
|
"README.md"
|
|
11
16
|
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
12
20
|
"license": "MIT",
|
|
13
21
|
"publishConfig": {
|
|
14
22
|
"access": "public"
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { install } from "./install.js";
|
|
2
|
+
import { launchApp } from "./launch.js";
|
|
3
|
+
import { doctor } from "./doctor.js";
|
|
4
|
+
import { getChannel, setChannel } from "./config.js";
|
|
5
|
+
import { fetchRelease, findAsset } from "./releases.js";
|
|
6
|
+
import { platformInfo } from "./platform.js";
|
|
7
|
+
|
|
8
|
+
const VERSION = "0.1.1";
|
|
9
|
+
|
|
10
|
+
function printHelp() {
|
|
11
|
+
console.log(`inferay ${VERSION}
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
inferay Install or launch Inferay
|
|
15
|
+
inferay . Open the current folder in Inferay
|
|
16
|
+
inferay <path> Open a folder in Inferay
|
|
17
|
+
inferay install Install Inferay from the latest release
|
|
18
|
+
inferay install --local <app>
|
|
19
|
+
inferay launch [path] Launch Inferay with a workspace
|
|
20
|
+
inferay update Re-run the release installer
|
|
21
|
+
inferay doctor [--dev] Check local setup
|
|
22
|
+
inferay channel [name] Show or set release channel
|
|
23
|
+
inferay version Print CLI version
|
|
24
|
+
|
|
25
|
+
Channels:
|
|
26
|
+
stable, nightly, dev
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function optionValue(args, name) {
|
|
31
|
+
const index = args.indexOf(name);
|
|
32
|
+
if (index === -1) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return args[index + 1] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function printDoctor(args) {
|
|
39
|
+
const checks = await doctor({ dev: args.includes("--dev") });
|
|
40
|
+
for (const [label, value] of checks) {
|
|
41
|
+
console.log(`${label.padEnd(18)} ${value}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function update() {
|
|
46
|
+
const channel = await getChannel();
|
|
47
|
+
const release = await fetchRelease(channel);
|
|
48
|
+
const asset = findAsset(release, platformInfo());
|
|
49
|
+
if (!asset) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`no installable asset found for ${release.tag_name || channel}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
console.log(
|
|
55
|
+
`Latest ${channel}: ${release.tag_name || release.name || "unknown"}`
|
|
56
|
+
);
|
|
57
|
+
console.log(`Asset: ${asset.name}`);
|
|
58
|
+
console.log("Run `inferay install` to download and open the installer.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function installAndReport(args) {
|
|
62
|
+
const local = optionValue(args, "--local");
|
|
63
|
+
const result = await install({ local });
|
|
64
|
+
console.log(result.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function main(argv) {
|
|
68
|
+
const args = argv.slice(2);
|
|
69
|
+
const command = args[0];
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
!command ||
|
|
73
|
+
command === "." ||
|
|
74
|
+
(!command.startsWith("-") && !isKnownCommand(command))
|
|
75
|
+
) {
|
|
76
|
+
const target = command || process.cwd();
|
|
77
|
+
try {
|
|
78
|
+
await launchApp(target);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (command && command !== ".") {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const result = await install();
|
|
84
|
+
console.log(result.message);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (command) {
|
|
90
|
+
case "--help":
|
|
91
|
+
case "-h":
|
|
92
|
+
case "help":
|
|
93
|
+
printHelp();
|
|
94
|
+
return;
|
|
95
|
+
case "--version":
|
|
96
|
+
case "-v":
|
|
97
|
+
case "version":
|
|
98
|
+
console.log(VERSION);
|
|
99
|
+
return;
|
|
100
|
+
case "install":
|
|
101
|
+
await installAndReport(args);
|
|
102
|
+
return;
|
|
103
|
+
case "launch":
|
|
104
|
+
await launchApp(args[1] || process.cwd());
|
|
105
|
+
return;
|
|
106
|
+
case "doctor":
|
|
107
|
+
await printDoctor(args);
|
|
108
|
+
return;
|
|
109
|
+
case "update":
|
|
110
|
+
await update();
|
|
111
|
+
return;
|
|
112
|
+
case "channel":
|
|
113
|
+
if (!args[1]) {
|
|
114
|
+
console.log(await getChannel());
|
|
115
|
+
} else {
|
|
116
|
+
await setChannel(args[1]);
|
|
117
|
+
console.log(`Channel set to ${args[1]}`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`unknown command "${command}". Run \`inferay --help\`.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isKnownCommand(command) {
|
|
126
|
+
return new Set([
|
|
127
|
+
"help",
|
|
128
|
+
"install",
|
|
129
|
+
"launch",
|
|
130
|
+
"doctor",
|
|
131
|
+
"update",
|
|
132
|
+
"channel",
|
|
133
|
+
"version",
|
|
134
|
+
]).has(command);
|
|
135
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_DIR = join(homedir(), ".inferay");
|
|
6
|
+
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
export async function readConfig() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error?.code === "ENOENT") {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function writeConfig(config) {
|
|
20
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true });
|
|
21
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getChannel() {
|
|
25
|
+
const config = await readConfig();
|
|
26
|
+
return config.channel || "stable";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function setChannel(channel) {
|
|
30
|
+
const allowed = new Set(["stable", "nightly", "dev"]);
|
|
31
|
+
if (!allowed.has(channel)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`unknown channel "${channel}". Use stable, nightly, or dev.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const config = await readConfig();
|
|
37
|
+
config.channel = channel;
|
|
38
|
+
await writeConfig(config);
|
|
39
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { getChannel, CONFIG_PATH } from "./config.js";
|
|
6
|
+
import { findExistingApp, platformInfo } from "./platform.js";
|
|
7
|
+
import { releaseApiUrl, releaseRepo } from "./releases.js";
|
|
8
|
+
|
|
9
|
+
async function canAccess(path) {
|
|
10
|
+
try {
|
|
11
|
+
await access(path);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function commandExists(command) {
|
|
19
|
+
const result = spawnSync("which", [command], { encoding: "utf8" });
|
|
20
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function appVersion(appPath) {
|
|
24
|
+
if (!appPath) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const plist = join(appPath, "Contents/Info.plist");
|
|
28
|
+
if (!existsSync(plist)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const result = spawnSync(
|
|
32
|
+
"/usr/libexec/PlistBuddy",
|
|
33
|
+
["-c", "Print :CFBundleShortVersionString", plist],
|
|
34
|
+
{
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function doctor({ dev = false } = {}) {
|
|
42
|
+
const platform = platformInfo();
|
|
43
|
+
const appPath = findExistingApp();
|
|
44
|
+
const version = await appVersion(appPath);
|
|
45
|
+
const channel = await getChannel();
|
|
46
|
+
const checks = [
|
|
47
|
+
[
|
|
48
|
+
"Platform",
|
|
49
|
+
`${platform.os}-${platform.cpu}${platform.supported ? "" : " (unsupported)"}`,
|
|
50
|
+
],
|
|
51
|
+
["Release repo", releaseRepo()],
|
|
52
|
+
["Release metadata", releaseApiUrl(channel)],
|
|
53
|
+
["Channel", channel],
|
|
54
|
+
[
|
|
55
|
+
"Config",
|
|
56
|
+
(await canAccess(CONFIG_PATH)) ? CONFIG_PATH : "not created yet",
|
|
57
|
+
],
|
|
58
|
+
["Installed app", appPath || "not found"],
|
|
59
|
+
["Installed version", version || "unknown"],
|
|
60
|
+
["Claude CLI", commandExists("claude") || "not found"],
|
|
61
|
+
["Codex CLI", commandExists("codex") || "not found"],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (dev) {
|
|
65
|
+
checks.push(["Bun", commandExists("bun") || "not found"]);
|
|
66
|
+
checks.push(["Git", commandExists("git") || "not found"]);
|
|
67
|
+
checks.push([
|
|
68
|
+
"package.json",
|
|
69
|
+
(await canAccess(join(process.cwd(), "package.json")))
|
|
70
|
+
? "found"
|
|
71
|
+
: "not found",
|
|
72
|
+
]);
|
|
73
|
+
try {
|
|
74
|
+
const packageJson = JSON.parse(
|
|
75
|
+
await readFile(join(process.cwd(), "package.json"), "utf8")
|
|
76
|
+
);
|
|
77
|
+
checks.push(["Project", packageJson.name || "unknown"]);
|
|
78
|
+
} catch {
|
|
79
|
+
checks.push(["Project", "unknown"]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return checks;
|
|
84
|
+
}
|
package/src/install.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { cp, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { getChannel } from "./config.js";
|
|
5
|
+
import {
|
|
6
|
+
defaultInstallPath,
|
|
7
|
+
findExistingApp,
|
|
8
|
+
platformInfo,
|
|
9
|
+
} from "./platform.js";
|
|
10
|
+
import { downloadAsset, fetchRelease, findAsset } from "./releases.js";
|
|
11
|
+
import { openFile } from "./launch.js";
|
|
12
|
+
|
|
13
|
+
async function copyAppBundle(source, destination = defaultInstallPath()) {
|
|
14
|
+
if (!source.endsWith(".app")) {
|
|
15
|
+
throw new Error("local install source must be a .app bundle");
|
|
16
|
+
}
|
|
17
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
18
|
+
await cp(source, destination, { recursive: true, force: true });
|
|
19
|
+
return destination;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function install({ local, launch = true } = {}) {
|
|
23
|
+
const platform = platformInfo();
|
|
24
|
+
if (!platform.supported) {
|
|
25
|
+
throw new Error(`unsupported platform ${platform.os}-${platform.cpu}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (local) {
|
|
29
|
+
const source = resolve(local);
|
|
30
|
+
if (!existsSync(source)) {
|
|
31
|
+
throw new Error(`local app not found: ${source}`);
|
|
32
|
+
}
|
|
33
|
+
const destination = await copyAppBundle(source);
|
|
34
|
+
return {
|
|
35
|
+
kind: "local-app",
|
|
36
|
+
message: `Installed ${basename(source)} to ${destination}`,
|
|
37
|
+
installedPath: destination,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const existing = findExistingApp();
|
|
42
|
+
if (existing) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "already-installed",
|
|
45
|
+
message: `Inferay is already available at ${existing}`,
|
|
46
|
+
installedPath: existing,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const channel = await getChannel();
|
|
51
|
+
const release = await fetchRelease(channel);
|
|
52
|
+
const asset = findAsset(release, platform);
|
|
53
|
+
if (!asset) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`no ${platform.target} release asset found for ${release.tag_name || channel}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const downloadedPath = await downloadAsset(asset);
|
|
60
|
+
if (downloadedPath.endsWith(".dmg")) {
|
|
61
|
+
if (launch) {
|
|
62
|
+
await openFile(downloadedPath);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
kind: "dmg",
|
|
66
|
+
message: `Downloaded ${asset.name}. Drag Inferay to Applications from the opened DMG.`,
|
|
67
|
+
downloadedPath,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
kind: "downloaded",
|
|
73
|
+
message: `Downloaded ${asset.name} to ${downloadedPath}`,
|
|
74
|
+
downloadedPath,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/launch.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
defaultInstallPath,
|
|
6
|
+
findExistingApp,
|
|
7
|
+
platformInfo,
|
|
8
|
+
} from "./platform.js";
|
|
9
|
+
|
|
10
|
+
function run(command, args) {
|
|
11
|
+
return new Promise((resolvePromise, reject) => {
|
|
12
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
13
|
+
child.on("error", reject);
|
|
14
|
+
child.on("exit", (code) => {
|
|
15
|
+
if (code === 0) {
|
|
16
|
+
resolvePromise();
|
|
17
|
+
} else {
|
|
18
|
+
reject(new Error(`${command} exited with code ${code}`));
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function launchApp(targetPath = process.cwd()) {
|
|
25
|
+
const platform = platformInfo();
|
|
26
|
+
if (platform.os !== "macos") {
|
|
27
|
+
throw new Error("launch is currently supported on macOS only");
|
|
28
|
+
}
|
|
29
|
+
const appPath = findExistingApp() || defaultInstallPath();
|
|
30
|
+
if (!existsSync(appPath)) {
|
|
31
|
+
throw new Error("Inferay is not installed. Run `inferay install` first.");
|
|
32
|
+
}
|
|
33
|
+
const cwd = resolve(targetPath);
|
|
34
|
+
await run("open", ["-a", appPath, "--args", "--cwd", cwd]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function openFile(filePath) {
|
|
38
|
+
await run("open", [filePath]);
|
|
39
|
+
}
|
package/src/platform.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { platform, arch, homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export function platformInfo() {
|
|
6
|
+
const os = platform();
|
|
7
|
+
const cpu = arch();
|
|
8
|
+
if (os !== "darwin") {
|
|
9
|
+
return { os, cpu, supported: false, target: `${os}-${cpu}` };
|
|
10
|
+
}
|
|
11
|
+
const mappedArch = cpu === "arm64" ? "arm64" : cpu === "x64" ? "x64" : cpu;
|
|
12
|
+
return {
|
|
13
|
+
os: "macos",
|
|
14
|
+
cpu: mappedArch,
|
|
15
|
+
supported: mappedArch === "arm64" || mappedArch === "x64",
|
|
16
|
+
target: `macos-${mappedArch}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defaultInstallPath() {
|
|
21
|
+
return "/Applications/inferay.app";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function devAppCandidates(cwd = process.cwd()) {
|
|
25
|
+
return [
|
|
26
|
+
resolve(cwd, "build/dev-macos-arm64/inferay-dev.app"),
|
|
27
|
+
resolve(cwd, "build/dev-macos-arm64/inferay.app"),
|
|
28
|
+
resolve(cwd, "build/stable-macos-arm64/inferay.app"),
|
|
29
|
+
resolve(cwd, "build/macos-arm64/inferay.app"),
|
|
30
|
+
join(homedir(), "Applications/inferay.app"),
|
|
31
|
+
defaultInstallPath(),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findExistingApp(cwd = process.cwd()) {
|
|
36
|
+
return devAppCandidates(cwd).find((candidate) => existsSync(candidate));
|
|
37
|
+
}
|
package/src/releases.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REPO = "raymondreaming/inferay";
|
|
9
|
+
|
|
10
|
+
export function releaseRepo() {
|
|
11
|
+
return process.env.INFERAY_RELEASE_REPO || DEFAULT_REPO;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function releaseApiUrl(channel = "stable") {
|
|
15
|
+
const repo = releaseRepo();
|
|
16
|
+
if (process.env.INFERAY_RELEASE_URL) {
|
|
17
|
+
return process.env.INFERAY_RELEASE_URL;
|
|
18
|
+
}
|
|
19
|
+
if (channel === "stable") {
|
|
20
|
+
return `https://api.github.com/repos/${repo}/releases/latest`;
|
|
21
|
+
}
|
|
22
|
+
return `https://api.github.com/repos/${repo}/releases/tags/${channel}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function fetchRelease(channel = "stable") {
|
|
26
|
+
const response = await fetch(releaseApiUrl(channel), {
|
|
27
|
+
headers: {
|
|
28
|
+
accept: "application/vnd.github+json",
|
|
29
|
+
"user-agent": "inferay-cli",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(`could not fetch release metadata (${response.status})`);
|
|
34
|
+
}
|
|
35
|
+
return response.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function findAsset(release, platform) {
|
|
39
|
+
const assets = Array.isArray(release.assets) ? release.assets : [];
|
|
40
|
+
const target = platform.target;
|
|
41
|
+
const preferred = [
|
|
42
|
+
(asset) => asset.name?.includes(target) && asset.name?.endsWith(".dmg"),
|
|
43
|
+
(asset) =>
|
|
44
|
+
asset.name?.includes(platform.os) &&
|
|
45
|
+
asset.name?.includes(platform.cpu) &&
|
|
46
|
+
asset.name?.endsWith(".dmg"),
|
|
47
|
+
(asset) => asset.name?.endsWith(".dmg"),
|
|
48
|
+
(asset) => asset.name?.includes(target) && asset.name?.endsWith(".tar.zst"),
|
|
49
|
+
];
|
|
50
|
+
return preferred.map((matcher) => assets.find(matcher)).find(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function findChecksumAsset(release) {
|
|
54
|
+
const assets = Array.isArray(release.assets) ? release.assets : [];
|
|
55
|
+
return assets.find((asset) => /checksums?\.txt$/i.test(asset.name || ""));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function downloadAsset(asset) {
|
|
59
|
+
if (!asset?.browser_download_url) {
|
|
60
|
+
throw new Error("release asset is missing a download URL");
|
|
61
|
+
}
|
|
62
|
+
const cacheDir = join(tmpdir(), "inferay-downloads");
|
|
63
|
+
await mkdir(cacheDir, { recursive: true });
|
|
64
|
+
const destination = join(cacheDir, asset.name);
|
|
65
|
+
const response = await fetch(asset.browser_download_url, {
|
|
66
|
+
headers: { "user-agent": "inferay-cli" },
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok || !response.body) {
|
|
69
|
+
throw new Error(`could not download ${asset.name} (${response.status})`);
|
|
70
|
+
}
|
|
71
|
+
await pipeline(response.body, createWriteStream(destination));
|
|
72
|
+
return destination;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function sha256(filePath) {
|
|
76
|
+
const hash = createHash("sha256");
|
|
77
|
+
const file = await readFile(filePath);
|
|
78
|
+
hash.update(file);
|
|
79
|
+
return hash.digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function verifyChecksum(filePath, expected) {
|
|
83
|
+
if (!expected) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const actual = await sha256(filePath);
|
|
87
|
+
if (actual !== expected) {
|
|
88
|
+
throw new Error(`checksum mismatch for ${filePath}`);
|
|
89
|
+
}
|
|
90
|
+
return actual;
|
|
91
|
+
}
|