forklaunch 0.2.2 → 0.2.3
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/package.json +1 -1
- package/scripts/install.js +350 -101
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -1,153 +1,402 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { execSync } = require(
|
|
4
|
-
const fs = require(
|
|
5
|
-
const path = require(
|
|
6
|
-
const https = require(
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const { pipeline } = require("stream");
|
|
7
9
|
|
|
8
|
-
const GITHUB_REPO =
|
|
9
|
-
const VERSION = require(
|
|
10
|
+
const GITHUB_REPO = "forklaunch/forklaunch-js";
|
|
11
|
+
const VERSION = require("../package.json").version;
|
|
10
12
|
|
|
11
13
|
function getPlatform() {
|
|
12
14
|
const type = process.platform;
|
|
13
15
|
const arch = process.arch;
|
|
14
16
|
|
|
15
|
-
if (type ===
|
|
16
|
-
return arch ===
|
|
17
|
+
if (type === "darwin") {
|
|
18
|
+
return arch === "arm64" ? "darwin-aarch64" : "darwin-x86_64";
|
|
17
19
|
}
|
|
18
|
-
if (type ===
|
|
19
|
-
return arch ===
|
|
20
|
+
if (type === "linux") {
|
|
21
|
+
return arch === "arm64" ? "linux-aarch64" : "linux-x86_64";
|
|
20
22
|
}
|
|
21
|
-
if (type ===
|
|
22
|
-
return
|
|
23
|
+
if (type === "win32") {
|
|
24
|
+
return "windows-x86_64";
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
throw new Error(`Unsupported platform: ${type} ${arch}`);
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
function semverIsNewer(newVersion, oldVersion) {
|
|
31
|
+
const newParts = newVersion.split(".").map(Number);
|
|
32
|
+
const oldParts = oldVersion.split(".").map(Number);
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < Math.max(newParts.length, oldParts.length); i++) {
|
|
35
|
+
const newPart = newParts[i] || 0;
|
|
36
|
+
const oldPart = oldParts[i] || 0;
|
|
37
|
+
if (newPart > oldPart) return true;
|
|
38
|
+
if (newPart < oldPart) return false;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findExecutablePath(name) {
|
|
44
|
+
const command =
|
|
45
|
+
process.platform === "win32" ? `where ${name}.exe` : `which ${name}`;
|
|
46
|
+
try {
|
|
47
|
+
const result = execSync(command, { stdio: "pipe" }).toString().trim();
|
|
48
|
+
const firstPath = result.split(/\r?\n/)[0];
|
|
49
|
+
if (fs.existsSync(firstPath)) {
|
|
50
|
+
return firstPath;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getTargetBinDir() {
|
|
59
|
+
return path.join(os.homedir(), ".forklaunch", "bin");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updatePathVariable(binDir) {
|
|
63
|
+
const absoluteBinDir = path.resolve(binDir);
|
|
64
|
+
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
console.log("Adding install directory to your PATH...");
|
|
67
|
+
const powershellCommand = `
|
|
68
|
+
$currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User");
|
|
69
|
+
if (($currentUserPath -split ';') -notcontains "${absoluteBinDir}") {
|
|
70
|
+
$newUserPath = $currentUserPath + ";${absoluteBinDir}";
|
|
71
|
+
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User");
|
|
72
|
+
Write-Host "Installation directory added to your PATH.";
|
|
73
|
+
} else {
|
|
74
|
+
Write-Host "Installation directory is already in your PATH.";
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
try {
|
|
78
|
+
execSync(
|
|
79
|
+
`powershell -Command "${powershellCommand.replace(/"/g, '\\"')}"`,
|
|
80
|
+
{ stdio: "inherit" }
|
|
81
|
+
);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error(
|
|
84
|
+
"Failed to update PATH with PowerShell. Please add it manually."
|
|
85
|
+
);
|
|
86
|
+
console.warn(
|
|
87
|
+
`You need to add the following directory to your User PATH: ${absoluteBinDir}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
const shell = process.env.SHELL || "";
|
|
92
|
+
let shellConfigFile = null;
|
|
93
|
+
const homeDir = os.homedir();
|
|
94
|
+
const commandToAdd = `\n# ForkLaunch Path\nexport PATH="${absoluteBinDir}:$PATH"\n`;
|
|
95
|
+
|
|
96
|
+
if (shell.includes("zsh")) {
|
|
97
|
+
shellConfigFile = path.join(homeDir, ".zshrc");
|
|
98
|
+
} else if (shell.includes("bash")) {
|
|
99
|
+
shellConfigFile = path.join(homeDir, ".bash_profile");
|
|
100
|
+
if (!fs.existsSync(shellConfigFile)) {
|
|
101
|
+
shellConfigFile = path.join(homeDir, ".bashrc");
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
console.warn(
|
|
105
|
+
`Unsupported shell: ${shell}. Please add the following to your shell config file:`
|
|
106
|
+
);
|
|
107
|
+
console.warn(commandToAdd);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(shellConfigFile)) {
|
|
112
|
+
fs.writeFileSync(shellConfigFile, "");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fileContent = fs.readFileSync(shellConfigFile, "utf8");
|
|
116
|
+
if (fileContent.includes(absoluteBinDir)) {
|
|
117
|
+
console.log("Installation directory is already in your PATH.");
|
|
118
|
+
} else {
|
|
119
|
+
console.log(
|
|
120
|
+
`Updating ${path.basename(shellConfigFile)} to include forklaunch...`
|
|
121
|
+
);
|
|
122
|
+
fs.appendFileSync(shellConfigFile, commandToAdd);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
28
127
|
function createAlias(source, target) {
|
|
29
|
-
// Remove existing alias if it exists
|
|
30
128
|
if (fs.existsSync(target)) {
|
|
31
129
|
fs.unlinkSync(target);
|
|
32
130
|
}
|
|
33
|
-
|
|
34
|
-
if (process.platform ===
|
|
35
|
-
// Create a copy of the binary on Windows
|
|
131
|
+
|
|
132
|
+
if (process.platform === "win32") {
|
|
36
133
|
fs.copyFileSync(source, target);
|
|
37
134
|
} else {
|
|
38
|
-
|
|
39
|
-
const relativePath = path.relative(path.dirname(target), source);
|
|
40
|
-
fs.symlinkSync(relativePath, target);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Make alias executable on Unix systems
|
|
44
|
-
if (process.platform !== 'win32') {
|
|
45
|
-
fs.chmodSync(target, 0o755);
|
|
135
|
+
fs.symlinkSync(source, target);
|
|
46
136
|
}
|
|
47
137
|
}
|
|
48
138
|
|
|
49
139
|
function downloadBinary() {
|
|
50
140
|
const platform = getPlatform();
|
|
51
|
-
const binaryName =
|
|
52
|
-
|
|
53
|
-
const
|
|
141
|
+
const binaryName =
|
|
142
|
+
process.platform === "win32" ? "forklaunch.exe" : "forklaunch";
|
|
143
|
+
const aliasName = process.platform === "win32" ? "fl.exe" : "fl";
|
|
144
|
+
const artifactName =
|
|
145
|
+
process.platform === "win32"
|
|
146
|
+
? `forklaunch-${platform}.exe`
|
|
147
|
+
: `forklaunch-${platform}`;
|
|
54
148
|
const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/cli-v${VERSION}/${artifactName}`;
|
|
55
|
-
|
|
56
|
-
|
|
149
|
+
|
|
150
|
+
let binDir;
|
|
151
|
+
try {
|
|
152
|
+
binDir = getTargetBinDir();
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return Promise.reject(e);
|
|
155
|
+
}
|
|
156
|
+
|
|
57
157
|
const binaryPath = path.join(binDir, binaryName);
|
|
58
158
|
const aliasPath = path.join(binDir, aliasName);
|
|
59
159
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
160
|
+
if (fs.existsSync(binaryPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const installedVersionOutput = execSync(`"${binaryPath}" --version`, {
|
|
163
|
+
timeout: 5000,
|
|
164
|
+
})
|
|
165
|
+
.toString()
|
|
166
|
+
.trim();
|
|
167
|
+
const versionMatch = installedVersionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
168
|
+
|
|
169
|
+
if (versionMatch && versionMatch[1]) {
|
|
170
|
+
const installedVersion = versionMatch[1];
|
|
171
|
+
if (!semverIsNewer(VERSION, installedVersion)) {
|
|
172
|
+
console.log(
|
|
173
|
+
`forklaunch v${installedVersion} is already installed and up-to-date.`
|
|
174
|
+
);
|
|
175
|
+
if (!fs.existsSync(aliasPath)) {
|
|
176
|
+
createAlias(binaryPath, aliasPath);
|
|
177
|
+
}
|
|
178
|
+
return Promise.resolve({ installed: false, binDir: binDir });
|
|
179
|
+
}
|
|
180
|
+
console.log(
|
|
181
|
+
`Updating forklaunch from v${installedVersion} to v${VERSION}...`
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(
|
|
185
|
+
"Could not determine version of existing binary. Re-installing..."
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.warn(
|
|
190
|
+
"Could not execute existing binary to check version. Re-installing..."
|
|
191
|
+
);
|
|
192
|
+
console.warn(`Error details: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
console.log("Removing old forklaunch binary...");
|
|
197
|
+
fs.unlinkSync(binaryPath);
|
|
198
|
+
if (fs.existsSync(aliasPath)) {
|
|
199
|
+
fs.unlinkSync(aliasPath);
|
|
200
|
+
}
|
|
201
|
+
} catch (unlinkError) {
|
|
202
|
+
console.error(`Failed to remove existing binary: ${unlinkError.message}`);
|
|
203
|
+
console.error("Please check file permissions and try again.");
|
|
204
|
+
return Promise.reject(unlinkError);
|
|
205
|
+
}
|
|
63
206
|
}
|
|
64
207
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
208
|
+
try {
|
|
209
|
+
if (!fs.existsSync(binDir)) {
|
|
210
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err.code === "EACCES") {
|
|
214
|
+
console.error(`Permission denied to create directory: ${binDir}`);
|
|
215
|
+
console.error(
|
|
216
|
+
'Please try running the installation with administrator privileges (e.g., using "sudo").'
|
|
217
|
+
);
|
|
218
|
+
return Promise.reject(err);
|
|
71
219
|
}
|
|
72
|
-
return;
|
|
220
|
+
return Promise.reject(err);
|
|
73
221
|
}
|
|
74
222
|
|
|
75
|
-
console.log(`Downloading forklaunch for ${platform}...`);
|
|
223
|
+
console.log(`Downloading forklaunch v${VERSION} for ${platform}...`);
|
|
224
|
+
console.log(`Installing to: ${binDir}`);
|
|
76
225
|
console.log(`URL: ${downloadUrl}`);
|
|
77
226
|
|
|
78
227
|
return new Promise((resolve, reject) => {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
228
|
+
const makeRequest = (url) => {
|
|
229
|
+
const request = https
|
|
230
|
+
.get(url, { agent: false }, (response) => {
|
|
231
|
+
if (
|
|
232
|
+
response.statusCode >= 300 &&
|
|
233
|
+
response.statusCode < 400 &&
|
|
234
|
+
response.headers.location
|
|
235
|
+
) {
|
|
236
|
+
response.resume();
|
|
237
|
+
makeRequest(response.headers.location);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (response.statusCode !== 200) {
|
|
242
|
+
reject(
|
|
243
|
+
new Error(
|
|
244
|
+
`Failed to download: ${response.statusCode} - ${response.statusMessage}`
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const totalSize = parseInt(response.headers["content-length"], 10);
|
|
251
|
+
const showProgressBar = !isNaN(totalSize);
|
|
252
|
+
let downloadedSize = 0;
|
|
253
|
+
const progressBarWidth = 40;
|
|
254
|
+
|
|
255
|
+
response.on("data", (chunk) => {
|
|
256
|
+
downloadedSize += chunk.length;
|
|
257
|
+
if (showProgressBar) {
|
|
258
|
+
const percentage = Math.floor((downloadedSize / totalSize) * 100);
|
|
259
|
+
const completedWidth = Math.round(
|
|
260
|
+
(progressBarWidth * downloadedSize) / totalSize
|
|
261
|
+
);
|
|
262
|
+
const remainingWidth = progressBarWidth - completedWidth;
|
|
263
|
+
const bar = `[${"=".repeat(completedWidth)}${" ".repeat(remainingWidth)}]`;
|
|
264
|
+
process.stdout.write(`\r${bar} ${percentage}%`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
pipeline(response, fs.createWriteStream(binaryPath), (err) => {
|
|
269
|
+
if (showProgressBar) {
|
|
270
|
+
process.stdout.write("\n");
|
|
271
|
+
}
|
|
272
|
+
if (err) {
|
|
273
|
+
if (err.code === "EACCES") {
|
|
274
|
+
console.error(`Permission denied to write to ${binaryPath}`);
|
|
275
|
+
console.error(
|
|
276
|
+
"Please try running the installation with administrator privileges."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
fs.unlink(binaryPath, () => {});
|
|
280
|
+
return reject(err);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
91
284
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
285
|
+
createAlias(binaryPath, aliasPath);
|
|
286
|
+
|
|
287
|
+
resolve({ installed: true, binDir: binDir });
|
|
288
|
+
});
|
|
289
|
+
})
|
|
290
|
+
.on("error", (err) => {
|
|
291
|
+
fs.unlink(binaryPath, () => {});
|
|
292
|
+
reject(err);
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
makeRequest(downloadUrl);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function waitForAsyncOperations() {
|
|
300
|
+
return new Promise((resolve) => {
|
|
301
|
+
const checkOperations = () => {
|
|
302
|
+
const pendingOperations = process._getActiveRequests
|
|
303
|
+
? process._getActiveRequests()
|
|
304
|
+
: [];
|
|
305
|
+
const pendingHandles = process._getActiveHandles
|
|
306
|
+
? process._getActiveHandles().filter((handle) => {
|
|
307
|
+
return (
|
|
308
|
+
handle.constructor.name !== "WriteStream" ||
|
|
309
|
+
(handle !== process.stdout && handle !== process.stderr)
|
|
310
|
+
);
|
|
311
|
+
})
|
|
312
|
+
: [];
|
|
313
|
+
|
|
314
|
+
if (pendingOperations.length === 0 && pendingHandles.length === 0) {
|
|
104
315
|
resolve();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
316
|
+
} else {
|
|
317
|
+
setTimeout(checkOperations, 10);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
setTimeout(checkOperations, 10);
|
|
110
322
|
});
|
|
111
323
|
}
|
|
112
324
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
325
|
+
function flushOutputStreams() {
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
let flushed = 0;
|
|
328
|
+
const totalStreams = 2;
|
|
329
|
+
|
|
330
|
+
const checkComplete = () => {
|
|
331
|
+
flushed++;
|
|
332
|
+
if (flushed >= totalStreams) {
|
|
333
|
+
resolve();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (process.stdout.write("")) {
|
|
338
|
+
checkComplete();
|
|
339
|
+
} else {
|
|
340
|
+
process.stdout.once("drain", checkComplete);
|
|
128
341
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.log('Built and installed forklaunch locally');
|
|
137
|
-
console.log('Available commands: forklaunch, fl');
|
|
138
|
-
} catch (error) {
|
|
139
|
-
console.error('Failed to build locally:', error.message);
|
|
140
|
-
console.error('Please ensure Rust is installed and try running: cargo build --release');
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
342
|
+
|
|
343
|
+
if (process.stderr.write("")) {
|
|
344
|
+
checkComplete();
|
|
345
|
+
} else {
|
|
346
|
+
process.stderr.once("drain", checkComplete);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
143
349
|
}
|
|
144
350
|
|
|
145
351
|
async function main() {
|
|
146
352
|
try {
|
|
147
|
-
await downloadBinary();
|
|
353
|
+
const { installed, binDir } = await downloadBinary();
|
|
354
|
+
|
|
355
|
+
if (binDir) {
|
|
356
|
+
updatePathVariable(binDir);
|
|
357
|
+
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (installed) {
|
|
361
|
+
console.log("forklaunch installed successfully");
|
|
362
|
+
|
|
363
|
+
if (process.platform !== "win32") {
|
|
364
|
+
const shell = process.env.SHELL || "";
|
|
365
|
+
let profile = "";
|
|
366
|
+
if (shell.includes("zsh")) {
|
|
367
|
+
profile = "~/.zshrc";
|
|
368
|
+
} else if (shell.includes("bash")) {
|
|
369
|
+
profile = "~/.bash_profile or ~/.bashrc";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (profile) {
|
|
373
|
+
console.log(
|
|
374
|
+
`\nTo make the 'forklaunch' command available, please run:`
|
|
375
|
+
);
|
|
376
|
+
console.log(` source ${profile}`);
|
|
377
|
+
console.log(`\nAlternatively, open a new terminal window.`);
|
|
378
|
+
} else {
|
|
379
|
+
console.log(
|
|
380
|
+
"\nPlease restart your terminal for the changes to take effect."
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
console.log(
|
|
385
|
+
"\nPlease open a new terminal for the changes to take effect."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await flushOutputStreams();
|
|
391
|
+
await waitForAsyncOperations();
|
|
148
392
|
} catch (error) {
|
|
149
|
-
console.
|
|
150
|
-
|
|
393
|
+
console.error("\nDownload failed:", error.message);
|
|
394
|
+
console.error(
|
|
395
|
+
"Could not download forklaunch binary. Please check your network connection or if a binary for your platform is available."
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
await flushOutputStreams();
|
|
399
|
+
process.exit(1);
|
|
151
400
|
}
|
|
152
401
|
}
|
|
153
402
|
|