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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/install.js +350 -101
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forklaunch",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Launch faster with forklaunch",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,153 +1,402 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require('child_process');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const https = require('https');
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 = 'forklaunch/forklaunch-js';
9
- const VERSION = require('../package.json').version;
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 === 'darwin') {
16
- return arch === 'arm64' ? 'darwin-aarch64' : 'darwin-x86_64';
17
+ if (type === "darwin") {
18
+ return arch === "arm64" ? "darwin-aarch64" : "darwin-x86_64";
17
19
  }
18
- if (type === 'linux') {
19
- return arch === 'arm64' ? 'linux-aarch64' : 'linux-x86_64';
20
+ if (type === "linux") {
21
+ return arch === "arm64" ? "linux-aarch64" : "linux-x86_64";
20
22
  }
21
- if (type === 'win32') {
22
- return 'windows-x86_64';
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 === 'win32') {
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
- // Create a symbolic link on Unix-based systems using relative path
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 = process.platform === 'win32' ? 'forklaunch.exe' : 'forklaunch';
52
- const aliasName = process.platform === 'win32' ? 'fl.exe' : 'fl';
53
- const artifactName = process.platform === 'win32' ? `forklaunch-${platform}.exe` : `forklaunch-${platform}`;
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
- const binDir = path.join(__dirname, '..', 'bin');
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
- // Create bin directory if it doesn't exist
61
- if (!fs.existsSync(binDir)) {
62
- fs.mkdirSync(binDir, { recursive: true });
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
- // Check if binary already exists
66
- if (fs.existsSync(binaryPath)) {
67
- console.log('forklaunch binary already exists');
68
- // Create alias if it doesn't exist
69
- if (!fs.existsSync(aliasPath)) {
70
- createAlias(binaryPath, aliasPath);
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 file = fs.createWriteStream(binaryPath);
80
-
81
- https.get(downloadUrl, (response) => {
82
- if (response.statusCode === 404) {
83
- reject(new Error(`Binary not found for platform ${platform}. Please build locally or check if releases are available.`));
84
- return;
85
- }
86
-
87
- if (response.statusCode !== 200) {
88
- reject(new Error(`Failed to download: ${response.statusCode}`));
89
- return;
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
- response.pipe(file);
93
-
94
- file.on('finish', () => {
95
- file.close();
96
- // Make binary executable
97
- fs.chmodSync(binaryPath, 0o755);
98
-
99
- // Create alias
100
- createAlias(binaryPath, aliasPath);
101
-
102
- console.log('forklaunch installed successfully');
103
- console.log('Available commands: forklaunch, fl');
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
- }).on('error', (err) => {
107
- fs.unlink(binaryPath, () => {});
108
- reject(err);
109
- });
316
+ } else {
317
+ setTimeout(checkOperations, 10);
318
+ }
319
+ };
320
+
321
+ setTimeout(checkOperations, 10);
110
322
  });
111
323
  }
112
324
 
113
- // Fallback: try to build locally if download fails
114
- function buildLocally() {
115
- console.log('Attempting to build locally...');
116
- try {
117
- execSync('cargo build --release', { stdio: 'inherit' });
118
-
119
- const binDir = path.join(__dirname, '..', 'bin');
120
- const binaryName = process.platform === 'win32' ? 'forklaunch.exe' : 'forklaunch';
121
- const aliasName = process.platform === 'win32' ? 'fl.exe' : 'fl';
122
- const sourcePath = path.join(__dirname, '..', 'target', 'release', binaryName);
123
- const targetPath = path.join(binDir, binaryName);
124
- const aliasPath = path.join(binDir, aliasName);
125
-
126
- if (!fs.existsSync(binDir)) {
127
- fs.mkdirSync(binDir, { recursive: true });
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
- fs.copyFileSync(sourcePath, targetPath);
131
- fs.chmodSync(targetPath, 0o755);
132
-
133
- // Create alias
134
- createAlias(targetPath, aliasPath);
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.warn('Download failed:', error.message);
150
- buildLocally();
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