arashi 1.11.3 → 1.13.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 +21 -1
- package/bin/arashi.js +133 -85
- package/bin/install-binary.js +211 -0
- package/bin/update.js +323 -0
- package/package.json +3 -3
- package/scripts/postinstall.js +0 -176
package/README.md
CHANGED
|
@@ -47,13 +47,31 @@ 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
|
+
|
|
58
|
+
To check for package updates or refresh the matching platform binary, run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
arashi update --check
|
|
62
|
+
arashi update --dry-run
|
|
63
|
+
arashi update --yes
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`arashi update` can update npm-managed installs when it can confidently detect the package manager, including npm, pnpm, Yarn, Bun, and Vite+ (`vp update -g arashi`). For official curl installer installs, `arashi update --yes` reruns the installer against the current binary directory.
|
|
67
|
+
|
|
50
68
|
Verify install:
|
|
51
69
|
|
|
52
70
|
```bash
|
|
53
71
|
arashi --version
|
|
54
72
|
```
|
|
55
73
|
|
|
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).
|
|
74
|
+
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
75
|
|
|
58
76
|
### Manual install from GitHub Releases
|
|
59
77
|
|
|
@@ -94,6 +112,8 @@ bun run build
|
|
|
94
112
|
Arashi currently provides these commands:
|
|
95
113
|
|
|
96
114
|
- `arashi init`
|
|
115
|
+
- `arashi install`
|
|
116
|
+
- `arashi update [--check] [--dry-run] [--yes]`
|
|
97
117
|
- `arashi add <git-url>`
|
|
98
118
|
- `arashi clone [--all]`
|
|
99
119
|
- `arashi create <branch>`
|
package/bin/arashi.js
CHANGED
|
@@ -1,110 +1,158 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn
|
|
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";
|
|
7
|
+
import { runNpmManagedUpdate } from "./update.js";
|
|
6
8
|
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
10
|
const __dirname = dirname(__filename);
|
|
9
11
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
+
const currentPlatform = process.platform;
|
|
13
|
+
const currentArch = process.arch;
|
|
14
|
+
const isWindows = currentPlatform === "win32";
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
export function getDefaultBinaryName(platform = currentPlatform) {
|
|
17
|
+
return platform === "win32" ? "arashi.bin.exe" : "arashi.bin";
|
|
18
|
+
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
export function getWrapperName(platform = currentPlatform) {
|
|
21
|
+
return platform === "win32" ? "arashi.bat" : "arashi";
|
|
22
|
+
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
export function resolvePlatformBinaryName({ arch = currentArch, platform = currentPlatform } = {}) {
|
|
25
|
+
return getPlatformInfo({ arch, platform }).binaryName;
|
|
26
|
+
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
export function resolveBinaryPath(options = {}) {
|
|
29
|
+
const binDir = options.binDir ?? __dirname;
|
|
30
|
+
const platform = options.platform ?? currentPlatform;
|
|
31
|
+
const arch = options.arch ?? currentArch;
|
|
32
|
+
const exists = options.existsSyncImpl ?? existsSync;
|
|
33
|
+
const defaultBinaryPath = join(binDir, getDefaultBinaryName(platform));
|
|
28
34
|
|
|
29
|
-
if (
|
|
30
|
-
return
|
|
35
|
+
if (exists(defaultBinaryPath)) {
|
|
36
|
+
return defaultBinaryPath;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
39
|
+
try {
|
|
40
|
+
return join(binDir, resolvePlatformBinaryName({ arch, platform }));
|
|
41
|
+
} catch {
|
|
42
|
+
return defaultBinaryPath;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
46
|
+
export function hasRunnableBinary(options = {}) {
|
|
47
|
+
const binDir = options.binDir ?? __dirname;
|
|
48
|
+
const platform = options.platform ?? currentPlatform;
|
|
49
|
+
const arch = options.arch ?? currentArch;
|
|
50
|
+
const exists = options.existsSyncImpl ?? existsSync;
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
if (exists(join(binDir, getDefaultBinaryName(platform)))) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
66
55
|
|
|
56
|
+
try {
|
|
57
|
+
return exists(join(binDir, resolvePlatformBinaryName({ arch, platform })));
|
|
58
|
+
} catch {
|
|
67
59
|
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function ensureInstalled(options = {}) {
|
|
64
|
+
const binDir = options.binDir ?? __dirname;
|
|
65
|
+
const rootDir = options.rootDir ?? join(binDir, "..");
|
|
66
|
+
const platform = options.platform ?? currentPlatform;
|
|
67
|
+
const arch = options.arch ?? currentArch;
|
|
68
|
+
const log = options.log ?? console.log;
|
|
69
|
+
const installBinaryImpl = options.installBinaryImpl ?? installBinary;
|
|
70
|
+
|
|
71
|
+
if (hasRunnableBinary({ arch, binDir, existsSyncImpl: options.existsSyncImpl, platform })) {
|
|
72
|
+
return { status: "already-installed" };
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
log("arashi binary missing; installing the matching platform binary before continuing.");
|
|
76
|
+
return installBinaryImpl({ ...options, arch, binDir, log, platform, rootDir });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isExplicitInstallCommand(argv) {
|
|
80
|
+
return argv[0] === "install";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isExplicitUpdateCommand(argv) {
|
|
84
|
+
return argv[0] === "update";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function runExplicitInstall(options) {
|
|
88
|
+
const binDir = options.binDir ?? __dirname;
|
|
89
|
+
const rootDir = options.rootDir ?? join(binDir, "..");
|
|
90
|
+
const installBinaryImpl = options.installBinaryImpl ?? installBinary;
|
|
91
|
+
|
|
92
|
+
await installBinaryImpl({ ...options, binDir, rootDir });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function spawnArashi(argv, options = {}) {
|
|
96
|
+
const binDir = options.binDir ?? __dirname;
|
|
97
|
+
const platform = options.platform ?? currentPlatform;
|
|
98
|
+
const windows = platform === "win32";
|
|
99
|
+
const spawnImpl = options.spawnImpl ?? spawn;
|
|
100
|
+
const wrapperPath = join(binDir, getWrapperName(platform));
|
|
101
|
+
const binaryPath = resolveBinaryPath({
|
|
102
|
+
arch: options.arch,
|
|
103
|
+
binDir,
|
|
104
|
+
existsSyncImpl: options.existsSyncImpl,
|
|
105
|
+
platform,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const child = windows
|
|
110
|
+
? spawnImpl(binaryPath, argv, {
|
|
111
|
+
stdio: "inherit",
|
|
112
|
+
windowsHide: false,
|
|
113
|
+
})
|
|
114
|
+
: spawnImpl(wrapperPath, argv, { stdio: "inherit" });
|
|
115
|
+
|
|
116
|
+
child.on("exit", (code, signal) => {
|
|
117
|
+
if (typeof code === "number") {
|
|
118
|
+
resolve(code);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resolve(signal ? 1 : 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
child.on("error", (error) => {
|
|
126
|
+
const errorLog = options.error ?? console.error;
|
|
127
|
+
errorLog(`Failed to start arashi. ${error.message}.`);
|
|
128
|
+
resolve(1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function runEntrypoint(argv = process.argv.slice(2), options = {}) {
|
|
134
|
+
const errorLog = options.error ?? console.error;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
if (isExplicitInstallCommand(argv)) {
|
|
138
|
+
await runExplicitInstall(options);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
76
141
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
142
|
+
if (isExplicitUpdateCommand(argv)) {
|
|
143
|
+
return runNpmManagedUpdate(argv.slice(1), options);
|
|
144
|
+
}
|
|
81
145
|
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
146
|
+
await ensureInstalled(options);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
errorLog(formatInstallError(error));
|
|
149
|
+
return 1;
|
|
102
150
|
}
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
}
|
|
152
|
+
return spawnArashi(argv, options);
|
|
153
|
+
}
|
|
106
154
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
process.
|
|
110
|
-
}
|
|
155
|
+
const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
156
|
+
if (import.meta.url === invokedPath) {
|
|
157
|
+
process.exitCode = await runEntrypoint();
|
|
158
|
+
}
|
|
@@ -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 (!options.force && (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/bin/update.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { getPlatformInfo, installBinary, MANUAL_INSTALL_URL, PACKAGE_NAME } from "./install-binary.js";
|
|
7
|
+
|
|
8
|
+
export const UPDATE_COMMAND_DESCRIPTION = "Check for and apply Arashi updates";
|
|
9
|
+
|
|
10
|
+
export function parseUpdateArgs(argv = []) {
|
|
11
|
+
return {
|
|
12
|
+
check: argv.includes("--check"),
|
|
13
|
+
dryRun: argv.includes("--dry-run"),
|
|
14
|
+
yes: argv.includes("--yes") || argv.includes("-y"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeVersion(version) {
|
|
19
|
+
return String(version ?? "").trim().replace(/^v/, "").split("+")[0];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function compareVersions(a, b) {
|
|
23
|
+
const left = normalizeVersion(a).split(/[.-]/);
|
|
24
|
+
const right = normalizeVersion(b).split(/[.-]/);
|
|
25
|
+
const length = Math.max(left.length, right.length);
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index < length; index += 1) {
|
|
28
|
+
const leftPart = left[index] ?? "0";
|
|
29
|
+
const rightPart = right[index] ?? "0";
|
|
30
|
+
const leftNumber = Number(leftPart);
|
|
31
|
+
const rightNumber = Number(rightPart);
|
|
32
|
+
|
|
33
|
+
if (Number.isInteger(leftNumber) && Number.isInteger(rightNumber)) {
|
|
34
|
+
if (leftNumber > rightNumber) return 1;
|
|
35
|
+
if (leftNumber < rightNumber) return -1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (leftPart === rightPart) continue;
|
|
40
|
+
if (leftPart === "") return 1;
|
|
41
|
+
if (rightPart === "") return -1;
|
|
42
|
+
return leftPart > rightPart ? 1 : -1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readPackageMetadata(rootDir, { readFileImpl = readFile } = {}) {
|
|
49
|
+
const packageJsonPath = join(rootDir, "package.json");
|
|
50
|
+
const packageJson = JSON.parse(await readFileImpl(packageJsonPath, "utf8"));
|
|
51
|
+
return { name: packageJson.name, packageJsonPath, version: packageJson.version };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function detectNpmManagedInstall({ rootDir, metadata } = {}) {
|
|
55
|
+
if (!rootDir || metadata?.name !== PACKAGE_NAME || !metadata?.version) {
|
|
56
|
+
return { method: "ambiguous" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { method: "npm-managed", rootDir, version: metadata.version };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchLatestPackageVersion({ fetchImpl = fetch, packageName = PACKAGE_NAME } = {}) {
|
|
63
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
64
|
+
headers: { accept: "application/json" },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`npm registry returned ${response.status} ${response.statusText}`.trim());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const metadata = await response.json();
|
|
72
|
+
if (!metadata.version) {
|
|
73
|
+
throw new Error("npm registry response did not include a version");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return metadata.version;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function fetchLatestGitHubRelease({ fetchImpl = fetch, repo = "corwinm/arashi" } = {}) {
|
|
80
|
+
const response = await fetchImpl(`https://api.github.com/repos/${repo}/releases/latest`, {
|
|
81
|
+
headers: { accept: "application/vnd.github+json" },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`GitHub releases returned ${response.status} ${response.statusText}`.trim());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const release = await response.json();
|
|
89
|
+
const version = normalizeVersion(release.tag_name ?? release.name);
|
|
90
|
+
if (!version) {
|
|
91
|
+
throw new Error("GitHub release response did not include a tag");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
htmlUrl: release.html_url ?? MANUAL_INSTALL_URL,
|
|
96
|
+
version,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function selectPackageManagerCommand({ env = process.env, rootDir } = {}) {
|
|
101
|
+
const userAgent = env.npm_config_user_agent ?? "";
|
|
102
|
+
const execPath = env.npm_execpath ?? "";
|
|
103
|
+
const combined = `${userAgent} ${execPath}`.toLowerCase();
|
|
104
|
+
const normalizedRootDir = String(rootDir ?? "").toLowerCase();
|
|
105
|
+
const looksLikeVitePlus =
|
|
106
|
+
combined.includes("vite-plus") ||
|
|
107
|
+
/(^|[\\/\s])vp(?:\.exe)?($|[\\/\s])/.test(combined) ||
|
|
108
|
+
/(^|[\\/])\.vite-plus([\\/]|$)/.test(normalizedRootDir);
|
|
109
|
+
|
|
110
|
+
if (looksLikeVitePlus) {
|
|
111
|
+
return { args: ["update", "-g", PACKAGE_NAME], command: "vp", label: "Vite+" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (combined.includes("pnpm")) {
|
|
115
|
+
return { args: ["add", "-g", `${PACKAGE_NAME}@latest`], command: "pnpm", label: "pnpm" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (combined.includes("yarn")) {
|
|
119
|
+
return { args: ["global", "add", `${PACKAGE_NAME}@latest`], command: "yarn", label: "yarn" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (combined.includes("bun")) {
|
|
123
|
+
return { args: ["add", "-g", `${PACKAGE_NAME}@latest`], command: "bun", label: "bun" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (combined.includes("npm")) {
|
|
127
|
+
return { args: ["install", "-g", `${PACKAGE_NAME}@latest`], command: "npm", label: "npm" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatManualUpdateGuidance(latestVersion, platformInfo) {
|
|
134
|
+
const lines = [
|
|
135
|
+
"Automatic update is not available for this install method.",
|
|
136
|
+
`Latest release: v${latestVersion}`,
|
|
137
|
+
`Manual releases: ${MANUAL_INSTALL_URL}`,
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
if (platformInfo?.binaryName) {
|
|
141
|
+
lines.push(`Download asset for this platform: ${platformInfo.binaryName}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lines.push("Replace the existing arashi binary with the downloaded asset and make it executable if needed.");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function formatSupportedPackageManagerGuidance() {
|
|
149
|
+
return [
|
|
150
|
+
"Could not confidently detect the package manager used for this npm-managed install.",
|
|
151
|
+
"Run one of these manually if it matches how you installed Arashi:",
|
|
152
|
+
` npm install -g ${PACKAGE_NAME}@latest`,
|
|
153
|
+
` pnpm add -g ${PACKAGE_NAME}@latest`,
|
|
154
|
+
` yarn global add ${PACKAGE_NAME}@latest`,
|
|
155
|
+
` bun add -g ${PACKAGE_NAME}@latest`,
|
|
156
|
+
` vp update -g ${PACKAGE_NAME}`,
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function defaultConfirmPrompt(message) {
|
|
161
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
162
|
+
try {
|
|
163
|
+
const answer = await rl.question(message);
|
|
164
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
165
|
+
} finally {
|
|
166
|
+
rl.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function confirmUpdate({ promptImpl = defaultConfirmPrompt, yes }) {
|
|
171
|
+
if (yes) return true;
|
|
172
|
+
if (!process.stdin.isTTY) return false;
|
|
173
|
+
return promptImpl("Update Arashi now? [y/N] ");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function defaultRootDir(binDir) {
|
|
177
|
+
return join(binDir ?? fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function runNpmManagedUpdate(argv = [], options = {}) {
|
|
181
|
+
const flags = parseUpdateArgs(argv);
|
|
182
|
+
const log = options.log ?? console.log;
|
|
183
|
+
const errorLog = options.error ?? console.error;
|
|
184
|
+
const rootDir = options.rootDir ?? defaultRootDir(options.binDir);
|
|
185
|
+
let metadata;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
metadata = options.metadata ?? (await readPackageMetadata(rootDir, options));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
errorLog(`Failed to read package metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
|
+
errorLog(formatSupportedPackageManagerGuidance());
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const install = detectNpmManagedInstall({ metadata, rootDir });
|
|
196
|
+
if (install.method !== "npm-managed") {
|
|
197
|
+
log(formatSupportedPackageManagerGuidance());
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let latestVersion;
|
|
202
|
+
try {
|
|
203
|
+
latestVersion = options.latestVersion ?? (await fetchLatestPackageVersion(options));
|
|
204
|
+
} catch (error) {
|
|
205
|
+
errorLog(`Failed to check latest ${PACKAGE_NAME} version: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
errorLog(`Manual releases: ${MANUAL_INSTALL_URL}`);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (compareVersions(metadata.version, latestVersion) >= 0) {
|
|
211
|
+
log(`${PACKAGE_NAME} is already up to date (v${metadata.version}).`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const packageManager = options.packageManager ?? selectPackageManagerCommand(options);
|
|
216
|
+
log(`Update available: ${PACKAGE_NAME} v${metadata.version} -> v${latestVersion}`);
|
|
217
|
+
|
|
218
|
+
if (flags.check) {
|
|
219
|
+
log("Check only: no changes made.");
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!packageManager) {
|
|
224
|
+
log(formatSupportedPackageManagerGuidance());
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const renderedCommand = [packageManager.command, ...packageManager.args].join(" ");
|
|
229
|
+
log(`Selected update command: ${renderedCommand}`);
|
|
230
|
+
log("Binary refresh: reinstall the matching platform binary after the package update.");
|
|
231
|
+
|
|
232
|
+
if (flags.dryRun) {
|
|
233
|
+
log("Dry run: no changes made.");
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!(await confirmUpdate({ promptImpl: options.promptImpl, yes: flags.yes }))) {
|
|
238
|
+
errorLog("Update skipped. Rerun with --yes for non-interactive updates or use --dry-run to inspect the plan.");
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
|
|
243
|
+
const result = spawnSyncImpl(packageManager.command, packageManager.args, {
|
|
244
|
+
cwd: rootDir,
|
|
245
|
+
encoding: "utf8",
|
|
246
|
+
stdio: "inherit",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (result.error) {
|
|
250
|
+
errorLog(`Package manager failed to start: ${result.error.message}`);
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (result.status !== 0) {
|
|
255
|
+
errorLog(`Package manager update failed with exit code ${result.status ?? "unknown"}. Existing binary was not removed.`);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let updatedMetadata = metadata;
|
|
260
|
+
try {
|
|
261
|
+
updatedMetadata = await readPackageMetadata(rootDir, options);
|
|
262
|
+
} catch {
|
|
263
|
+
updatedMetadata = { ...metadata, version: latestVersion };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const installBinaryImpl = options.installBinaryImpl ?? installBinary;
|
|
268
|
+
const installResult = await installBinaryImpl({
|
|
269
|
+
...options,
|
|
270
|
+
binDir: options.binDir,
|
|
271
|
+
force: true,
|
|
272
|
+
rootDir,
|
|
273
|
+
version: updatedMetadata.version,
|
|
274
|
+
});
|
|
275
|
+
log(`✓ Updated ${PACKAGE_NAME} from v${metadata.version} to v${updatedMetadata.version}.`);
|
|
276
|
+
log(` Package-manager command: ${renderedCommand}`);
|
|
277
|
+
log(` Binary location: ${installResult.binaryPath ?? "installed platform binary"}`);
|
|
278
|
+
return 0;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
errorLog(`Package updated, but binary refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
281
|
+
errorLog("Run `arashi install` to retry the binary installation, or download a release manually.");
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function runDirectBinaryUpdate(argv = [], options = {}) {
|
|
287
|
+
const flags = parseUpdateArgs(argv);
|
|
288
|
+
const log = options.log ?? console.log;
|
|
289
|
+
const errorLog = options.error ?? console.error;
|
|
290
|
+
const currentVersion = options.currentVersion;
|
|
291
|
+
|
|
292
|
+
let release;
|
|
293
|
+
try {
|
|
294
|
+
release = options.latestRelease ?? (await fetchLatestGitHubRelease(options));
|
|
295
|
+
} catch (error) {
|
|
296
|
+
errorLog(`Failed to check latest ${PACKAGE_NAME} release: ${error instanceof Error ? error.message : String(error)}`);
|
|
297
|
+
errorLog(`Manual releases: ${MANUAL_INSTALL_URL}`);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let platformInfo;
|
|
302
|
+
try {
|
|
303
|
+
platformInfo = getPlatformInfo({ arch: options.arch, platform: options.platform });
|
|
304
|
+
} catch {
|
|
305
|
+
platformInfo = undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (currentVersion && compareVersions(currentVersion, release.version) >= 0) {
|
|
309
|
+
log(`${PACKAGE_NAME} direct binary is already current (v${currentVersion}).`);
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (currentVersion) {
|
|
314
|
+
log(`Update available: ${PACKAGE_NAME} v${currentVersion} -> v${release.version}`);
|
|
315
|
+
} else {
|
|
316
|
+
log(`Latest ${PACKAGE_NAME} release: v${release.version}`);
|
|
317
|
+
}
|
|
318
|
+
log(formatManualUpdateGuidance(release.version, platformInfo));
|
|
319
|
+
if (release.htmlUrl) log(`Release URL: ${release.htmlUrl}`);
|
|
320
|
+
if (flags.check) log("Check only: no changes made.");
|
|
321
|
+
if (flags.dryRun) log("Dry run: no changes made.");
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arashi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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,10 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"bin/arashi",
|
|
31
31
|
"bin/arashi.js",
|
|
32
|
+
"bin/install-binary.js",
|
|
33
|
+
"bin/update.js",
|
|
32
34
|
"bin/arashi.bat",
|
|
33
35
|
"bin/arashi.ps1",
|
|
34
|
-
"scripts/postinstall.js",
|
|
35
36
|
"schema/config.schema.json",
|
|
36
37
|
"README.md",
|
|
37
38
|
"LICENSE"
|
|
@@ -65,7 +66,6 @@
|
|
|
65
66
|
"format": "oxfmt --config .oxfmtrc.json --write .",
|
|
66
67
|
"format:check": "oxfmt --config .oxfmtrc.json --check .",
|
|
67
68
|
"quality:changed": "bun run scripts/quality/changed-files-quality.ts",
|
|
68
|
-
"postinstall": "node scripts/postinstall.js",
|
|
69
69
|
"prepublishOnly": "bun run schema:publish",
|
|
70
70
|
"prepare": "husky"
|
|
71
71
|
},
|
package/scripts/postinstall.js
DELETED
|
@@ -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();
|