@piperace/cli 0.3.5 → 0.3.6
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/lib/index.js +50 -12
- package/package.json +1 -1
- package/scripts/install.js +81 -20
package/lib/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const BINARY_MAP = {
|
|
|
18
18
|
gui: {
|
|
19
19
|
'darwin arm64': 'piperace-gui-darwin-arm64',
|
|
20
20
|
'darwin x64': 'piperace-gui-darwin-amd64',
|
|
21
|
+
'linux arm64': 'piperace-gui-linux-arm64',
|
|
22
|
+
'linux x64': 'piperace-gui-linux-amd64',
|
|
21
23
|
'win32 x64': 'piperace-gui.exe'
|
|
22
24
|
}
|
|
23
25
|
};
|
|
@@ -26,26 +28,61 @@ function platformKey() {
|
|
|
26
28
|
return `${os.platform()} ${os.arch()}`;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
function
|
|
31
|
+
function binaryPathFor(kind) {
|
|
30
32
|
const name = kind === 'gui' ? 'piperace-gui' : 'piperace';
|
|
31
|
-
|
|
33
|
+
return path.join(BIN_DIR, name + (os.platform() === 'win32' ? '.exe' : ''));
|
|
34
|
+
}
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`Try running: npm run postinstall\n` +
|
|
39
|
-
`Or set PIPERACE_BINARY_PATH to the downloaded binary.`
|
|
40
|
-
);
|
|
36
|
+
function binaryExists(binaryPath) {
|
|
37
|
+
try {
|
|
38
|
+
return fs.statSync(binaryPath).size > 1000;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
41
|
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function missingBinaryError(kind, binaryPath) {
|
|
45
|
+
const expectedSource = BINARY_MAP[kind]?.[platformKey()];
|
|
46
|
+
return new Error(
|
|
47
|
+
`Piperace ${kind} binary not found at ${binaryPath}.\n` +
|
|
48
|
+
`Expected source binary: ${expectedSource || 'none for ' + platformKey()}\n` +
|
|
49
|
+
`Try running: npm run postinstall\n` +
|
|
50
|
+
`Or set PIPERACE_BINARY_PATH to the downloaded binary.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
42
53
|
|
|
54
|
+
function getBinaryPath(kind = 'cli') {
|
|
55
|
+
const binaryPath = binaryPathFor(kind);
|
|
56
|
+
if (!binaryExists(binaryPath)) {
|
|
57
|
+
throw missingBinaryError(kind, binaryPath);
|
|
58
|
+
}
|
|
59
|
+
return binaryPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Ensure the binary is present, downloading it on demand. The GUI binary is not
|
|
63
|
+
// fetched during `npm install`, so its first launch lazily downloads it here.
|
|
64
|
+
async function ensureBinaryPath(kind = 'cli') {
|
|
65
|
+
const binaryPath = binaryPathFor(kind);
|
|
66
|
+
if (binaryExists(binaryPath)) return binaryPath;
|
|
67
|
+
|
|
68
|
+
if (kind === 'gui') {
|
|
69
|
+
console.log('[piperace] First GUI launch — downloading the desktop binary (one-time)...');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Reuse the installer's download/retry logic. The require.main guard in
|
|
73
|
+
// install.js keeps this from re-running the CLI install as a side effect.
|
|
74
|
+
const { installBinary } = require('../scripts/install.js');
|
|
75
|
+
await installBinary(kind);
|
|
76
|
+
|
|
77
|
+
if (!binaryExists(binaryPath)) {
|
|
78
|
+
throw missingBinaryError(kind, binaryPath);
|
|
79
|
+
}
|
|
43
80
|
return binaryPath;
|
|
44
81
|
}
|
|
45
82
|
|
|
46
|
-
function run(args = [], options = {}) {
|
|
83
|
+
async function run(args = [], options = {}) {
|
|
47
84
|
const kind = options.gui ? 'gui' : 'cli';
|
|
48
|
-
const binaryPath =
|
|
85
|
+
const binaryPath = await ensureBinaryPath(kind);
|
|
49
86
|
const child = spawn(binaryPath, args, {
|
|
50
87
|
stdio: options.stdio || 'inherit',
|
|
51
88
|
env: { ...process.env, ...options.env },
|
|
@@ -71,6 +108,7 @@ function run(args = [], options = {}) {
|
|
|
71
108
|
|
|
72
109
|
module.exports = {
|
|
73
110
|
getBinaryPath,
|
|
111
|
+
ensureBinaryPath,
|
|
74
112
|
run,
|
|
75
113
|
platformKey
|
|
76
114
|
};
|
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -5,13 +5,15 @@ const path = require('path');
|
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const http = require('http');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const crypto = require('crypto');
|
|
9
8
|
const { execSync } = require('child_process');
|
|
10
9
|
|
|
11
10
|
const VERSION = require('../package.json').version;
|
|
12
11
|
const ROOT = path.join(__dirname, '..');
|
|
13
12
|
const BIN_DIR = path.join(ROOT, 'binaries');
|
|
14
13
|
|
|
14
|
+
const DOWNLOAD_TIMEOUT_MS = 30000; // per-attempt socket timeout
|
|
15
|
+
const DOWNLOAD_RETRIES = 2; // extra attempts after the first
|
|
16
|
+
|
|
15
17
|
// Map Node platform/arch to the binary names produced by the Go build.
|
|
16
18
|
const BINARY_MAP = {
|
|
17
19
|
cli: {
|
|
@@ -25,6 +27,8 @@ const BINARY_MAP = {
|
|
|
25
27
|
gui: {
|
|
26
28
|
'darwin arm64': 'piperace-gui-darwin-arm64',
|
|
27
29
|
'darwin x64': 'piperace-gui-darwin-amd64',
|
|
30
|
+
'linux arm64': 'piperace-gui-linux-arm64',
|
|
31
|
+
'linux x64': 'piperace-gui-linux-amd64',
|
|
28
32
|
'win32 x64': 'piperace-gui.exe'
|
|
29
33
|
}
|
|
30
34
|
};
|
|
@@ -65,32 +69,76 @@ function findLocalBinary(sourceName) {
|
|
|
65
69
|
return null;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
// Download a single file with a hard per-attempt timeout so a stalled origin
|
|
73
|
+
// connection can never hang the install indefinitely.
|
|
74
|
+
function downloadFile(url, dest, { timeout = DOWNLOAD_TIMEOUT_MS, redirects = 5 } = {}) {
|
|
69
75
|
return new Promise((resolve, reject) => {
|
|
70
76
|
const client = url.startsWith('https:') ? https : http;
|
|
71
77
|
const file = fs.createWriteStream(dest);
|
|
78
|
+
let settled = false;
|
|
72
79
|
|
|
73
|
-
|
|
80
|
+
const cleanup = () => {
|
|
81
|
+
file.destroy();
|
|
82
|
+
try { fs.unlinkSync(dest); } catch {}
|
|
83
|
+
};
|
|
84
|
+
const fail = (err) => {
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
cleanup();
|
|
88
|
+
reject(err);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const req = client.get(url, (res) => {
|
|
74
92
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
settled = true; // hand off to the redirect target
|
|
94
|
+
cleanup();
|
|
95
|
+
res.resume();
|
|
96
|
+
if (redirects <= 0) {
|
|
97
|
+
return reject(new Error(`too many redirects from ${url}`));
|
|
98
|
+
}
|
|
99
|
+
return downloadFile(res.headers.location, dest, { timeout, redirects: redirects - 1 })
|
|
100
|
+
.then(resolve).catch(reject);
|
|
78
101
|
}
|
|
79
102
|
if (res.statusCode !== 200) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
103
|
+
res.resume();
|
|
104
|
+
return fail(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
83
105
|
}
|
|
84
106
|
|
|
107
|
+
res.on('error', fail);
|
|
85
108
|
res.pipe(file);
|
|
86
|
-
file.on('finish', () =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
file.on('finish', () => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
settled = true;
|
|
112
|
+
file.close((err) => (err ? reject(err) : resolve()));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Fires on connection-establish AND idle-read stalls.
|
|
117
|
+
req.setTimeout(timeout, () => {
|
|
118
|
+
req.destroy(new Error(`download timed out after ${timeout}ms: ${url}`));
|
|
90
119
|
});
|
|
120
|
+
req.on('error', fail);
|
|
121
|
+
file.on('error', fail);
|
|
91
122
|
});
|
|
92
123
|
}
|
|
93
124
|
|
|
125
|
+
async function downloadWithRetry(url, dest, { timeout = DOWNLOAD_TIMEOUT_MS, retries = DOWNLOAD_RETRIES } = {}) {
|
|
126
|
+
let lastErr;
|
|
127
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
128
|
+
if (attempt > 0) {
|
|
129
|
+
const delay = 1000 * attempt;
|
|
130
|
+
console.warn(`[piperace] download failed (${lastErr.message}); retry ${attempt}/${retries} in ${delay}ms...`);
|
|
131
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return await downloadFile(url, dest, { timeout });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
lastErr = err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw lastErr;
|
|
140
|
+
}
|
|
141
|
+
|
|
94
142
|
async function installBinary(kind = 'cli') {
|
|
95
143
|
const sourceName = binaryName(kind);
|
|
96
144
|
if (!sourceName) {
|
|
@@ -107,7 +155,6 @@ async function installBinary(kind = 'cli') {
|
|
|
107
155
|
try {
|
|
108
156
|
fs.chmodSync(target, 0o755);
|
|
109
157
|
} catch {}
|
|
110
|
-
console.log(`[piperace] ${kind} binary already installed.`);
|
|
111
158
|
return;
|
|
112
159
|
}
|
|
113
160
|
|
|
@@ -127,7 +174,7 @@ async function installBinary(kind = 'cli') {
|
|
|
127
174
|
console.log(`[piperace] Downloading ${kind} binary from ${url}`);
|
|
128
175
|
|
|
129
176
|
try {
|
|
130
|
-
await
|
|
177
|
+
await downloadWithRetry(url, target);
|
|
131
178
|
fs.chmodSync(target, 0o755);
|
|
132
179
|
removeQuarantine(target);
|
|
133
180
|
console.log(`[piperace] ${kind} binary installed successfully.`);
|
|
@@ -135,7 +182,8 @@ async function installBinary(kind = 'cli') {
|
|
|
135
182
|
try { fs.unlinkSync(target); } catch {}
|
|
136
183
|
console.error(`[piperace] Failed to download ${kind} binary: ${err.message}`);
|
|
137
184
|
console.error(`[piperace] You can manually download ${sourceName} from ${baseUrl} and place it at ${target}`);
|
|
138
|
-
// Do not fail npm install; the launcher will
|
|
185
|
+
// Do not fail npm install; the launcher will retry lazily and print a
|
|
186
|
+
// helpful message if the binary is still missing at run time.
|
|
139
187
|
}
|
|
140
188
|
}
|
|
141
189
|
|
|
@@ -148,7 +196,20 @@ function removeQuarantine(filePath) {
|
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
198
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
199
|
+
module.exports = {
|
|
200
|
+
installBinary,
|
|
201
|
+
binaryName,
|
|
202
|
+
finalBinaryPath,
|
|
203
|
+
platformKey,
|
|
204
|
+
BINARY_MAP
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Only the lightweight CLI binary is fetched at install time. The much larger
|
|
208
|
+
// GUI binary is downloaded lazily on the first `piperace-gui` run (see
|
|
209
|
+
// lib/index.js), so `npm install` stays fast for the common CLI-only case.
|
|
210
|
+
if (require.main === module) {
|
|
211
|
+
// Never let a failure here break `npm install`; the launcher retries lazily.
|
|
212
|
+
Promise.resolve()
|
|
213
|
+
.then(() => installBinary('cli'))
|
|
214
|
+
.catch((err) => console.error(`[piperace] install skipped: ${err.message}`));
|
|
215
|
+
}
|