@instafy/cli 0.1.0-staging.138

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.
@@ -0,0 +1,212 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ const DEFAULT_VERSION = process.env.RATHOLE_VERSION ?? "latest";
8
+ const DEFAULT_CACHE_DIR = process.env.RATHOLE_CACHE_DIR ?? path.join(os.homedir(), ".instafy", "rathole");
9
+ export async function ensureRatholeBinary(options = {}) {
10
+ const logger = options.logger ?? (() => { });
11
+ const versionInput = (options.version ?? DEFAULT_VERSION).trim() || "latest";
12
+ const cacheRoot = path.resolve(options.cacheDir ?? DEFAULT_CACHE_DIR);
13
+ const plan = resolveInstallPlan();
14
+ if (plan.method === "cargo") {
15
+ return ensureRatholeViaCargo({
16
+ version: versionInput,
17
+ cacheRoot,
18
+ binaryName: plan.binaryName,
19
+ logger,
20
+ });
21
+ }
22
+ const tag = normalizeGithubTag(versionInput);
23
+ const targetDir = path.join(cacheRoot, tag, plan.assetName.replace(/\.zip$/i, ""));
24
+ const binaryPath = path.join(targetDir, plan.binaryName);
25
+ try {
26
+ await fs.promises.access(binaryPath, fs.constants.X_OK);
27
+ return binaryPath;
28
+ }
29
+ catch {
30
+ // continue and attempt download
31
+ }
32
+ await fs.promises.mkdir(targetDir, { recursive: true });
33
+ const downloadUrl = buildDownloadUrl(plan.assetName, tag);
34
+ logger(`Downloading rathole (${plan.assetName}) from ${downloadUrl}`);
35
+ const tempFile = path.join(targetDir, `${plan.assetName}.${Date.now()}.download`);
36
+ try {
37
+ await downloadFile(downloadUrl, tempFile, options.fetchImpl);
38
+ await extractZip(tempFile, targetDir);
39
+ }
40
+ finally {
41
+ await safeUnlink(tempFile);
42
+ }
43
+ if (!(await fileExists(binaryPath))) {
44
+ throw new Error(`rathole binary missing after extraction (${binaryPath})`);
45
+ }
46
+ if (process.platform !== "win32") {
47
+ await fs.promises.chmod(binaryPath, 0o755);
48
+ }
49
+ logger(`rathole ready at ${binaryPath}`);
50
+ return binaryPath;
51
+ }
52
+ function resolveInstallPlan() {
53
+ const arch = process.arch;
54
+ const platform = process.platform;
55
+ if (platform === "darwin" && arch === "arm64") {
56
+ return { method: "cargo", binaryName: "rathole" };
57
+ }
58
+ if (platform === "darwin") {
59
+ if (arch !== "x64") {
60
+ throw new Error(`Unsupported platform ${platform}/${arch} for prebuilt rathole; set RATHOLE_BIN or install via cargo.`);
61
+ }
62
+ return {
63
+ method: "download",
64
+ assetName: "rathole-x86_64-apple-darwin.zip",
65
+ binaryName: "rathole",
66
+ };
67
+ }
68
+ if (platform === "linux") {
69
+ if (arch === "arm64") {
70
+ return {
71
+ method: "download",
72
+ assetName: "rathole-aarch64-unknown-linux-musl.zip",
73
+ binaryName: "rathole",
74
+ };
75
+ }
76
+ if (arch === "x64") {
77
+ return {
78
+ method: "download",
79
+ assetName: "rathole-x86_64-unknown-linux-gnu.zip",
80
+ binaryName: "rathole",
81
+ };
82
+ }
83
+ throw new Error(`Unsupported platform ${platform}/${arch}. Set RATHOLE_BIN to a valid executable.`);
84
+ }
85
+ if (platform === "win32") {
86
+ if (arch !== "x64") {
87
+ throw new Error(`Unsupported platform ${platform}/${arch}. Set RATHOLE_BIN to a valid executable.`);
88
+ }
89
+ return {
90
+ method: "download",
91
+ assetName: "rathole-x86_64-pc-windows-msvc.zip",
92
+ binaryName: "rathole.exe",
93
+ };
94
+ }
95
+ throw new Error(`Unsupported platform ${platform}/${arch}. Set RATHOLE_BIN to a valid executable.`);
96
+ }
97
+ function normalizeGithubTag(raw) {
98
+ const trimmed = raw.trim();
99
+ if (!trimmed || trimmed.toLowerCase() === "latest") {
100
+ return "latest";
101
+ }
102
+ return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
103
+ }
104
+ function buildDownloadUrl(assetName, tag) {
105
+ const base = tag === "latest"
106
+ ? "https://github.com/rapiz1/rathole/releases/latest/download"
107
+ : `https://github.com/rapiz1/rathole/releases/download/${tag}`;
108
+ return `${base}/${assetName}`;
109
+ }
110
+ async function downloadFile(url, destination, fetchImpl) {
111
+ const fetcher = fetchImpl ?? fetch;
112
+ const response = await fetcher(url);
113
+ if (!response.ok || !response.body) {
114
+ const text = await response.text().catch(() => "");
115
+ throw new Error(`failed to download rathole (${response.status}): ${text}`);
116
+ }
117
+ const nodeStream = typeof Readable.fromWeb === "function"
118
+ ? Readable.fromWeb(response.body)
119
+ : response.body;
120
+ await pipeline(nodeStream, fs.createWriteStream(destination));
121
+ }
122
+ async function extractZip(archivePath, targetDir) {
123
+ const cmd = process.platform === "win32" ? "powershell.exe" : "unzip";
124
+ const args = process.platform === "win32"
125
+ ? [
126
+ "-NoProfile",
127
+ "-NonInteractive",
128
+ "-Command",
129
+ `Expand-Archive -LiteralPath '${escapePowershellString(archivePath)}' -DestinationPath '${escapePowershellString(targetDir)}' -Force`,
130
+ ]
131
+ : ["-o", archivePath, "-d", targetDir];
132
+ await new Promise((resolve, reject) => {
133
+ const child = spawn(cmd, args);
134
+ child.on("error", reject);
135
+ child.on("close", (code) => {
136
+ if (code === 0) {
137
+ resolve();
138
+ }
139
+ else {
140
+ reject(new Error(`${cmd} exited with code ${code ?? -1}`));
141
+ }
142
+ });
143
+ });
144
+ }
145
+ function escapePowershellString(value) {
146
+ return value.replace(/'/g, "''");
147
+ }
148
+ async function ensureRatholeViaCargo(params) {
149
+ const tag = normalizeGithubTag(params.version);
150
+ const installRoot = path.join(params.cacheRoot, tag, "cargo");
151
+ const binPath = path.join(installRoot, "bin", params.binaryName);
152
+ try {
153
+ await fs.promises.access(binPath, fs.constants.X_OK);
154
+ return binPath;
155
+ }
156
+ catch {
157
+ // continue
158
+ }
159
+ await fs.promises.mkdir(installRoot, { recursive: true });
160
+ const crateVersion = tag === "latest" ? null : tag.replace(/^v/, "").trim() || null;
161
+ params.logger(crateVersion
162
+ ? `Installing rathole ${crateVersion} via cargo (arm64 macOS fallback)...`
163
+ : "Installing rathole via cargo (arm64 macOS fallback)...");
164
+ const argsLocked = ["install", "rathole", "--locked", "--root", installRoot];
165
+ const argsUnlocked = ["install", "rathole", "--root", installRoot];
166
+ if (crateVersion) {
167
+ argsLocked.push("--version", crateVersion);
168
+ argsUnlocked.push("--version", crateVersion);
169
+ }
170
+ const runCargoInstall = async (args) => await new Promise((resolve, reject) => {
171
+ const child = spawn("cargo", args, { stdio: "inherit" });
172
+ child.on("error", (error) => {
173
+ reject(new Error(`failed to run cargo install for rathole (${String(error)}). Install Rust/cargo or set RATHOLE_BIN.`));
174
+ });
175
+ child.on("close", (code) => {
176
+ if (code === 0) {
177
+ resolve();
178
+ }
179
+ else {
180
+ reject(new Error(`cargo install exited with code ${code ?? -1}`));
181
+ }
182
+ });
183
+ });
184
+ try {
185
+ await runCargoInstall(argsLocked);
186
+ }
187
+ catch (error) {
188
+ params.logger(`cargo install --locked failed (${error instanceof Error ? error.message : String(error)}); retrying without --locked...`);
189
+ await runCargoInstall(argsUnlocked);
190
+ }
191
+ if (!(await fileExists(binPath))) {
192
+ throw new Error(`rathole binary missing after cargo install (${binPath})`);
193
+ }
194
+ return binPath;
195
+ }
196
+ async function fileExists(candidate) {
197
+ try {
198
+ await fs.promises.access(candidate, fs.constants.X_OK);
199
+ return true;
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ }
205
+ async function safeUnlink(filePath) {
206
+ try {
207
+ await fs.promises.unlink(filePath);
208
+ }
209
+ catch {
210
+ // ignore
211
+ }
212
+ }