sphere-cli 0.2.7 → 0.2.9
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 +44 -115
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.9",
|
|
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.9';
|
|
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,146 +1,76 @@
|
|
|
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
|
-
log(`downloading ${ASSET} (v${VERSION}) …`);
|
|
82
|
-
curl(`${BASE}/${ASSET}`, tarball);
|
|
83
|
-
|
|
84
|
-
// Verify checksum against the published SHA256SUMS.txt
|
|
85
|
-
const sumsFile = path.join(VENDOR, 'SHA256SUMS.txt');
|
|
86
|
-
curl(`${BASE}/SHA256SUMS.txt`, sumsFile);
|
|
87
|
-
const sums = fs.readFileSync(sumsFile, 'utf8');
|
|
88
|
-
const expected = sums.split('\n')
|
|
89
|
-
.map((l) => l.trim().split(/\s+/))
|
|
90
|
-
.find((p) => p[1] && p[1].endsWith(ASSET));
|
|
91
|
-
if (!expected) fail(`no checksum for ${ASSET} in SHA256SUMS.txt`);
|
|
92
|
-
const got = sha256(tarball);
|
|
93
|
-
if (got !== expected[0]) {
|
|
94
|
-
fail(`checksum mismatch for ${ASSET}\n expected ${expected[0]}\n got ${got}`);
|
|
95
|
-
}
|
|
96
|
-
log('checksum verified ✓');
|
|
97
|
-
|
|
98
|
-
// Extract
|
|
99
|
-
fs.rmSync(path.join(VENDOR, 'sphere-cli'), { recursive: true, force: true });
|
|
100
|
-
execFileSync('tar', ['-xzf', tarball, '-C', VENDOR], { stdio: 'inherit' });
|
|
101
|
-
fs.chmodSync(BIN, 0o755);
|
|
102
|
-
fs.unlinkSync(tarball);
|
|
103
|
-
fs.rmSync(sumsFile, { force: true });
|
|
104
|
-
fs.writeFileSync(STAMP, BINARY_RELEASE);
|
|
38
|
+
const bin = engine.ensureInstalled(log);
|
|
39
|
+
log(`engine ready at ${bin}`);
|
|
105
40
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
}
|
|
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 */ }
|
|
128
58
|
|
|
129
59
|
setupGlobalPath();
|
|
130
|
-
|
|
131
|
-
log(`installed sealed binary for ${KEY} ✓`);
|
|
60
|
+
log(`installed sealed binary for ${engine.KEY} ✓`);
|
|
132
61
|
} catch (e) {
|
|
133
|
-
|
|
134
|
-
`
|
|
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);
|
|
135
67
|
}
|
|
136
68
|
|
|
137
69
|
// ── Make `sphere` callable without manual PATH editing ──────────────────────
|
|
138
|
-
// For a GLOBAL install whose prefix/bin is not already on PATH (common on HPC,
|
|
139
|
-
//
|
|
140
|
-
// the
|
|
141
|
-
//
|
|
142
|
-
// command then works in every future session on every node — no reinstall.
|
|
143
|
-
// Idempotent; skipped when already on PATH; opt out with SPHERE_NO_PATH_SETUP=1.
|
|
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.
|
|
144
74
|
function setupGlobalPath() {
|
|
145
75
|
try {
|
|
146
76
|
if (process.env.SPHERE_NO_PATH_SETUP === '1') return;
|
|
@@ -155,7 +85,6 @@ function setupGlobalPath() {
|
|
|
155
85
|
const parts = (process.env.PATH || '').split(path.delimiter);
|
|
156
86
|
if (parts.includes(binDir)) return; // already callable — nothing to do
|
|
157
87
|
|
|
158
|
-
const os = require('os');
|
|
159
88
|
const home = os.homedir();
|
|
160
89
|
const block = `\n# added by sphere-cli — makes the \`sphere\` command available\n`
|
|
161
90
|
+ `export PATH="${binDir}:$PATH"\n`;
|
|
@@ -176,10 +105,10 @@ function setupGlobalPath() {
|
|
|
176
105
|
}
|
|
177
106
|
|
|
178
107
|
if (wrote.length) {
|
|
179
|
-
log(`PATH: added ${binDir} to ${wrote.join(', ')} — \`sphere\`
|
|
108
|
+
log(`PATH: added ${binDir} to ${wrote.join(', ')} — \`sphere\` works in new shells (and every node, since home is shared).`);
|
|
180
109
|
log(`for THIS shell now: export PATH="${binDir}:$PATH"`);
|
|
181
110
|
} else {
|
|
182
111
|
log(`to run \`sphere\`, add to your shell rc: export PATH="${binDir}:$PATH"`);
|
|
183
112
|
}
|
|
184
|
-
} catch (_) { /*
|
|
113
|
+
} catch (_) { /* never fail the install over PATH setup */ }
|
|
185
114
|
}
|