arashi 1.11.2 → 1.12.0

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 CHANGED
@@ -47,13 +47,21 @@ If curl installation fails, or if the smoke test reports a bad release artifact,
47
47
  npm install -g arashi
48
48
  ```
49
49
 
50
+ The npm package is script-free: it does not require package-manager lifecycle scripts or `postinstall` approval. It installs the lightweight JavaScript entrypoint and wrapper files first, then downloads the matching platform binary on first use.
51
+
52
+ To preinstall the binary explicitly, run:
53
+
54
+ ```bash
55
+ arashi install
56
+ ```
57
+
50
58
  Verify install:
51
59
 
52
60
  ```bash
53
61
  arashi --version
54
62
  ```
55
63
 
56
- If npm is unavailable or fails, use the curl installer command above or the manual release instructions in [`docs/INSTALLATION.md`](./docs/INSTALLATION.md).
64
+ If npm is unavailable or binary installation fails, use the curl installer command above or the manual release instructions in [`docs/INSTALLATION.md`](./docs/INSTALLATION.md).
57
65
 
58
66
  ### Manual install from GitHub Releases
59
67
 
@@ -94,6 +102,7 @@ bun run build
94
102
  Arashi currently provides these commands:
95
103
 
96
104
  - `arashi init`
105
+ - `arashi install`
97
106
  - `arashi add <git-url>`
98
107
  - `arashi clone [--all]`
99
108
  - `arashi create <branch>`
package/bin/arashi.js CHANGED
@@ -1,110 +1,149 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, spawnSync } from "node:child_process";
2
+ import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { formatInstallError, getPlatformInfo, installBinary } from "./install-binary.js";
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = dirname(__filename);
9
10
 
10
- const argv = process.argv.slice(2);
11
- const isWindows = process.platform === "win32";
11
+ const currentPlatform = process.platform;
12
+ const currentArch = process.arch;
13
+ const isWindows = currentPlatform === "win32";
12
14
 
13
- const resolveBinaryPath = () => {
14
- const defaultBinary = isWindows ? "arashi.bin.exe" : "arashi.bin";
15
- const defaultBinaryPath = join(__dirname, defaultBinary);
15
+ export function getDefaultBinaryName(platform = currentPlatform) {
16
+ return platform === "win32" ? "arashi.bin.exe" : "arashi.bin";
17
+ }
16
18
 
17
- if (existsSync(defaultBinaryPath)) {
18
- return defaultBinaryPath;
19
- }
19
+ export function getWrapperName(platform = currentPlatform) {
20
+ return platform === "win32" ? "arashi.bat" : "arashi";
21
+ }
20
22
 
21
- if (isWindows) {
22
- return join(__dirname, "arashi-windows-x64.exe");
23
- }
23
+ export function resolvePlatformBinaryName({ arch = currentArch, platform = currentPlatform } = {}) {
24
+ return getPlatformInfo({ arch, platform }).binaryName;
25
+ }
24
26
 
25
- if (process.platform === "darwin" && process.arch === "arm64") {
26
- return join(__dirname, "arashi-macos-arm64");
27
- }
27
+ export function resolveBinaryPath(options = {}) {
28
+ const binDir = options.binDir ?? __dirname;
29
+ const platform = options.platform ?? currentPlatform;
30
+ const arch = options.arch ?? currentArch;
31
+ const exists = options.existsSyncImpl ?? existsSync;
32
+ const defaultBinaryPath = join(binDir, getDefaultBinaryName(platform));
28
33
 
29
- if (process.platform === "linux" && process.arch === "x64") {
30
- return join(__dirname, "arashi-linux-x64");
34
+ if (exists(defaultBinaryPath)) {
35
+ return defaultBinaryPath;
31
36
  }
32
37
 
33
- return defaultBinaryPath;
34
- };
35
-
36
- const ensureInstalled = () => {
37
- const wrapper = isWindows ? "arashi.bat" : "arashi";
38
- const wrapperPath = join(__dirname, wrapper);
39
- const postinstallPath = join(__dirname, "..", "scripts", "postinstall.js");
40
- const defaultBinary = isWindows ? "arashi.bin.exe" : "arashi.bin";
41
- const defaultBinaryPath = join(__dirname, defaultBinary);
42
- const platformBinary = (() => {
43
- if (isWindows) {
44
- return "arashi-windows-x64.exe";
45
- }
46
-
47
- if (process.platform === "darwin" && process.arch === "arm64") {
48
- return "arashi-macos-arm64";
49
- }
50
-
51
- if (process.platform === "linux" && process.arch === "x64") {
52
- return "arashi-linux-x64";
53
- }
38
+ try {
39
+ return join(binDir, resolvePlatformBinaryName({ arch, platform }));
40
+ } catch {
41
+ return defaultBinaryPath;
42
+ }
43
+ }
54
44
 
55
- return null;
56
- })();
57
- const platformBinaryPath = platformBinary ? join(__dirname, platformBinary) : null;
58
- const binaryExists = () => {
59
- if (existsSync(defaultBinaryPath)) {
60
- return true;
61
- }
45
+ export function hasRunnableBinary(options = {}) {
46
+ const binDir = options.binDir ?? __dirname;
47
+ const platform = options.platform ?? currentPlatform;
48
+ const arch = options.arch ?? currentArch;
49
+ const exists = options.existsSyncImpl ?? existsSync;
62
50
 
63
- if (platformBinaryPath && existsSync(platformBinaryPath)) {
64
- return true;
65
- }
51
+ if (exists(join(binDir, getDefaultBinaryName(platform)))) {
52
+ return true;
53
+ }
66
54
 
55
+ try {
56
+ return exists(join(binDir, resolvePlatformBinaryName({ arch, platform })));
57
+ } catch {
67
58
  return false;
68
- };
69
-
70
- if (existsSync(wrapperPath) && binaryExists()) {
71
- return;
72
59
  }
73
-
74
- console.log("arashi binary missing; running postinstall to download.");
75
- const result = spawnSync(process.execPath, [postinstallPath], { stdio: "inherit" });
76
-
77
- if (result.error) {
78
- console.error(`Failed to run postinstall. ${result.error.message}.`);
79
- process.exit(1);
60
+ }
61
+
62
+ export async function ensureInstalled(options = {}) {
63
+ const binDir = options.binDir ?? __dirname;
64
+ const rootDir = options.rootDir ?? join(binDir, "..");
65
+ const platform = options.platform ?? currentPlatform;
66
+ const arch = options.arch ?? currentArch;
67
+ const log = options.log ?? console.log;
68
+ const installBinaryImpl = options.installBinaryImpl ?? installBinary;
69
+
70
+ if (hasRunnableBinary({ arch, binDir, existsSyncImpl: options.existsSyncImpl, platform })) {
71
+ return { status: "already-installed" };
80
72
  }
81
73
 
82
- if (typeof result.status === "number" && result.status !== 0) {
83
- process.exit(result.status);
84
- }
85
- };
86
-
87
- ensureInstalled();
88
- const wrapper = isWindows ? "arashi.bat" : "arashi";
89
- const wrapperPath = join(__dirname, wrapper);
90
- const binaryPath = resolveBinaryPath();
91
-
92
- const child = isWindows
93
- ? spawn(binaryPath, argv, {
94
- stdio: "inherit",
95
- windowsHide: false,
96
- })
97
- : spawn(wrapperPath, argv, { stdio: "inherit" });
98
-
99
- child.on("exit", (code, signal) => {
100
- if (typeof code === "number") {
101
- process.exit(code);
74
+ log("arashi binary missing; installing the matching platform binary before continuing.");
75
+ return installBinaryImpl({ ...options, arch, binDir, log, platform, rootDir });
76
+ }
77
+
78
+ export function isExplicitInstallCommand(argv) {
79
+ return argv[0] === "install";
80
+ }
81
+
82
+ async function runExplicitInstall(options) {
83
+ const binDir = options.binDir ?? __dirname;
84
+ const rootDir = options.rootDir ?? join(binDir, "..");
85
+ const installBinaryImpl = options.installBinaryImpl ?? installBinary;
86
+
87
+ await installBinaryImpl({ ...options, binDir, rootDir });
88
+ }
89
+
90
+ function spawnArashi(argv, options = {}) {
91
+ const binDir = options.binDir ?? __dirname;
92
+ const platform = options.platform ?? currentPlatform;
93
+ const windows = platform === "win32";
94
+ const spawnImpl = options.spawnImpl ?? spawn;
95
+ const wrapperPath = join(binDir, getWrapperName(platform));
96
+ const binaryPath = resolveBinaryPath({
97
+ arch: options.arch,
98
+ binDir,
99
+ existsSyncImpl: options.existsSyncImpl,
100
+ platform,
101
+ });
102
+
103
+ return new Promise((resolve) => {
104
+ const child = windows
105
+ ? spawnImpl(binaryPath, argv, {
106
+ stdio: "inherit",
107
+ windowsHide: false,
108
+ })
109
+ : spawnImpl(wrapperPath, argv, { stdio: "inherit" });
110
+
111
+ child.on("exit", (code, signal) => {
112
+ if (typeof code === "number") {
113
+ resolve(code);
114
+ return;
115
+ }
116
+
117
+ resolve(signal ? 1 : 0);
118
+ });
119
+
120
+ child.on("error", (error) => {
121
+ const errorLog = options.error ?? console.error;
122
+ errorLog(`Failed to start arashi. ${error.message}.`);
123
+ resolve(1);
124
+ });
125
+ });
126
+ }
127
+
128
+ export async function runEntrypoint(argv = process.argv.slice(2), options = {}) {
129
+ const errorLog = options.error ?? console.error;
130
+
131
+ try {
132
+ if (isExplicitInstallCommand(argv)) {
133
+ await runExplicitInstall(options);
134
+ return 0;
135
+ }
136
+
137
+ await ensureInstalled(options);
138
+ } catch (error) {
139
+ errorLog(formatInstallError(error));
140
+ return 1;
102
141
  }
103
142
 
104
- process.exit(signal ? 1 : 0);
105
- });
143
+ return spawnArashi(argv, options);
144
+ }
106
145
 
107
- child.on("error", (error) => {
108
- console.error(`Failed to start arashi. ${error.message}.`);
109
- process.exit(1);
110
- });
146
+ const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
147
+ if (import.meta.url === invokedPath) {
148
+ process.exitCode = await runEntrypoint();
149
+ }
@@ -0,0 +1,211 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { chmodSync, createWriteStream } from "node:fs";
3
+ import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
4
+ import { get } from "node:https";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export const PACKAGE_NAME = "arashi";
12
+ export const GITHUB_REPO = "corwinm/arashi";
13
+ export const MANUAL_INSTALL_URL = `https://github.com/${GITHUB_REPO}/releases`;
14
+
15
+ export class UnsupportedPlatformError extends Error {
16
+ constructor(platform = process.platform, arch = process.arch) {
17
+ super(
18
+ `Unsupported platform: ${platform}-${arch}. Please build from source or file an issue at https://github.com/${GITHUB_REPO}/issues`,
19
+ );
20
+ this.name = "UnsupportedPlatformError";
21
+ this.platform = platform;
22
+ this.arch = arch;
23
+ }
24
+ }
25
+
26
+ export function getPlatformInfo({ platform = process.platform, arch = process.arch } = {}) {
27
+ if (platform === "darwin" && arch === "arm64") {
28
+ return { arch, binaryName: `${PACKAGE_NAME}-macos-arm64`, isWindows: false, platform };
29
+ }
30
+
31
+ if (platform === "linux" && arch === "x64") {
32
+ return { arch, binaryName: `${PACKAGE_NAME}-linux-x64`, isWindows: false, platform };
33
+ }
34
+
35
+ if (platform === "win32" && arch === "x64") {
36
+ return { arch, binaryName: `${PACKAGE_NAME}-windows-x64.exe`, isWindows: true, platform };
37
+ }
38
+
39
+ throw new UnsupportedPlatformError(platform, arch);
40
+ }
41
+
42
+ export function buildReleaseAssetUrl(version, binaryName, repo = GITHUB_REPO) {
43
+ return `https://github.com/${repo}/releases/download/v${version}/${binaryName}`;
44
+ }
45
+
46
+ export async function readPackageVersion(packageJsonPath, { readFileImpl = readFile } = {}) {
47
+ const packageJson = JSON.parse(await readFileImpl(packageJsonPath, "utf8"));
48
+ return packageJson.version;
49
+ }
50
+
51
+ export async function isBinaryInstalled(binaryPath, { accessImpl = access } = {}) {
52
+ try {
53
+ await accessImpl(binaryPath, constants.F_OK);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export function downloadFile(url, dest, options = {}) {
61
+ const {
62
+ createWriteStreamImpl = createWriteStream,
63
+ getImpl = get,
64
+ log = console.log,
65
+ maxRedirects = 5,
66
+ } = options;
67
+
68
+ return new Promise((resolve, reject) => {
69
+ if (maxRedirects < 0) {
70
+ reject(new Error("Too many redirects while downloading binary"));
71
+ return;
72
+ }
73
+
74
+ log(`Downloading ${url}...`);
75
+
76
+ const file = createWriteStreamImpl(dest);
77
+ const request = getImpl(url, (response) => {
78
+ if (
79
+ response.statusCode === 301 ||
80
+ response.statusCode === 302 ||
81
+ response.statusCode === 307 ||
82
+ response.statusCode === 308
83
+ ) {
84
+ file.close();
85
+ const location = response.headers.location;
86
+ if (!location) {
87
+ reject(new Error(`Redirect response from ${url} did not include a location header`));
88
+ return;
89
+ }
90
+
91
+ downloadFile(location, dest, { ...options, maxRedirects: maxRedirects - 1 })
92
+ .then(resolve)
93
+ .catch(reject);
94
+ return;
95
+ }
96
+
97
+ if (response.statusCode !== 200) {
98
+ file.close();
99
+ reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`));
100
+ return;
101
+ }
102
+
103
+ response.pipe(file);
104
+ file.on("finish", () => {
105
+ file.close(resolve);
106
+ });
107
+ });
108
+
109
+ request.on("error", (error) => {
110
+ file.close();
111
+ reject(error);
112
+ });
113
+
114
+ file.on("error", (error) => {
115
+ file.close();
116
+ reject(error);
117
+ });
118
+ });
119
+ }
120
+
121
+ export function verifyBinary(binaryPath, { log = console.log, spawnSyncImpl = spawnSync } = {}) {
122
+ const result = spawnSyncImpl(binaryPath, ["--version"], {
123
+ encoding: "utf8",
124
+ stdio: ["ignore", "pipe", "pipe"],
125
+ });
126
+
127
+ if (result.error) {
128
+ throw result.error;
129
+ }
130
+
131
+ if (result.status !== 0) {
132
+ const signalDetail = result.signal ? ` (signal: ${result.signal})` : "";
133
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
134
+ throw new Error(
135
+ `Downloaded binary failed smoke test with exit code ${result.status ?? "unknown"}${signalDetail}${output ? `: ${output}` : ""}`,
136
+ );
137
+ }
138
+
139
+ const versionOutput = (result.stdout ?? "").trim();
140
+ if (!versionOutput) {
141
+ throw new Error("Downloaded binary returned success but produced no version output");
142
+ }
143
+
144
+ log(`✓ Verified ${PACKAGE_NAME} executable (${versionOutput})`);
145
+ return versionOutput;
146
+ }
147
+
148
+ export async function installBinary(options = {}) {
149
+ const rootDir = options.rootDir ?? join(__dirname, "..");
150
+ const binDir = options.binDir ?? join(rootDir, "bin");
151
+ const packageJsonPath = options.packageJsonPath ?? join(rootDir, "package.json");
152
+ const log = options.log ?? console.log;
153
+ const accessImpl = options.accessImpl ?? access;
154
+ const chmodImpl = options.chmodImpl ?? chmodSync;
155
+ const downloadFileImpl = options.downloadFileImpl ?? downloadFile;
156
+ const mkdirImpl = options.mkdirImpl ?? mkdir;
157
+ const readFileImpl = options.readFileImpl ?? readFile;
158
+ const rmImpl = options.rmImpl ?? rm;
159
+ const verifyBinaryImpl = options.verifyBinaryImpl ?? verifyBinary;
160
+
161
+ const version = options.version ?? (await readPackageVersion(packageJsonPath, { readFileImpl }));
162
+ const { binaryName, isWindows } = getPlatformInfo({
163
+ arch: options.arch ?? process.arch,
164
+ platform: options.platform ?? process.platform,
165
+ });
166
+ const binaryPath = join(binDir, binaryName);
167
+ const downloadUrl = buildReleaseAssetUrl(version, binaryName, options.repo ?? GITHUB_REPO);
168
+
169
+ if (await isBinaryInstalled(binaryPath, { accessImpl })) {
170
+ log(`✓ Binary already installed at ${binaryPath}`);
171
+ return { binaryName, binaryPath, downloadUrl, status: "already-installed", version };
172
+ }
173
+
174
+ try {
175
+ await mkdirImpl(binDir, { recursive: true });
176
+ await downloadFileImpl(downloadUrl, binaryPath, { log });
177
+
178
+ if (!isWindows) {
179
+ chmodImpl(binaryPath, 0o755);
180
+ }
181
+
182
+ verifyBinaryImpl(binaryPath, { log });
183
+
184
+ log(`✓ Successfully installed ${PACKAGE_NAME} v${version}`);
185
+ log(` Binary location: ${binaryPath}`);
186
+ return { binaryName, binaryPath, downloadUrl, status: "installed", version };
187
+ } catch (error) {
188
+ await rmImpl(binaryPath, { force: true }).catch(() => {});
189
+ throw error;
190
+ }
191
+ }
192
+
193
+ export function formatInstallError(error) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ return [
196
+ `✗ Failed to install ${PACKAGE_NAME}: ${message}`,
197
+ "",
198
+ `You can manually download binaries from: ${MANUAL_INSTALL_URL}`,
199
+ ].join("\n");
200
+ }
201
+
202
+ export async function runInstallCli(options = {}) {
203
+ const errorLog = options.error ?? console.error;
204
+ try {
205
+ await installBinary(options);
206
+ return 0;
207
+ } catch (error) {
208
+ errorLog(formatInstallError(error));
209
+ return 1;
210
+ }
211
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arashi",
3
- "version": "1.11.2",
3
+ "version": "1.12.0",
4
4
  "description": "Git worktree manager for meta-repositories - The eye of the storm for your development workflow",
5
5
  "keywords": [
6
6
  "cli",
@@ -29,9 +29,9 @@
29
29
  "files": [
30
30
  "bin/arashi",
31
31
  "bin/arashi.js",
32
+ "bin/install-binary.js",
32
33
  "bin/arashi.bat",
33
34
  "bin/arashi.ps1",
34
- "scripts/postinstall.js",
35
35
  "schema/config.schema.json",
36
36
  "README.md",
37
37
  "LICENSE"
@@ -65,7 +65,6 @@
65
65
  "format": "oxfmt --config .oxfmtrc.json --write .",
66
66
  "format:check": "oxfmt --config .oxfmtrc.json --check .",
67
67
  "quality:changed": "bun run scripts/quality/changed-files-quality.ts",
68
- "postinstall": "node scripts/postinstall.js",
69
68
  "prepublishOnly": "bun run schema:publish",
70
69
  "prepare": "husky"
71
70
  },
@@ -85,7 +84,7 @@
85
84
  "ora": "^8.1.1",
86
85
  "oxfmt": "0.28.0",
87
86
  "oxlint": "1.43.0",
88
- "semantic-release": "^24.2.0",
87
+ "semantic-release": "^25.0.3",
89
88
  "ts-json-schema-generator": "2.4.0",
90
89
  "typescript": "^5.9.3"
91
90
  },
@@ -1,176 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Post-install script to download the platform-specific arashi binary from GitHub releases
5
- * This keeps the npm package size small while providing pre-compiled binaries
6
- */
7
-
8
- import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
9
- import { chmodSync, createWriteStream } from "node:fs";
10
- import { dirname, join } from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- import { get } from "node:https";
13
- import { spawnSync } from "node:child_process";
14
-
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
-
18
- const PACKAGE_NAME = "arashi";
19
- const GITHUB_REPO = "corwinm/arashi";
20
-
21
- // Determine platform and binary name
22
- function getPlatformInfo() {
23
- const { platform } = process;
24
- const { arch } = process;
25
-
26
- let binaryName = "";
27
- if (platform === "darwin" && arch === "arm64") {
28
- binaryName = `${PACKAGE_NAME}-macos-arm64`;
29
- } else if (platform === "linux" && arch === "x64") {
30
- binaryName = `${PACKAGE_NAME}-linux-x64`;
31
- } else if (platform === "win32" && arch === "x64") {
32
- binaryName = `${PACKAGE_NAME}-windows-x64.exe`;
33
- } else {
34
- throw new Error(
35
- `Unsupported platform: ${platform}-${arch}. Please build from source or file an issue at https://github.com/${GITHUB_REPO}/issues`,
36
- );
37
- }
38
-
39
- return { binaryName, isWindows: platform === "win32" };
40
- }
41
-
42
- // Download file from URL
43
- function downloadFile(url, dest) {
44
- return new Promise((resolve, reject) => {
45
- console.log(`Downloading ${url}...`);
46
-
47
- const file = createWriteStream(dest);
48
- const request = get(url, (response) => {
49
- // Handle redirects
50
- if (
51
- response.statusCode === 301 ||
52
- response.statusCode === 302 ||
53
- response.statusCode === 307
54
- ) {
55
- file.close();
56
- downloadFile(response.headers.location, dest).then(resolve).catch(reject);
57
- return;
58
- }
59
-
60
- if (response.statusCode !== 200) {
61
- file.close();
62
- reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`));
63
- return;
64
- }
65
-
66
- response.pipe(file);
67
-
68
- file.on("finish", () => {
69
- file.close(resolve);
70
- });
71
- });
72
-
73
- request.on("error", (err) => {
74
- file.close();
75
- reject(err);
76
- });
77
-
78
- file.on("error", (err) => {
79
- file.close();
80
- reject(err);
81
- });
82
- });
83
- }
84
-
85
- function verifyBinary(binaryPath) {
86
- const result = spawnSync(binaryPath, ["--version"], {
87
- encoding: "utf8",
88
- stdio: ["ignore", "pipe", "pipe"],
89
- });
90
-
91
- if (result.error) {
92
- throw result.error;
93
- }
94
-
95
- if (result.status !== 0) {
96
- const signalDetail = result.signal ? ` (signal: ${result.signal})` : "";
97
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
98
- throw new Error(
99
- `Downloaded binary failed smoke test with exit code ${result.status ?? "unknown"}${signalDetail}${output ? `: ${output}` : ""}`,
100
- );
101
- }
102
-
103
- const versionOutput = (result.stdout ?? "").trim();
104
- if (!versionOutput) {
105
- throw new Error("Downloaded binary returned success but produced no version output");
106
- }
107
-
108
- console.log(`✓ Verified ${PACKAGE_NAME} executable (${versionOutput})`);
109
- }
110
-
111
- // Main installation logic
112
- async function install() {
113
- let binaryPath = "";
114
-
115
- try {
116
- // Skip postinstall in development (when installing from the repo)
117
- // Check if we're in development by looking for src/ directory
118
- const srcDir = join(__dirname, "..", "src");
119
- try {
120
- await access(srcDir, constants.F_OK);
121
- console.log("✓ Development environment detected, skipping binary download");
122
- console.log(" Run 'bun run build' to build binary locally");
123
- return;
124
- } catch {
125
- // Not in development, continue with download
126
- }
127
-
128
- // Get version from package.json
129
- const packageJsonPath = join(__dirname, "..", "package.json");
130
- const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
131
- const { version } = packageJson;
132
-
133
- const { binaryName, isWindows } = getPlatformInfo();
134
- const binDir = join(__dirname, "..", "bin");
135
- binaryPath = join(binDir, binaryName);
136
-
137
- // Check if binary already exists
138
- try {
139
- await access(binaryPath, constants.F_OK);
140
- console.log(`✓ Binary already exists at ${binaryPath}`);
141
- return;
142
- } catch {
143
- // Binary doesn't exist, continue with download
144
- }
145
-
146
- // Ensure bin directory exists
147
- await mkdir(binDir, { recursive: true });
148
-
149
- // Download binary from GitHub releases
150
- const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
151
-
152
- await downloadFile(downloadUrl, binaryPath);
153
-
154
- // Make binary executable (Unix-like systems)
155
- if (!isWindows) {
156
- chmodSync(binaryPath, 0o755);
157
- }
158
-
159
- verifyBinary(binaryPath);
160
-
161
- console.log(`✓ Successfully installed ${PACKAGE_NAME} v${version}`);
162
- console.log(` Binary location: ${binaryPath}`);
163
- } catch (error) {
164
- if (binaryPath) {
165
- await rm(binaryPath, { force: true }).catch(() => {});
166
- }
167
-
168
- console.error(`✗ Failed to install ${PACKAGE_NAME}:`, error.message);
169
- console.error(
170
- `\nYou can manually download binaries from: https://github.com/${GITHUB_REPO}/releases`,
171
- );
172
- process.exit(1);
173
- }
174
- }
175
-
176
- await install();