sphere-cli 0.2.6 → 0.2.8
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/bin/sphere.js +21 -15
- package/package.json +3 -2
- package/scripts/engine.js +242 -0
- package/scripts/postinstall.js +88 -107
package/bin/sphere.js
CHANGED
|
@@ -5,30 +5,36 @@
|
|
|
5
5
|
* sphere — thin launcher.
|
|
6
6
|
*
|
|
7
7
|
* The actual CLI is a sealed, signed+notarized native binary (PyInstaller bundle
|
|
8
|
-
* of the SPHERE engine
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* of the SPHERE engine). It is auto-placed in a roomy directory by the install
|
|
9
|
+
* step (see scripts/engine.js). This launcher locates it, re-downloads it if it
|
|
10
|
+
* has gone missing (e.g. scratch purge), optionally caches it on node-local disk
|
|
11
|
+
* for fast startup on network filesystems, and forwards argv. No algorithm code
|
|
12
|
+
* ships in the npm package.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const fs = require('fs');
|
|
15
15
|
const { spawnSync } = require('child_process');
|
|
16
|
+
const engine = require('../scripts/engine.js');
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const tell = (m) => process.stderr.write(`sphere: ${m}\n`);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let bin;
|
|
21
|
+
try {
|
|
22
|
+
bin = engine.resolveBinary();
|
|
23
|
+
if (!bin) {
|
|
24
|
+
tell('engine not found — installing it now (one-time) …');
|
|
25
|
+
bin = engine.ensureInstalled((m) => process.stderr.write(` ${m}\n`));
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
tell(`could not locate or install the engine: ${e.message}`);
|
|
29
|
+
tell('try: npm install -g sphere-cli (or set SPHERE_HOME=/path/with/space)');
|
|
26
30
|
process.exit(1);
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
const
|
|
33
|
+
const run = engine.fastBinary(bin, tell);
|
|
34
|
+
|
|
35
|
+
const res = spawnSync(run, process.argv.slice(2), { stdio: 'inherit' });
|
|
30
36
|
if (res.error) {
|
|
31
|
-
|
|
37
|
+
tell(`failed to launch: ${res.error.message}`);
|
|
32
38
|
process.exit(1);
|
|
33
39
|
}
|
|
34
40
|
process.exit(res.status === null ? 1 : res.status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphere-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "SPHERE CLI — synthetic data generation, evaluation, and certification (sealed native binary)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"synthetic-data",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/sphere.js",
|
|
27
|
-
"scripts/postinstall.js"
|
|
27
|
+
"scripts/postinstall.js",
|
|
28
|
+
"scripts/engine.js"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared engine logic for the SPHERE CLI npm package — used by BOTH the
|
|
5
|
+
* postinstall (to place the binary) and the launcher (to find / self-heal /
|
|
6
|
+
* fast-start it). No algorithm code lives here; this only locates, downloads,
|
|
7
|
+
* verifies, and runs the sealed native binary.
|
|
8
|
+
*
|
|
9
|
+
* Design goals (works on ANY HPC / laptop, zero per-cluster config):
|
|
10
|
+
* 1. The ~500 MB binary is NOT kept inside node_modules (that forces users to
|
|
11
|
+
* repoint `npm config set prefix` to dodge home-dir quotas). Instead it is
|
|
12
|
+
* placed in an auto-detected roomy directory — a generic scan of common HPC
|
|
13
|
+
* scratch/project vars, falling back to the XDG data dir.
|
|
14
|
+
* 2. The launcher self-heals: if the binary is gone (scratch purged, moved),
|
|
15
|
+
* it re-downloads on next run.
|
|
16
|
+
* 3. Fast-start: if the binary lives on a *network* filesystem (Lustre/NFS/
|
|
17
|
+
* GPFS — slow to open the ~330 bundled libs), cache a copy on node-local
|
|
18
|
+
* disk so startup isn't dominated by network round-trips.
|
|
19
|
+
*
|
|
20
|
+
* Env overrides: SPHERE_HOME (install dir), SPHERE_NO_FAST=1, SPHERE_FAST_DIR,
|
|
21
|
+
* SPHERE_NO_PATH_SETUP=1, SPHERE_BINARY_BASEURL, SPHERE_SKIP_POSTINSTALL.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const { execFileSync } = require('child_process');
|
|
29
|
+
|
|
30
|
+
const REPO = 'statzihuai/sphere-cli';
|
|
31
|
+
// Binary release tag — decoupled from the npm package version so JS-only patch
|
|
32
|
+
// releases reuse the same prebuilt/notarized binaries.
|
|
33
|
+
const BINARY_RELEASE = 'v0.2.6';
|
|
34
|
+
|
|
35
|
+
const PLATFORM = process.platform; // 'darwin' | 'linux'
|
|
36
|
+
const ARCH = process.arch; // 'arm64' | 'x64'
|
|
37
|
+
const KEY = `${PLATFORM}-${ARCH}`;
|
|
38
|
+
const SUPPORTED = new Set(['darwin-arm64', 'darwin-x64', 'linux-x64']);
|
|
39
|
+
const ASSET = `sphere-${KEY}.tar.gz`;
|
|
40
|
+
const BASE = process.env.SPHERE_BINARY_BASEURL
|
|
41
|
+
|| `https://github.com/${REPO}/releases/download/${BINARY_RELEASE}`;
|
|
42
|
+
|
|
43
|
+
// ── small persistent record of where the binary was installed ────────────────
|
|
44
|
+
// Lives in the (tiny, always-writable) XDG config dir so the launcher can find
|
|
45
|
+
// the binary regardless of which roomy dir the installer chose.
|
|
46
|
+
function configFile() {
|
|
47
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
48
|
+
return path.join(base, 'sphere-cli', 'install.json');
|
|
49
|
+
}
|
|
50
|
+
function readRecord() {
|
|
51
|
+
try { return JSON.parse(fs.readFileSync(configFile(), 'utf8')); } catch (_) { return null; }
|
|
52
|
+
}
|
|
53
|
+
function writeRecord(rec) {
|
|
54
|
+
try {
|
|
55
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
56
|
+
fs.writeFileSync(configFile(), JSON.stringify(rec, null, 2));
|
|
57
|
+
} catch (_) { /* best-effort */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── free space (GB) on the filesystem holding `dir` (best-effort) ────────────
|
|
61
|
+
function freeGB(dir) {
|
|
62
|
+
try {
|
|
63
|
+
let p = dir;
|
|
64
|
+
while (p && !fs.existsSync(p)) p = path.dirname(p);
|
|
65
|
+
if (!p) return Infinity; // unknown → don't exclude
|
|
66
|
+
if (typeof fs.statfsSync === 'function') {
|
|
67
|
+
const s = fs.statfsSync(p);
|
|
68
|
+
return (s.bavail * s.bsize) / 1e9;
|
|
69
|
+
}
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
return Infinity; // can't tell → optimistic; real test is the extraction
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── filesystem type magic (Linux statfs) → is this a slow network FS? ────────
|
|
75
|
+
const NETWORK_FS = new Set([
|
|
76
|
+
0x6969, // NFS
|
|
77
|
+
0xff534d42, // CIFS/SMB
|
|
78
|
+
0x0bd00bd0, // Lustre
|
|
79
|
+
0x47504653, // GPFS
|
|
80
|
+
0x19830326, // BeeGFS/FhGFS
|
|
81
|
+
0x65735546, // FUSE (often network-backed: sshfs, etc.)
|
|
82
|
+
0x1161970, // CODA
|
|
83
|
+
]);
|
|
84
|
+
function isNetworkFS(dir) {
|
|
85
|
+
try {
|
|
86
|
+
if (typeof fs.statfsSync !== 'function') return false;
|
|
87
|
+
let p = dir;
|
|
88
|
+
while (p && !fs.existsSync(p)) p = path.dirname(p);
|
|
89
|
+
const t = fs.statfsSync(p).type;
|
|
90
|
+
return NETWORK_FS.has(t);
|
|
91
|
+
} catch (_) { return false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── ordered list of dirs to try installing into (roomy, persistent-preferred) ─
|
|
95
|
+
function candidateDirs() {
|
|
96
|
+
const sub = path.join('sphere-cli', BINARY_RELEASE);
|
|
97
|
+
if (process.env.SPHERE_HOME) return [path.join(process.env.SPHERE_HOME, BINARY_RELEASE)];
|
|
98
|
+
|
|
99
|
+
const home = os.homedir();
|
|
100
|
+
const env = process.env;
|
|
101
|
+
const looksHpc = !!(env.SCRATCH || env.WORK || env.PROJECT || env.OAK
|
|
102
|
+
|| env.GROUP_HOME || env.PI_HOME || env.SLURM_JOB_ID || env.PBS_JOBID
|
|
103
|
+
|| env.LSB_JOBID || env.GROUP_SCRATCH || env.PSCRATCH);
|
|
104
|
+
|
|
105
|
+
const persistent = ['WORK', 'PROJECT', 'PROJECTS', 'OAK', 'GROUP_HOME', 'PI_HOME', 'DATA']
|
|
106
|
+
.map((v) => env[v]).filter(Boolean);
|
|
107
|
+
const scratch = ['SCRATCH', 'PSCRATCH', 'GROUP_SCRATCH']
|
|
108
|
+
.map((v) => env[v]).filter(Boolean);
|
|
109
|
+
|
|
110
|
+
const xdg = env.XDG_DATA_HOME
|
|
111
|
+
? path.join(env.XDG_DATA_HOME, 'sphere-cli', BINARY_RELEASE)
|
|
112
|
+
: path.join(home, '.local', 'share', sub);
|
|
113
|
+
|
|
114
|
+
// On a cluster: prefer roomy persistent storage, then scratch; always keep the
|
|
115
|
+
// XDG home dir as a last resort. On a laptop: just the XDG dir.
|
|
116
|
+
let candidates = looksHpc
|
|
117
|
+
? [...persistent, ...scratch].map((d) => path.join(d, '.' + sub)).concat([xdg])
|
|
118
|
+
: [xdg];
|
|
119
|
+
|
|
120
|
+
// Stable de-dup, then float the ones that *look* like they have space to the
|
|
121
|
+
// front (df can't see per-user quotas, so this is a hint — installInto() falls
|
|
122
|
+
// through to the next candidate if extraction actually runs out of space).
|
|
123
|
+
candidates = [...new Set(candidates)];
|
|
124
|
+
const withSpace = candidates.filter((c) => freeGB(path.dirname(path.dirname(c))) >= 1.5);
|
|
125
|
+
const without = candidates.filter((c) => !withSpace.includes(c));
|
|
126
|
+
return [...withSpace, ...without];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ensure the binary is installed; returns its path. Idempotent — if a matching
|
|
130
|
+
// install is already recorded and present, returns it without re-downloading.
|
|
131
|
+
// Tries each candidate dir until one works (handles quota/space failures).
|
|
132
|
+
function ensureInstalled(log) {
|
|
133
|
+
log = log || (() => {});
|
|
134
|
+
const existing = resolveBinary();
|
|
135
|
+
if (existing) return existing;
|
|
136
|
+
let lastErr;
|
|
137
|
+
for (const dir of candidateDirs()) {
|
|
138
|
+
try {
|
|
139
|
+
const bin = installInto(dir, log);
|
|
140
|
+
writeRecord({ release: BINARY_RELEASE, key: KEY, dir, bin });
|
|
141
|
+
return bin;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
lastErr = e;
|
|
144
|
+
try { fs.rmSync(path.join(dir, 'sphere-cli'), { recursive: true, force: true }); } catch (_) {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw lastErr || new Error('no writable install location with enough space found');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── download + checksum + extract into <dir>/sphere-cli ──────────────────────
|
|
151
|
+
function curl(url, dest) {
|
|
152
|
+
execFileSync('curl', ['-fL', '--retry', '3', '--retry-delay', '2',
|
|
153
|
+
'--connect-timeout', '30', '-o', dest, url], { stdio: ['ignore', 'ignore', 'inherit'] });
|
|
154
|
+
}
|
|
155
|
+
function sha256(file) {
|
|
156
|
+
const h = crypto.createHash('sha256');
|
|
157
|
+
h.update(fs.readFileSync(file));
|
|
158
|
+
return h.digest('hex');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Download the platform tarball into `dir`, verify SHA256, extract so that
|
|
162
|
+
// `dir/sphere-cli/sphere` exists. Returns the binary path. Throws on failure.
|
|
163
|
+
function installInto(dir, log) {
|
|
164
|
+
log = log || (() => {});
|
|
165
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
166
|
+
const tarball = path.join(dir, ASSET);
|
|
167
|
+
|
|
168
|
+
log(`downloading ${ASSET} (${BINARY_RELEASE}) …`);
|
|
169
|
+
curl(`${BASE}/${ASSET}`, tarball);
|
|
170
|
+
|
|
171
|
+
const sumsFile = path.join(dir, 'SHA256SUMS.txt');
|
|
172
|
+
curl(`${BASE}/SHA256SUMS.txt`, sumsFile);
|
|
173
|
+
const expected = fs.readFileSync(sumsFile, 'utf8').split('\n')
|
|
174
|
+
.map((l) => l.trim().split(/\s+/))
|
|
175
|
+
.find((p) => p[1] && p[1].endsWith(ASSET));
|
|
176
|
+
if (!expected) throw new Error(`no checksum for ${ASSET}`);
|
|
177
|
+
const got = sha256(tarball);
|
|
178
|
+
if (got !== expected[0]) throw new Error(`checksum mismatch for ${ASSET}`);
|
|
179
|
+
log('checksum verified ✓');
|
|
180
|
+
|
|
181
|
+
fs.rmSync(path.join(dir, 'sphere-cli'), { recursive: true, force: true });
|
|
182
|
+
execFileSync('tar', ['-xzf', tarball, '-C', dir], { stdio: 'inherit' });
|
|
183
|
+
fs.rmSync(tarball, { force: true });
|
|
184
|
+
fs.rmSync(sumsFile, { force: true });
|
|
185
|
+
|
|
186
|
+
const bin = path.join(dir, 'sphere-cli', 'sphere');
|
|
187
|
+
if (!fs.existsSync(bin)) throw new Error('extraction did not produce sphere binary');
|
|
188
|
+
fs.chmodSync(bin, 0o755);
|
|
189
|
+
if (PLATFORM === 'darwin') {
|
|
190
|
+
try { execFileSync('xattr', ['-cr', path.join(dir, 'sphere-cli')], { stdio: 'ignore' }); } catch (_) {}
|
|
191
|
+
}
|
|
192
|
+
return bin;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── find the installed binary (record → that dir; else legacy vendor) ────────
|
|
196
|
+
function vendorBin() {
|
|
197
|
+
return path.join(__dirname, '..', 'vendor', 'sphere-cli', 'sphere');
|
|
198
|
+
}
|
|
199
|
+
function resolveBinary() {
|
|
200
|
+
const rec = readRecord();
|
|
201
|
+
if (rec && rec.release === BINARY_RELEASE && rec.bin && fs.existsSync(rec.bin)) return rec.bin;
|
|
202
|
+
const v = vendorBin();
|
|
203
|
+
if (fs.existsSync(v)) return v;
|
|
204
|
+
return null; // caller decides to (re)install
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── fast-start: if `bin` is on a network FS, run from a node-local copy ───────
|
|
208
|
+
// Network filesystems (Lustre/NFS/GPFS) are slow to open the ~330 bundled libs.
|
|
209
|
+
// Caching to local disk makes startup fast for the rest of the job. Only kicks
|
|
210
|
+
// in on network FS (local installs gain nothing); opt out with SPHERE_NO_FAST=1.
|
|
211
|
+
function fastBinary(bin, log) {
|
|
212
|
+
log = log || (() => {});
|
|
213
|
+
try {
|
|
214
|
+
if (process.env.SPHERE_NO_FAST === '1') return bin;
|
|
215
|
+
const srcDir = path.dirname(bin); // …/sphere-cli
|
|
216
|
+
if (!isNetworkFS(srcDir)) return bin; // already local → nothing to gain
|
|
217
|
+
const localRoot = process.env.SPHERE_FAST_DIR
|
|
218
|
+
|| process.env.L_SCRATCH || process.env.TMPDIR || os.tmpdir();
|
|
219
|
+
if (!localRoot) return bin;
|
|
220
|
+
const dest = path.join(localRoot, 'sphere-cli-cache', BINARY_RELEASE);
|
|
221
|
+
const localBin = path.join(dest, 'sphere-cli', 'sphere');
|
|
222
|
+
if (fs.existsSync(localBin)) return localBin; // already cached on this node
|
|
223
|
+
if (freeGB(localRoot) < 1.5) return bin; // not enough local space
|
|
224
|
+
// copy once (first run on this node pays it; later runs in the job are fast).
|
|
225
|
+
// Copy to a temp dir then rename so a partial/aborted copy is never used.
|
|
226
|
+
log('caching engine to node-local disk for faster startup (one-time) …');
|
|
227
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
228
|
+
const tmp = fs.mkdtempSync(path.join(localRoot, 'sphere-cache-'));
|
|
229
|
+
execFileSync('cp', ['-a', srcDir, path.join(tmp, 'sphere-cli')], { stdio: 'ignore' });
|
|
230
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
231
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
232
|
+
fs.renameSync(path.join(tmp, 'sphere-cli'), path.join(dest, 'sphere-cli'));
|
|
233
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
234
|
+
return fs.existsSync(localBin) ? localBin : bin;
|
|
235
|
+
} catch (_) { return bin; } // any trouble → just use the original
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
KEY, ASSET, BINARY_RELEASE, SUPPORTED, PLATFORM,
|
|
240
|
+
configFile, readRecord, writeRecord,
|
|
241
|
+
candidateDirs, installInto, ensureInstalled, resolveBinary, fastBinary, vendorBin,
|
|
242
|
+
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,133 +1,114 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SPHERE CLI postinstall
|
|
4
|
+
* SPHERE CLI postinstall.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Places the platform-specific sealed binary in a roomy, auto-detected location
|
|
7
|
+
* (NOT inside node_modules — that would force users to repoint `npm config set
|
|
8
|
+
* prefix` to dodge home-dir quotas). All the location/download/verify logic lives
|
|
9
|
+
* in ./engine.js, shared with the launcher. This script just drives it, warms the
|
|
10
|
+
* binary once, and wires up PATH so `sphere` is callable without manual setup.
|
|
11
11
|
*
|
|
12
|
-
* Env:
|
|
13
|
-
* SPHERE_SKIP_POSTINSTALL=1 skip download (CI / offline)
|
|
14
|
-
* SPHERE_BINARY_BASEURL=... override the release base URL (testing)
|
|
12
|
+
* Env: SPHERE_SKIP_POSTINSTALL=1, SPHERE_HOME=<dir>, SPHERE_NO_PATH_SETUP=1.
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
15
|
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
18
17
|
const path = require('path');
|
|
19
|
-
const crypto = require('crypto');
|
|
20
18
|
const { execFileSync } = require('child_process');
|
|
21
|
-
|
|
22
|
-
const PKG = require('../package.json');
|
|
23
|
-
const VERSION = PKG.version;
|
|
24
|
-
const REPO = 'statzihuai/sphere-cli';
|
|
25
|
-
// Binary release tag — decoupled from the npm package version so JS-only patch
|
|
26
|
-
// releases reuse the same prebuilt/notarized binaries without re-uploading them.
|
|
27
|
-
const BINARY_RELEASE = 'v0.2.6';
|
|
28
|
-
|
|
29
|
-
const PLATFORM = process.platform; // 'darwin' | 'linux'
|
|
30
|
-
const ARCH = process.arch; // 'arm64' | 'x64'
|
|
31
|
-
const KEY = `${PLATFORM}-${ARCH}`;
|
|
32
|
-
const SUPPORTED = new Set(['darwin-arm64', 'darwin-x64', 'linux-x64']);
|
|
19
|
+
const engine = require('./engine');
|
|
33
20
|
|
|
34
21
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
35
|
-
const
|
|
36
|
-
const ASSET = `sphere-${KEY}.tar.gz`;
|
|
37
|
-
const BASE = process.env.SPHERE_BINARY_BASEURL
|
|
38
|
-
|| `https://github.com/${REPO}/releases/download/${BINARY_RELEASE}`;
|
|
39
|
-
|
|
22
|
+
const PLATFORM = engine.PLATFORM;
|
|
40
23
|
function log(m) { process.stdout.write(`sphere-cli: ${m}\n`); }
|
|
41
|
-
function fail(m) { process.stderr.write(`sphere-cli: ${m}\n`); process.exit(1); }
|
|
42
24
|
|
|
43
25
|
if (process.env.SPHERE_SKIP_POSTINSTALL === '1') {
|
|
44
26
|
log('SPHERE_SKIP_POSTINSTALL=1 — skipping binary download.');
|
|
45
27
|
process.exit(0);
|
|
46
28
|
}
|
|
47
|
-
|
|
48
|
-
if (!SUPPORTED.has(KEY)) {
|
|
49
|
-
// Don't hard-fail the install; the launcher prints guidance if run.
|
|
29
|
+
if (!engine.SUPPORTED.has(engine.KEY)) {
|
|
50
30
|
process.stderr.write(
|
|
51
|
-
`sphere-cli: no prebuilt binary for ${KEY}
|
|
52
|
-
`(supported: ${[...SUPPORTED].join(', ')}).\n`,
|
|
31
|
+
`sphere-cli: no prebuilt binary for ${engine.KEY} ` +
|
|
32
|
+
`(supported: ${[...engine.SUPPORTED].join(', ')}).\n`,
|
|
53
33
|
);
|
|
54
34
|
process.exit(0);
|
|
55
35
|
}
|
|
56
36
|
|
|
57
|
-
// Already installed for this version?
|
|
58
|
-
const STAMP = path.join(VENDOR, '.version');
|
|
59
|
-
const BIN = path.join(VENDOR, 'sphere-cli', 'sphere');
|
|
60
|
-
if (fs.existsSync(BIN) && fs.existsSync(STAMP) &&
|
|
61
|
-
fs.readFileSync(STAMP, 'utf8').trim() === BINARY_RELEASE) {
|
|
62
|
-
log(`binary already present (${BINARY_RELEASE}).`);
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function curl(url, dest) {
|
|
67
|
-
execFileSync('curl', ['-fL', '--retry', '3', '--retry-delay', '2',
|
|
68
|
-
'--connect-timeout', '30', '-o', dest, url], { stdio: ['ignore', 'ignore', 'inherit'] });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sha256(file) {
|
|
72
|
-
const h = crypto.createHash('sha256');
|
|
73
|
-
h.update(fs.readFileSync(file));
|
|
74
|
-
return h.digest('hex');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
37
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fs.chmodSync(BIN, 0o755);
|
|
102
|
-
fs.unlinkSync(tarball);
|
|
103
|
-
fs.rmSync(sumsFile, { force: true });
|
|
104
|
-
fs.writeFileSync(STAMP, BINARY_RELEASE);
|
|
105
|
-
|
|
106
|
-
// ── macOS: avoid a painfully slow FIRST run ────────────────────────────────
|
|
107
|
-
// A downloaded, unstapled notarized onedir pays a one-time Gatekeeper check of
|
|
108
|
-
// all ~330 bundled libraries on first launch (looks frozen on "loading pandas").
|
|
109
|
-
// (1) strip quarantine/provenance xattrs → skips the slow *online* assessment;
|
|
110
|
-
// (2) warm the binary now (during install, when waiting is expected) so the
|
|
111
|
-
// user's first real command is instant. Both are best-effort.
|
|
112
|
-
if (PLATFORM === 'darwin') {
|
|
113
|
-
try { execFileSync('xattr', ['-cr', path.join(VENDOR, 'sphere-cli')], { stdio: 'ignore' }); } catch (_) {}
|
|
114
|
-
try {
|
|
115
|
-
log('warming binary (one-time macOS security scan, ~30–60s) …');
|
|
116
|
-
const warmIn = path.join(VENDOR, '.warm.csv');
|
|
117
|
-
const warmOut = path.join(VENDOR, '.warm_out.csv');
|
|
118
|
-
fs.writeFileSync(warmIn, 'a,b\n1.0,2.0\n3.0,4.0\n5.0,6.0\n');
|
|
119
|
-
execFileSync(BIN, ['generate', warmIn, '-o', warmOut], {
|
|
120
|
-
stdio: 'ignore',
|
|
121
|
-
timeout: 180000,
|
|
122
|
-
env: { ...process.env, SPHERE_LICENSE_REQUIRED: 'false' },
|
|
123
|
-
});
|
|
124
|
-
for (const f of [warmIn, warmOut, warmOut + '.sphere.json']) fs.rmSync(f, { force: true });
|
|
125
|
-
log('binary warmed ✓ — first run will be fast.');
|
|
126
|
-
} catch (_) { /* best-effort; user just pays the scan on first run */ }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
log(`installed sealed binary for ${KEY} ✓`);
|
|
38
|
+
const bin = engine.ensureInstalled(log);
|
|
39
|
+
log(`engine ready at ${bin}`);
|
|
40
|
+
|
|
41
|
+
// Warm once (best-effort): the first run loads ~330 bundled libraries; doing it
|
|
42
|
+
// now (while an install wait is expected) means the user's first real command
|
|
43
|
+
// is fast. On macOS this also clears the one-time Gatekeeper scan.
|
|
44
|
+
try {
|
|
45
|
+
log('warming (first run loads the analysis stack; one-time) …');
|
|
46
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sphere-warm-'));
|
|
47
|
+
const warmIn = path.join(tmp, 'in.csv');
|
|
48
|
+
const warmOut = path.join(tmp, 'out.csv');
|
|
49
|
+
fs.writeFileSync(warmIn, 'a,b\n1.0,2.0\n3.0,4.0\n5.0,6.0\n');
|
|
50
|
+
execFileSync(bin, ['generate', warmIn, '-o', warmOut], {
|
|
51
|
+
stdio: 'ignore',
|
|
52
|
+
timeout: 240000,
|
|
53
|
+
env: { ...process.env, SPHERE_LICENSE_REQUIRED: 'false' },
|
|
54
|
+
});
|
|
55
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
56
|
+
log('warmed ✓ — first run will be fast.');
|
|
57
|
+
} catch (_) { /* best-effort */ }
|
|
58
|
+
|
|
59
|
+
setupGlobalPath();
|
|
60
|
+
log(`installed sealed binary for ${engine.KEY} ✓`);
|
|
130
61
|
} catch (e) {
|
|
131
|
-
|
|
132
|
-
`
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
`sphere-cli: failed to install binary: ${e.message}\n` +
|
|
64
|
+
`Retry: npm rebuild sphere-cli (or set SPHERE_HOME=/path/with/space and retry)\n`,
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Make `sphere` callable without manual PATH editing ──────────────────────
|
|
70
|
+
// For a GLOBAL install whose prefix/bin is not already on PATH (common on HPC),
|
|
71
|
+
// append the bin dir to the user's shell rc. Because the rc lives in the shared
|
|
72
|
+
// home dir, the `sphere` command then works in every future session on every
|
|
73
|
+
// node. Idempotent; skipped when already on PATH; opt out with SPHERE_NO_PATH_SETUP=1.
|
|
74
|
+
function setupGlobalPath() {
|
|
75
|
+
try {
|
|
76
|
+
if (process.env.SPHERE_NO_PATH_SETUP === '1') return;
|
|
77
|
+
if (PLATFORM === 'win32') return;
|
|
78
|
+
const isGlobal = process.env.npm_config_global === 'true'
|
|
79
|
+
|| /[\\/]lib[\\/]node_modules[\\/]sphere-cli$/.test(PKG_ROOT);
|
|
80
|
+
if (!isGlobal) return; // local installs run via `npx sphere` — no PATH needed
|
|
81
|
+
|
|
82
|
+
const prefix = process.env.npm_config_prefix
|
|
83
|
+
|| path.resolve(PKG_ROOT, '..', '..', '..'); // <prefix>/lib/node_modules/sphere-cli
|
|
84
|
+
const binDir = path.join(prefix, 'bin');
|
|
85
|
+
const parts = (process.env.PATH || '').split(path.delimiter);
|
|
86
|
+
if (parts.includes(binDir)) return; // already callable — nothing to do
|
|
87
|
+
|
|
88
|
+
const home = os.homedir();
|
|
89
|
+
const block = `\n# added by sphere-cli — makes the \`sphere\` command available\n`
|
|
90
|
+
+ `export PATH="${binDir}:$PATH"\n`;
|
|
91
|
+
|
|
92
|
+
const targets = [];
|
|
93
|
+
for (const f of ['.bashrc', '.zshrc']) {
|
|
94
|
+
const p = path.join(home, f);
|
|
95
|
+
if (fs.existsSync(p)) targets.push(p);
|
|
96
|
+
}
|
|
97
|
+
if (targets.length === 0) targets.push(path.join(home, '.bashrc')); // create one
|
|
98
|
+
|
|
99
|
+
const wrote = [];
|
|
100
|
+
for (const rc of targets) {
|
|
101
|
+
let cur = '';
|
|
102
|
+
try { cur = fs.readFileSync(rc, 'utf8'); } catch (_) {}
|
|
103
|
+
if (cur.includes(binDir)) continue; // idempotent
|
|
104
|
+
try { fs.appendFileSync(rc, block); wrote.push(path.basename(rc)); } catch (_) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (wrote.length) {
|
|
108
|
+
log(`PATH: added ${binDir} to ${wrote.join(', ')} — \`sphere\` works in new shells (and every node, since home is shared).`);
|
|
109
|
+
log(`for THIS shell now: export PATH="${binDir}:$PATH"`);
|
|
110
|
+
} else {
|
|
111
|
+
log(`to run \`sphere\`, add to your shell rc: export PATH="${binDir}:$PATH"`);
|
|
112
|
+
}
|
|
113
|
+
} catch (_) { /* never fail the install over PATH setup */ }
|
|
133
114
|
}
|