opscope 0.0.0 → 0.3.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 +30 -3
- package/bin/opscope +61 -0
- package/package.json +37 -4
- package/platform.js +270 -0
- package/postinstall.js +32 -0
package/README.md
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
# opscope
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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>
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
[GNU AGPL-3.0](LICENSE). Commercial licenses are available; write to
|
|
31
|
+
[email@wiiiimm.codes](mailto:email@wiiiimm.codes).
|
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.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
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.3.0",
|
|
13
|
+
"opscope-darwin-arm64": "0.3.0",
|
|
14
|
+
"opscope-darwin-x64": "0.3.0"
|
|
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
|
-
"
|
|
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
|
+
}
|