opscope 0.0.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,26 @@
1
- Placeholder so this package name can attach GitHub Actions trusted publishing.
1
+ # opscope
2
2
 
3
- Real releases are published from https://github.com/stealth-factory/opscope
4
- by `.github/workflows/release.yml`. Do not depend on this version.
3
+ Sci-fi terminal widgets that show only real data.
4
+
5
+ ```sh
6
+ npx opscope
7
+ ```
8
+
9
+ That is the launcher: a menu of the fourteen widgets, or name one and skip
10
+ it (`npx opscope latency`). The binaries it starts are the ones attached
11
+ to the GitHub release of the same version — `--version` on any of them
12
+ answers with that version, commit and date.
13
+
14
+ ## Platforms
15
+
16
+ Linux x86_64 (glibc 2.35 or newer), macOS Apple Silicon, and macOS Intel.
17
+ Windows, Alpine/musl, 32-bit, Linux arm64 and an older glibc fail at
18
+ install with a sentence saying so.
19
+
20
+ The launcher is a thin package. Each platform is an optional dependency
21
+ npm installs only when `os` / `cpu` / `libc` match, so a Mac never
22
+ downloads the Linux binaries.
23
+
24
+ ## Source
25
+
26
+ <https://github.com/stealth-factory/opscope>
package/bin/opscope ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // opscope - small dependency-free terminal widgets
3
+ // Copyright (C) 2026 William Li
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published
7
+ // by the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ 'use strict';
19
+
20
+ const { spawn } = require('child_process');
21
+ const { constants } = require('os');
22
+ const platform = require('../platform');
23
+
24
+ // The native `opscope` binary, from the platform package npm installed
25
+ // beside us. Exec-ing it — rather than reimplementing the menu in JS —
26
+ // is what keeps `--version` answering with the same version, commit
27
+ // and date as the GitHub release: it is the same binary.
28
+ let start;
29
+ try {
30
+ start = platform.resolveStart();
31
+ } catch (err) {
32
+ console.error(err.message);
33
+ process.exit(1);
34
+ }
35
+
36
+ const child = spawn(start, process.argv.slice(2), { stdio: 'inherit' });
37
+
38
+ function forward(signal) {
39
+ if (!child.killed) child.kill(signal);
40
+ }
41
+
42
+ process.on('SIGINT', () => forward('SIGINT'));
43
+ process.on('SIGTERM', () => forward('SIGTERM'));
44
+
45
+ child.on('error', (err) => {
46
+ console.error(err.message);
47
+ process.exit(1);
48
+ });
49
+
50
+ child.on('exit', (code, signal) => {
51
+ if (signal) {
52
+ // Re-raising the signal while this process still has a handler
53
+ // for it re-enters that handler, which then no-ops because the
54
+ // child is already gone, and this process can exit 0. 128+n is
55
+ // what a shell reports when a child dies of that signal.
56
+ const n = constants.signals[signal];
57
+ process.exit(typeof n === 'number' ? 128 + n : 1);
58
+ return;
59
+ }
60
+ process.exit(code ?? 1);
61
+ });
package/package.json CHANGED
@@ -1,12 +1,45 @@
1
1
  {
2
2
  "name": "opscope",
3
- "version": "0.0.0",
4
- "description": "Placeholder so this name can attach GitHub Actions trusted publishing. Real releases ship from stealth-factory/opscope.",
5
- "license": "AGPL-3.0-or-later",
3
+ "version": "0.2.2",
4
+ "description": "Sci-fi terminal widgets that show only real data.",
5
+ "bin": {
6
+ "opscope": "bin/opscope"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node postinstall.js"
10
+ },
11
+ "optionalDependencies": {
12
+ "opscope-linux-x64": "0.2.2",
13
+ "opscope-darwin-arm64": "0.2.2",
14
+ "opscope-darwin-x64": "0.2.2"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "platform.js",
19
+ "postinstall.js",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
6
29
  "repository": {
7
30
  "type": "git",
8
31
  "url": "git+https://github.com/stealth-factory/opscope.git"
9
32
  },
33
+ "bugs": {
34
+ "url": "https://github.com/stealth-factory/opscope/issues"
35
+ },
10
36
  "homepage": "https://github.com/stealth-factory/opscope#readme",
11
- "publishConfig": { "access": "public" }
37
+ "license": "AGPL-3.0-or-later",
38
+ "keywords": [
39
+ "opscope",
40
+ "terminal",
41
+ "tui",
42
+ "widgets",
43
+ "dashboard"
44
+ ]
12
45
  }
package/platform.js ADDED
@@ -0,0 +1,270 @@
1
+ // opscope - small dependency-free terminal widgets
2
+ // Copyright (C) 2026 William Li
3
+ //
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU Affero General Public License as published
6
+ // by the Free Software Foundation, either version 3 of the License, or
7
+ // (at your option) any later version.
8
+ //
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU Affero General Public License for more details.
13
+ //
14
+ // You should have received a copy of the GNU Affero General Public License
15
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // The unscoped name is free, which is why this is `opscope` and
23
+ // not a scoped package. `npx opscope` is the command somebody
24
+ // with only Node is asked to type.
25
+ const LAUNCHER = 'opscope';
26
+
27
+ // One row per artefact the release workflow actually produces. A
28
+ // platform that is only a wish is not a row: Alpine, Windows and
29
+ // Linux arm64 fail at install because they are absent here, which is
30
+ // the sentence the issue asked for rather than a package that
31
+ // installs and then cannot run.
32
+ //
33
+ // `os` / `cpu` / `libc` are npm's own selectors. npm installs only
34
+ // the optionalDependency whose selectors match and skips the rest,
35
+ // so a Mac never downloads the Linux tarball. `rust` is the release
36
+ // tarball suffix, and is how pack.js finds the right artefact.
37
+ const PLATFORMS = [
38
+ {
39
+ pkg: 'opscope-linux-x64',
40
+ os: 'linux',
41
+ cpu: 'x64',
42
+ libc: 'glibc',
43
+ rust: 'x86_64-unknown-linux-gnu',
44
+ label: 'Linux x86_64 (glibc)',
45
+ },
46
+ {
47
+ pkg: 'opscope-darwin-arm64',
48
+ os: 'darwin',
49
+ cpu: 'arm64',
50
+ rust: 'aarch64-apple-darwin',
51
+ label: 'macOS Apple Silicon',
52
+ },
53
+ {
54
+ pkg: 'opscope-darwin-x64',
55
+ os: 'darwin',
56
+ cpu: 'x64',
57
+ rust: 'x86_64-apple-darwin',
58
+ label: 'macOS Intel',
59
+ },
60
+ ];
61
+
62
+ // The Linux binary is built on Ubuntu 22.04. That is glibc 2.35, and
63
+ // the oldest glibc it will load on — a 20.04 machine has 2.31, the
64
+ // package would install, and then the linker would fail with a
65
+ // missing GLIBC_* symbol. npm's `libc` selector cannot see the
66
+ // version, so we compare it here and fail at install the same way
67
+ // we fail for musl.
68
+ const MIN_GLIBC = '2.35';
69
+
70
+ function parseGlibc(version) {
71
+ const m = String(version).trim().match(/^(\d+)\.(\d+)/);
72
+ if (!m) return null;
73
+ return [Number(m[1]), Number(m[2])];
74
+ }
75
+
76
+ function glibcAtLeast(have, need) {
77
+ const a = parseGlibc(have);
78
+ const b = parseGlibc(need);
79
+ if (!a || !b) return false;
80
+ return a[0] > b[0] || (a[0] === b[0] && a[1] >= b[1]);
81
+ }
82
+
83
+ function glibcTooOld(h) {
84
+ return Boolean(h && h.os === 'linux' && h.glibc && !glibcAtLeast(h.glibc, MIN_GLIBC));
85
+ }
86
+
87
+ // The fourteen binaries, read from the manifest rather than restated.
88
+ // A restated list is how a fifteenth widget ships in the tarball and
89
+ // not in the npm package, and the launcher then cannot launch it.
90
+ // `[[bin]]` is the same list `opscope` already asserts against.
91
+ function binsFromManifest(repoRoot) {
92
+ const toml = fs.readFileSync(path.join(repoRoot, 'widgets/Cargo.toml'), 'utf8');
93
+ const bins = [];
94
+ for (const part of toml.split('[[bin]]').slice(1)) {
95
+ const m = part.match(/name = "([a-z0-9-]+)"/);
96
+ if (m) bins.push(m[1]);
97
+ }
98
+ bins.sort();
99
+ if (bins.length === 0) {
100
+ throw new Error('widgets/Cargo.toml named no [[bin]] entries');
101
+ }
102
+ if (!bins.includes('opscope')) {
103
+ throw new Error('widgets/Cargo.toml has no opscope binary; the launcher cannot launch');
104
+ }
105
+ return bins;
106
+ }
107
+
108
+ function linuxRuntime() {
109
+ let glibc;
110
+ try {
111
+ const report = process.report && process.report.getReport && process.report.getReport();
112
+ if (report && report.header && report.header.glibcVersionRuntime) {
113
+ glibc = report.header.glibcVersionRuntime;
114
+ }
115
+ } catch (_) {
116
+ // getReport can throw in some embeddings. Fall through.
117
+ }
118
+ if (glibc) {
119
+ return { libc: 'glibc', glibc };
120
+ }
121
+ try {
122
+ if (
123
+ fs.existsSync('/lib/ld-musl-x86_64.so.1') ||
124
+ fs.existsSync('/lib/ld-musl-aarch64.so.1')
125
+ ) {
126
+ return { libc: 'musl' };
127
+ }
128
+ } catch (_) {
129
+ // A filesystem we cannot read is not evidence of musl.
130
+ }
131
+ // Linux and nothing said musl: the release target is gnu, so this
132
+ // is the match that target is for. A missing version is not proof
133
+ // of an old one — refuse only when we can read it and it is old.
134
+ return { libc: 'glibc' };
135
+ }
136
+
137
+ function detectLibc() {
138
+ if (process.platform !== 'linux') return undefined;
139
+ return linuxRuntime().libc;
140
+ }
141
+
142
+ function host() {
143
+ if (process.platform !== 'linux') {
144
+ return { os: process.platform, cpu: process.arch };
145
+ }
146
+ const linux = linuxRuntime();
147
+ return {
148
+ os: process.platform,
149
+ cpu: process.arch,
150
+ libc: linux.libc,
151
+ glibc: linux.glibc,
152
+ };
153
+ }
154
+
155
+ function describeHost(h) {
156
+ const who = h || host();
157
+ if (glibcTooOld(who)) {
158
+ return `${who.os}-${who.cpu} (glibc ${who.glibc})`;
159
+ }
160
+ const libc = who.libc ? ` (${who.libc})` : '';
161
+ return `${who.os}-${who.cpu}${libc}`;
162
+ }
163
+
164
+ function currentPlatform(h) {
165
+ const who = h || host();
166
+ if (glibcTooOld(who)) return null;
167
+ return (
168
+ PLATFORMS.find(
169
+ (p) =>
170
+ p.os === who.os &&
171
+ p.cpu === who.cpu &&
172
+ (p.libc === undefined || p.libc === who.libc),
173
+ ) || null
174
+ );
175
+ }
176
+
177
+ function publishedPlatforms() {
178
+ return PLATFORMS.map((p) => ` ${p.label}`).join('\n');
179
+ }
180
+
181
+ function unsupportedMessage(h) {
182
+ const who = h || host();
183
+ if (glibcTooOld(who)) {
184
+ return [
185
+ `${LAUNCHER} needs glibc ${MIN_GLIBC} or newer; this machine has ${who.glibc}.`,
186
+ '',
187
+ 'The Linux binaries are built against glibc 2.35. Build from source',
188
+ 'on this machine, or run them where glibc is new enough.',
189
+ '',
190
+ 'It publishes:',
191
+ publishedPlatforms(),
192
+ '',
193
+ 'Build from source: https://github.com/stealth-factory/opscope',
194
+ ].join('\n');
195
+ }
196
+ return [
197
+ `${LAUNCHER} has no binaries for ${describeHost(who)}.`,
198
+ '',
199
+ 'It publishes:',
200
+ publishedPlatforms(),
201
+ '',
202
+ 'Windows, Alpine/musl, 32-bit and Linux arm64 are not published.',
203
+ 'Build from source: https://github.com/stealth-factory/opscope',
204
+ ].join('\n');
205
+ }
206
+
207
+ function missingOptionalMessage(wanted) {
208
+ return [
209
+ `this machine is ${wanted.label}, which is published, but`,
210
+ `${wanted.pkg} did not install.`,
211
+ '',
212
+ 'npm skipped it — --omit=optional, an offline registry, or a',
213
+ 'network error. Retry without omitting optional dependencies,',
214
+ `or install ${wanted.pkg} at this same version.`,
215
+ ].join('\n');
216
+ }
217
+
218
+ function installedPackageDir(pkg) {
219
+ try {
220
+ return path.dirname(require.resolve(`${pkg}/package.json`));
221
+ } catch (_) {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ function resolveStart(h) {
227
+ const wanted = currentPlatform(h);
228
+ if (!wanted) {
229
+ const err = new Error(unsupportedMessage(h));
230
+ err.code = 'UNSUPPORTED_PLATFORM';
231
+ throw err;
232
+ }
233
+ const dir = installedPackageDir(wanted.pkg);
234
+ if (!dir) {
235
+ const err = new Error(missingOptionalMessage(wanted));
236
+ err.code = 'MISSING_OPTIONAL';
237
+ throw err;
238
+ }
239
+ const start = path.join(dir, 'bin', LAUNCHER);
240
+ if (!fs.existsSync(start)) {
241
+ const err = new Error(
242
+ `${wanted.pkg} is installed but bin/${LAUNCHER} is missing. Reinstall the package.`,
243
+ );
244
+ err.code = 'CORRUPT_OPTIONAL';
245
+ throw err;
246
+ }
247
+ return start;
248
+ }
249
+
250
+ function requireInstalled(h) {
251
+ resolveStart(h);
252
+ }
253
+
254
+ module.exports = {
255
+ LAUNCHER,
256
+ PLATFORMS,
257
+ MIN_GLIBC,
258
+ glibcAtLeast,
259
+ binsFromManifest,
260
+ detectLibc,
261
+ host,
262
+ describeHost,
263
+ currentPlatform,
264
+ publishedPlatforms,
265
+ unsupportedMessage,
266
+ missingOptionalMessage,
267
+ installedPackageDir,
268
+ resolveStart,
269
+ requireInstalled,
270
+ };
package/postinstall.js ADDED
@@ -0,0 +1,32 @@
1
+ // opscope - small dependency-free terminal widgets
2
+ // Copyright (C) 2026 William Li
3
+ //
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU Affero General Public License as published
6
+ // by the Free Software Foundation, either version 3 of the License, or
7
+ // (at your option) any later version.
8
+ //
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU Affero General Public License for more details.
13
+ //
14
+ // You should have received a copy of the GNU Affero General Public License
15
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ 'use strict';
18
+
19
+ // Optional dependencies that do not match `os` / `cpu` / `libc` are
20
+ // skipped, and a failed optional fetch is a warning. Either way npm
21
+ // reports the install as successful and leaves a package that cannot
22
+ // run. Failing here is what turns that into a sentence at install
23
+ // time, which is when somebody is still looking.
24
+
25
+ const platform = require('./platform');
26
+
27
+ try {
28
+ platform.requireInstalled();
29
+ } catch (err) {
30
+ console.error(err.message);
31
+ process.exit(1);
32
+ }