@orb44/cli 0.1.4 → 0.1.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/README.md +7 -4
- package/bin/orb44.mjs +95 -40
- package/package.json +4 -2
- package/server/pulse.mjs +43 -13
- package/server/sat-http.mjs +65 -0
- package/server/sat-i18n.mjs +48 -0
- package/server/sat-update.mjs +123 -0
package/README.md
CHANGED
|
@@ -13,10 +13,13 @@ npx @orb44/cli login --url https://orb44.com
|
|
|
13
13
|
Open the printed link in a browser where you are already signed in, confirm the domain, then optionally install the systemd daemon (default no). After a global install the command is `orb44`.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx @orb44/cli status
|
|
17
|
-
npx @orb44/cli pulse
|
|
18
|
-
npx @orb44/cli install # later: background pulse, starts after reboot
|
|
19
|
-
|
|
16
|
+
npx @orb44/cli@0.1.6 status
|
|
17
|
+
npx @orb44/cli@0.1.6 pulse
|
|
18
|
+
npx @orb44/cli@0.1.6 install # later: background pulse, starts after reboot
|
|
19
|
+
orb44 update # after install: replace /usr/lib/orb44-sat from npm
|
|
20
|
+
npx @orb44/cli@0.1.6 logout
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
Do not run bare `npx @orb44/cli` — it can reuse an old cache. Pin the version or use `orb44` after install.
|
|
24
|
+
|
|
22
25
|
Device key: `~/.config/orb44/device.json` (mode 0600). Not a shell token.
|
package/bin/orb44.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { collectPulse, formatPulsePreview } from "../server/pulse.mjs";
|
|
|
9
9
|
import { LANGS, LANG_LABEL, detectLang, normalizeLang, t } from "../server/sat-i18n.mjs";
|
|
10
10
|
import { pickFromList } from "../server/cli-menu.mjs";
|
|
11
11
|
import { SYSTEM_DEVICE, parseDeviceJson, hasExistingInstall, pickLiveDevice, deviceSearchPaths, loadFirstDevice, stripDevicePath } from "../server/sat-local.mjs";
|
|
12
|
+
import { CLI_PACKAGE, cmpVer, readCliVersion, pickSatRoots, staleSatRoots, applyUpdateTree, fetchLatestMeta, unpackTarball } from "../server/sat-update.mjs";
|
|
13
|
+
import { cabinetRequest, apiFailText } from "../server/sat-http.mjs";
|
|
12
14
|
|
|
13
15
|
const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
|
|
14
16
|
const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
|
|
@@ -81,16 +83,12 @@ function clearDevice() {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
body: json ? JSON.stringify(json) : undefined,
|
|
91
|
-
});
|
|
92
|
-
const body = await r.json().catch(() => ({}));
|
|
93
|
-
return { ok: r.ok, status: r.status, body };
|
|
86
|
+
function api(url, pathname, opts) {
|
|
87
|
+
return cabinetRequest(url, pathname, opts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function failLine(out, fallbackKey = "pulse_fail") {
|
|
91
|
+
return "⚠️ " + apiFailText(lang, out, t, fallbackKey);
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
function dim(s) {
|
|
@@ -258,7 +256,7 @@ async function cmdLogin(opts) {
|
|
|
258
256
|
}
|
|
259
257
|
const { out } = await sendPulse(live);
|
|
260
258
|
if (!out.ok) {
|
|
261
|
-
console.error(
|
|
259
|
+
console.error(failLine(out));
|
|
262
260
|
process.exit(1);
|
|
263
261
|
}
|
|
264
262
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -275,7 +273,7 @@ async function cmdLogin(opts) {
|
|
|
275
273
|
json: { hostname, name },
|
|
276
274
|
});
|
|
277
275
|
if (!begin.ok || !begin.body.pollToken) {
|
|
278
|
-
console.error(
|
|
276
|
+
console.error(failLine(begin, "pair_fail"));
|
|
279
277
|
process.exit(1);
|
|
280
278
|
}
|
|
281
279
|
console.log(`🔗 API ${url}`);
|
|
@@ -319,7 +317,7 @@ async function cmdLogin(opts) {
|
|
|
319
317
|
|
|
320
318
|
const { out } = await sendPulse(device);
|
|
321
319
|
if (!out.ok) {
|
|
322
|
-
console.error(
|
|
320
|
+
console.error(failLine(out));
|
|
323
321
|
process.exit(1);
|
|
324
322
|
}
|
|
325
323
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -349,10 +347,7 @@ async function cmdPulse() {
|
|
|
349
347
|
banner();
|
|
350
348
|
const { out } = await sendPulse(device);
|
|
351
349
|
if (!out.ok) {
|
|
352
|
-
console.error(
|
|
353
|
-
"⚠️ " +
|
|
354
|
-
(out.body.error === "bad_secret" || out.body.error === "not_found" ? t(lang, "key_revoked") : out.body.error || t(lang, "pulse_fail"))
|
|
355
|
-
);
|
|
350
|
+
console.error(failLine(out));
|
|
356
351
|
process.exit(1);
|
|
357
352
|
}
|
|
358
353
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -369,14 +364,17 @@ async function cmdDaemon(opts) {
|
|
|
369
364
|
const sec = intervalSec(opts.interval);
|
|
370
365
|
console.log(`🛰️ ${t(lang, "daemon_run", { sec, host: device.host })}`);
|
|
371
366
|
const tick = async () => {
|
|
372
|
-
const { out, pulse } = await sendPulse(device, { preview: false });
|
|
373
367
|
const hh = new Date().toISOString().slice(11, 19);
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
368
|
+
try {
|
|
369
|
+
const { out, pulse } = await sendPulse(device, { preview: false });
|
|
370
|
+
if (!out.ok) {
|
|
371
|
+
console.error(`⚠️ ${hh} ${apiFailText(lang, out, t)}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
console.log(`📡 ${hh} ok load ${pulse.load1} ${pulse.gradeInside || "—"}`);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.error(`⚠️ ${hh} ${String(e?.message || e).split("\n")[0].slice(0, 200)}`);
|
|
378
377
|
}
|
|
379
|
-
console.log(`📡 ${hh} ok load ${pulse.load1} ${pulse.gradeInside || "—"}`);
|
|
380
378
|
};
|
|
381
379
|
await tick();
|
|
382
380
|
const id = setInterval(tick, sec * 1000);
|
|
@@ -422,25 +420,19 @@ function linkSystemCli(node, script) {
|
|
|
422
420
|
fs.writeFileSync(CLI_LINK, body, { mode: 0o755 });
|
|
423
421
|
}
|
|
424
422
|
|
|
425
|
-
function satServerDir() {
|
|
426
|
-
return path.join(path.dirname(SCRIPT), "..", "server");
|
|
427
|
-
}
|
|
428
|
-
|
|
429
423
|
function installSystemTree() {
|
|
430
424
|
const lib = "/usr/lib/orb44-sat";
|
|
431
|
-
const
|
|
425
|
+
const srcRoot = path.join(path.dirname(SCRIPT), "..");
|
|
432
426
|
fs.mkdirSync(path.join(lib, "bin"), { recursive: true, mode: 0o755 });
|
|
433
427
|
fs.mkdirSync(path.join(lib, "server"), { recursive: true, mode: 0o755 });
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
fs.copyFileSync(from, to);
|
|
443
|
-
fs.chmodSync(to, 0o644);
|
|
428
|
+
applyUpdateTree(srcRoot, lib);
|
|
429
|
+
const ver = readCliVersion(SCRIPT);
|
|
430
|
+
if (ver && ver !== "0.0.0") {
|
|
431
|
+
fs.writeFileSync(
|
|
432
|
+
path.join(lib, "package.json"),
|
|
433
|
+
JSON.stringify({ name: CLI_PACKAGE, version: ver, type: "module", private: true }, null, 2) + "\n",
|
|
434
|
+
{ mode: 0o644 }
|
|
435
|
+
);
|
|
444
436
|
}
|
|
445
437
|
return path.join(lib, "bin", "orb44.mjs");
|
|
446
438
|
}
|
|
@@ -640,6 +632,63 @@ async function cmdUninstall(opts) {
|
|
|
640
632
|
console.log(dim(t(lang, "uninstall_key_kept", { file: SYSTEM_DEVICE })));
|
|
641
633
|
}
|
|
642
634
|
|
|
635
|
+
async function cmdUpdate() {
|
|
636
|
+
applyLang(opts);
|
|
637
|
+
const current = readCliVersion(SCRIPT);
|
|
638
|
+
let meta;
|
|
639
|
+
try {
|
|
640
|
+
meta = await fetchLatestMeta();
|
|
641
|
+
} catch (e) {
|
|
642
|
+
console.error("⚠️ " + t(lang, "update_fail", { err: `: ${String(e.message || e).slice(0, 160)}` }));
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
const pin = t(lang, "update_npx", { version: meta.version });
|
|
646
|
+
const roots = pickSatRoots(SCRIPT);
|
|
647
|
+
const stale = staleSatRoots(roots, meta.version, { force: Boolean(opts.force) });
|
|
648
|
+
if (!roots.length) {
|
|
649
|
+
console.error("⚠️ " + t(lang, "update_no_tree"));
|
|
650
|
+
console.log(dim(pin));
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
if (!stale.length) {
|
|
654
|
+
console.log("✅ " + t(lang, "update_same", { version: current || meta.version }));
|
|
655
|
+
console.log(dim(pin));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "orb44-upd-"));
|
|
659
|
+
let wrote = [];
|
|
660
|
+
try {
|
|
661
|
+
const pkg = await unpackTarball(meta.tarball, tmp);
|
|
662
|
+
const blocked = [];
|
|
663
|
+
for (const root of stale) {
|
|
664
|
+
try {
|
|
665
|
+
applyUpdateTree(pkg, root);
|
|
666
|
+
wrote.push(root);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
blocked.push({ root, err: String(e.message || e).slice(0, 160) });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!wrote.length) {
|
|
672
|
+
console.error("⚠️ " + t(lang, blocked.some((b) => /EACCES|permission/i.test(b.err)) ? "update_need_root" : "update_fail", { err: blocked[0]?.err ? `: ${blocked[0].err}` : "" }));
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
} finally {
|
|
676
|
+
try {
|
|
677
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
678
|
+
} catch {
|
|
679
|
+
/* tmp */
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
for (const root of wrote) {
|
|
683
|
+
console.log("✅ " + t(lang, "update_wrote", { version: meta.version, path: root }));
|
|
684
|
+
}
|
|
685
|
+
if (wrote.includes("/usr/lib/orb44-sat") && serviceIsActive(true)) {
|
|
686
|
+
spawnSync("systemctl", ["try-restart", "orb44-satellite"], { encoding: "utf8" });
|
|
687
|
+
console.log(dim(t(lang, "update_restarted")));
|
|
688
|
+
}
|
|
689
|
+
console.log(dim(pin));
|
|
690
|
+
}
|
|
691
|
+
|
|
643
692
|
function cmdStatus() {
|
|
644
693
|
applyLang(opts);
|
|
645
694
|
const device = loadDevice();
|
|
@@ -662,7 +711,7 @@ async function cmdLogout() {
|
|
|
662
711
|
applyLang(opts);
|
|
663
712
|
const device = loadDevice();
|
|
664
713
|
if (device?.secret) {
|
|
665
|
-
await api(device.api, "/api/satellites/logout", { method: "POST", json: { secret: device.secret } });
|
|
714
|
+
await api(device.api, "/api/satellites/logout", { method: "POST", json: { secret: device.secret } }).catch(() => {});
|
|
666
715
|
}
|
|
667
716
|
clearDevice();
|
|
668
717
|
console.log("✅ " + t(lang, "logged_out"));
|
|
@@ -708,6 +757,7 @@ const run = {
|
|
|
708
757
|
install: () => cmdInstall(opts, { enable: true }),
|
|
709
758
|
uninstall: () => cmdUninstall(opts),
|
|
710
759
|
status: cmdStatus,
|
|
760
|
+
update: cmdUpdate,
|
|
711
761
|
logout: cmdLogout,
|
|
712
762
|
lang: () => cmdLang(opts),
|
|
713
763
|
help,
|
|
@@ -717,4 +767,9 @@ if (!fn) {
|
|
|
717
767
|
help();
|
|
718
768
|
process.exit(1);
|
|
719
769
|
}
|
|
720
|
-
|
|
770
|
+
try {
|
|
771
|
+
await fn();
|
|
772
|
+
} catch (e) {
|
|
773
|
+
console.error("⚠️ " + String(e?.message || e).split("\n")[0].slice(0, 200));
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orb44/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Orb44: street snapshot of your site. This CLI is the Watch satellite — pulse from the host (load, listeners, hardening). The cabinet does not run commands on the machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"server/pulse.mjs",
|
|
12
12
|
"server/sat-i18n.mjs",
|
|
13
13
|
"server/cli-menu.mjs",
|
|
14
|
-
"server/sat-local.mjs"
|
|
14
|
+
"server/sat-local.mjs",
|
|
15
|
+
"server/sat-http.mjs",
|
|
16
|
+
"server/sat-update.mjs"
|
|
15
17
|
],
|
|
16
18
|
"engines": {
|
|
17
19
|
"node": ">=18"
|
package/server/pulse.mjs
CHANGED
|
@@ -174,11 +174,38 @@ function topTable() {
|
|
|
174
174
|
|
|
175
175
|
function okListenAddr(a) {
|
|
176
176
|
const s = String(a || "");
|
|
177
|
-
if (s === "0.0.0.0" || s === "::" || s === "::1") return true;
|
|
177
|
+
if (s === "0.0.0.0" || s === "::" || s === "::1" || s === "*") return true;
|
|
178
178
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(s)) return true;
|
|
179
179
|
return /^[0-9a-fA-F:]+$/.test(s) && s.includes(":");
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
export function collapseListen(rows = []) {
|
|
183
|
+
const world = [];
|
|
184
|
+
const rest = [];
|
|
185
|
+
for (const r of rows) {
|
|
186
|
+
if (r.addr === "0.0.0.0" || r.addr === "::" || r.addr === "*") world.push(r);
|
|
187
|
+
else rest.push(r);
|
|
188
|
+
}
|
|
189
|
+
const byPort = new Map();
|
|
190
|
+
for (const r of world) {
|
|
191
|
+
const port = Number(r.port);
|
|
192
|
+
if (!byPort.has(port)) byPort.set(port, []);
|
|
193
|
+
byPort.get(port).push(r);
|
|
194
|
+
}
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const list of byPort.values()) {
|
|
197
|
+
const addrs = new Set(list.map((x) => x.addr));
|
|
198
|
+
const comm = list.find((x) => x.comm)?.comm || list[0].comm;
|
|
199
|
+
if (addrs.has("*") || (addrs.has("0.0.0.0") && addrs.has("::"))) {
|
|
200
|
+
out.push({ addr: "*", port: list[0].port, comm });
|
|
201
|
+
} else {
|
|
202
|
+
out.push(...list);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
out.push(...rest);
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
182
209
|
export function sanitizePulse(raw = {}) {
|
|
183
210
|
const top = Array.isArray(raw.top)
|
|
184
211
|
? raw.top
|
|
@@ -190,16 +217,18 @@ export function sanitizePulse(raw = {}) {
|
|
|
190
217
|
}))
|
|
191
218
|
.filter((r) => r.comm)
|
|
192
219
|
: [];
|
|
193
|
-
const listen =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
220
|
+
const listen = collapseListen(
|
|
221
|
+
Array.isArray(raw.listen)
|
|
222
|
+
? raw.listen
|
|
223
|
+
.slice(0, 40)
|
|
224
|
+
.map((r) => ({
|
|
225
|
+
addr: String(r.addr || "").slice(0, 45),
|
|
226
|
+
port: Number(r.port) || 0,
|
|
227
|
+
comm: sanitizeComm(r.comm),
|
|
228
|
+
}))
|
|
229
|
+
.filter((r) => r.port > 0 && r.port < 65536 && okListenAddr(r.addr))
|
|
230
|
+
: []
|
|
231
|
+
);
|
|
203
232
|
const originA = Array.isArray(raw.originA)
|
|
204
233
|
? raw.originA.map((x) => String(x || "")).filter((x) => /^\d{1,3}(\.\d{1,3}){3}$/.test(x)).slice(0, 8)
|
|
205
234
|
: [];
|
|
@@ -710,9 +739,10 @@ export function formatPulsePreview(pulse, lang = "en") {
|
|
|
710
739
|
[t(lang, "preview_row_grade"), grade],
|
|
711
740
|
]
|
|
712
741
|
);
|
|
713
|
-
const listenRows = [...(pulse.listen || [])]
|
|
742
|
+
const listenRows = collapseListen([...(pulse.listen || [])])
|
|
714
743
|
.sort((a, b) => {
|
|
715
|
-
const rank = (addr) =>
|
|
744
|
+
const rank = (addr) =>
|
|
745
|
+
addr === "0.0.0.0" || addr === "::" || addr === "*" ? 0 : String(addr).startsWith("127.") || addr === "::1" ? 2 : 1;
|
|
716
746
|
return rank(a.addr) - rank(b.addr) || Number(a.port) - Number(b.port);
|
|
717
747
|
})
|
|
718
748
|
.slice(0, 16)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const FETCH_MS = 15_000;
|
|
2
|
+
|
|
3
|
+
const NET_CODES = new Set([
|
|
4
|
+
"ECONNREFUSED",
|
|
5
|
+
"ECONNRESET",
|
|
6
|
+
"ECONNABORTED",
|
|
7
|
+
"EHOSTUNREACH",
|
|
8
|
+
"ENETUNREACH",
|
|
9
|
+
"EPIPE",
|
|
10
|
+
"EAI_AGAIN",
|
|
11
|
+
"ENOTFOUND",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function classifyNetError(err) {
|
|
15
|
+
const cause = err?.cause && typeof err.cause === "object" ? err.cause : err;
|
|
16
|
+
const code = String(cause?.code || err?.code || "");
|
|
17
|
+
const name = String(err?.name || cause?.name || "");
|
|
18
|
+
const msg = `${cause?.message || ""} ${err?.message || ""}`;
|
|
19
|
+
if (name === "TimeoutError" || name === "AbortError" || code === "ABORT_ERR" || /timeout|aborted/i.test(msg)) {
|
|
20
|
+
return "timeout";
|
|
21
|
+
}
|
|
22
|
+
if (/CERT|UNABLE_TO_VERIFY|ERR_TLS|ERR_SSL/i.test(`${code} ${msg}`)) return "tls";
|
|
23
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN" || /getaddrinfo/i.test(msg)) return "dns";
|
|
24
|
+
if (NET_CODES.has(code) || /fetch failed|socket hang up/i.test(msg)) return "unreachable";
|
|
25
|
+
return "unreachable";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cabinetFail(url, err) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
status: 0,
|
|
32
|
+
body: { error: classifyNetError(err), url: String(url || "").replace(/\/$/, "") },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function cabinetRequest(url, pathname, { method = "GET", json, secret, fetchImpl = fetch, timeoutMs = FETCH_MS } = {}) {
|
|
37
|
+
const base = String(url || "").replace(/\/$/, "");
|
|
38
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
39
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
40
|
+
const signal = timeoutMs > 0 && typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
41
|
+
try {
|
|
42
|
+
const r = await fetchImpl(`${base}${pathname}`, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: json ? JSON.stringify(json) : undefined,
|
|
46
|
+
signal,
|
|
47
|
+
});
|
|
48
|
+
const body = await r.json().catch(() => ({}));
|
|
49
|
+
return { ok: Boolean(r.ok), status: r.status, body };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return cabinetFail(base, err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function apiFailText(lang, out, t, fallbackKey = "pulse_fail") {
|
|
56
|
+
const err = out?.body?.error;
|
|
57
|
+
const url = out?.body?.url || "";
|
|
58
|
+
if (err === "unreachable") return t(lang, "cabinet_down", { url });
|
|
59
|
+
if (err === "timeout") return t(lang, "cabinet_timeout", { url });
|
|
60
|
+
if (err === "tls") return t(lang, "cabinet_tls", { url });
|
|
61
|
+
if (err === "dns") return t(lang, "cabinet_dns", { url });
|
|
62
|
+
if (err === "bad_secret" || err === "not_found") return t(lang, "key_revoked");
|
|
63
|
+
if (typeof err === "string" && /^[a-z][a-z0-9_]{0,40}$/.test(err)) return err;
|
|
64
|
+
return t(lang, fallbackKey);
|
|
65
|
+
}
|
package/server/sat-i18n.mjs
CHANGED
|
@@ -35,6 +35,10 @@ const STR = {
|
|
|
35
35
|
payload: "What goes to the cabinet",
|
|
36
36
|
pulse_ok: "Pulse is in the cabinet.",
|
|
37
37
|
pulse_fail: "pulse not accepted",
|
|
38
|
+
cabinet_down: "Cabinet is not reachable ({url}). Start the cabinet or the tunnel to it.",
|
|
39
|
+
cabinet_timeout: "Cabinet did not answer ({url}). Try again later.",
|
|
40
|
+
cabinet_tls: "TLS to the cabinet failed ({url}).",
|
|
41
|
+
cabinet_dns: "Cannot resolve the cabinet host ({url}).",
|
|
38
42
|
pair_fail: "could not start pairing",
|
|
39
43
|
ask_daemon: "Install as a Watch daemon?\nRuns in the background and starts again after reboot. Pulse every 5 min.",
|
|
40
44
|
daemon_no: "No",
|
|
@@ -68,6 +72,13 @@ const STR = {
|
|
|
68
72
|
already_replace: "Replace — new login, old device is revoked",
|
|
69
73
|
already_kept: "Left as is. Pulse sent. Same device in the cabinet.",
|
|
70
74
|
already_no_key: "Daemon is on, but this user cannot read the key. Try: sudo orb44 status",
|
|
75
|
+
update_same: "Already {version}.",
|
|
76
|
+
update_wrote: "Updated to {version} → {path}",
|
|
77
|
+
update_need_root: "Cannot write the install tree. Run: sudo orb44 update",
|
|
78
|
+
update_no_tree: "No satellite install to overwrite. After install: sudo orb44 update. Until then pin npx.",
|
|
79
|
+
update_fail: "Update failed{err}",
|
|
80
|
+
update_npx: "Do not run bare npx @orb44/cli — pin: npx @orb44/cli@{version}",
|
|
81
|
+
update_restarted: "Restarted orb44-satellite.",
|
|
71
82
|
advice_title: "What to do on this machine",
|
|
72
83
|
advice_sub: "The cabinet will not run this — a leaked key must not become a remote shell.",
|
|
73
84
|
preview_host: "host {h}",
|
|
@@ -109,6 +120,7 @@ const STR = {
|
|
|
109
120
|
orb44 uninstall [--purge]
|
|
110
121
|
orb44 lang [en|ru|ko|es]
|
|
111
122
|
orb44 status
|
|
123
|
+
orb44 update
|
|
112
124
|
orb44 logout
|
|
113
125
|
|
|
114
126
|
Login asks language (saved) then daemon (default no).
|
|
@@ -131,6 +143,10 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
131
143
|
payload: "Что уходит в кабинет",
|
|
132
144
|
pulse_ok: "Пульс в кабинете.",
|
|
133
145
|
pulse_fail: "пульс не принят",
|
|
146
|
+
cabinet_down: "Кабинет недоступен ({url}). Поднимите кабинет или туннель к нему.",
|
|
147
|
+
cabinet_timeout: "Кабинет не ответил ({url}). Повторите позже.",
|
|
148
|
+
cabinet_tls: "TLS к кабинету не сошёлся ({url}).",
|
|
149
|
+
cabinet_dns: "Не резолвится хост кабинета ({url}).",
|
|
134
150
|
pair_fail: "не удалось начать пару",
|
|
135
151
|
ask_daemon: "Поставить Watch-демоном?\nБудет работать в фоне и подниматься после перезагрузки. Пульс каждые 5 мин.",
|
|
136
152
|
daemon_no: "Нет",
|
|
@@ -164,6 +180,13 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
164
180
|
already_replace: "Заменить — новый login, старое устройство отзовётся",
|
|
165
181
|
already_kept: "Оставили как есть. Пульс ушёл. В кабинете то же устройство.",
|
|
166
182
|
already_no_key: "Демон работает, но этот пользователь не читает ключ. Попробуйте: sudo orb44 status",
|
|
183
|
+
update_same: "Уже {version}.",
|
|
184
|
+
update_wrote: "Обновлено до {version} → {path}",
|
|
185
|
+
update_need_root: "Нельзя записать дерево установки. Запустите: sudo orb44 update",
|
|
186
|
+
update_no_tree: "Нет дерева сателлита для перезаписи. После install: sudo orb44 update. До этого — pin в npx.",
|
|
187
|
+
update_fail: "Обновление не вышло{err}",
|
|
188
|
+
update_npx: "Не вызывайте голый npx @orb44/cli — pin: npx @orb44/cli@{version}",
|
|
189
|
+
update_restarted: "Демон orb44-satellite перезапущен.",
|
|
167
190
|
advice_title: "Что сделать на этой машине",
|
|
168
191
|
advice_sub: "Кабинет это не выполнит — иначе утечка ключа была бы удалённым шеллом.",
|
|
169
192
|
preview_host: "хост {h}",
|
|
@@ -205,6 +228,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
205
228
|
orb44 uninstall [--purge]
|
|
206
229
|
orb44 lang [en|ru|ko|es]
|
|
207
230
|
orb44 status
|
|
231
|
+
orb44 update
|
|
208
232
|
orb44 logout
|
|
209
233
|
|
|
210
234
|
После login спрашивает язык (запоминает), затем демон — по умолчанию нет.
|
|
@@ -227,6 +251,10 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
227
251
|
payload: "콘솔로 보내는 내용",
|
|
228
252
|
pulse_ok: "펄스가 콘솔에 있습니다.",
|
|
229
253
|
pulse_fail: "펄스 거부",
|
|
254
|
+
cabinet_down: "콘솔에 연결할 수 없습니다 ({url}). 콘솔 또는 터널을 올리세요.",
|
|
255
|
+
cabinet_timeout: "콘솔이 응답하지 않습니다 ({url}). 나중에 다시 시도하세요.",
|
|
256
|
+
cabinet_tls: "콘솔 TLS 실패 ({url}).",
|
|
257
|
+
cabinet_dns: "콘솔 호스트를 찾지 못했습니다 ({url}).",
|
|
230
258
|
pair_fail: "페어링을 시작하지 못했습니다",
|
|
231
259
|
ask_daemon: "Watch 데몬으로 설치할까요?\n백그라운드에서 돌고 재부팅 후에도 올라옵니다. 5분마다 펄스.",
|
|
232
260
|
daemon_no: "아니요",
|
|
@@ -260,6 +288,13 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
260
288
|
already_replace: "교체 — 새 login, 이전 장치는 취소됨",
|
|
261
289
|
already_kept: "그대로 두었습니다. 펄스를 보냈습니다. 콘솔의 장치는 같습니다.",
|
|
262
290
|
already_no_key: "데몬은 켜져 있지만 이 사용자는 키를 읽지 못합니다. sudo orb44 status",
|
|
291
|
+
update_same: "이미 {version}.",
|
|
292
|
+
update_wrote: "{version}(으)로 갱신 → {path}",
|
|
293
|
+
update_need_root: "설치 트리를 쓸 수 없습니다. sudo orb44 update",
|
|
294
|
+
update_no_tree: "덮어쓸 위성 설치가 없습니다. install 후 sudo orb44 update. 그 전에는 npx를 pin 하세요.",
|
|
295
|
+
update_fail: "업데이트 실패{err}",
|
|
296
|
+
update_npx: "버전 없는 npx @orb44/cli 는 쓰지 마세요 — pin: npx @orb44/cli@{version}",
|
|
297
|
+
update_restarted: "orb44-satellite 를 재시작했습니다.",
|
|
263
298
|
advice_title: "이 기기에서 할 일",
|
|
264
299
|
advice_sub: "콘솔은 실행하지 않습니다. 키 유출이 원격 셸이 되면 안 됩니다.",
|
|
265
300
|
preview_host: "호스트 {h}",
|
|
@@ -301,6 +336,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
301
336
|
orb44 uninstall [--purge]
|
|
302
337
|
orb44 lang [en|ru|ko|es]
|
|
303
338
|
orb44 status
|
|
339
|
+
orb44 update
|
|
304
340
|
orb44 logout
|
|
305
341
|
|
|
306
342
|
login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니다.
|
|
@@ -323,6 +359,10 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
323
359
|
payload: "Qué se envía al gabinete",
|
|
324
360
|
pulse_ok: "Pulso en el gabinete.",
|
|
325
361
|
pulse_fail: "pulso rechazado",
|
|
362
|
+
cabinet_down: "El gabinete no responde ({url}). Levante el gabinete o el túnel.",
|
|
363
|
+
cabinet_timeout: "El gabinete no contestó ({url}). Pruebe más tarde.",
|
|
364
|
+
cabinet_tls: "Falló TLS al gabinete ({url}).",
|
|
365
|
+
cabinet_dns: "No se resuelve el host del gabinete ({url}).",
|
|
326
366
|
pair_fail: "no se pudo iniciar el emparejamiento",
|
|
327
367
|
ask_daemon: "¿Instalar como demonio Watch?\nCorre en segundo plano y arranca de nuevo tras el reinicio. Pulso cada 5 min.",
|
|
328
368
|
daemon_no: "No",
|
|
@@ -356,6 +396,13 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
356
396
|
already_replace: "Sustituir — login nuevo, se revoca el dispositivo anterior",
|
|
357
397
|
already_kept: "Se dejó igual. Pulso enviado. El mismo dispositivo en el gabinete.",
|
|
358
398
|
already_no_key: "El demonio está activo, pero este usuario no lee la clave. Pruebe: sudo orb44 status",
|
|
399
|
+
update_same: "Ya está {version}.",
|
|
400
|
+
update_wrote: "Actualizado a {version} → {path}",
|
|
401
|
+
update_need_root: "No se puede escribir el árbol. Ejecute: sudo orb44 update",
|
|
402
|
+
update_no_tree: "No hay instalación del satélite que sobrescribir. Tras install: sudo orb44 update. Mientras tanto, fije npx.",
|
|
403
|
+
update_fail: "La actualización falló{err}",
|
|
404
|
+
update_npx: "No use npx @orb44/cli sin versión — fije: npx @orb44/cli@{version}",
|
|
405
|
+
update_restarted: "orb44-satellite reiniciado.",
|
|
359
406
|
advice_title: "Qué hacer en esta máquina",
|
|
360
407
|
advice_sub: "El gabinete no lo ejecutará: una clave filtrada no debe ser un shell remoto.",
|
|
361
408
|
preview_host: "host {h}",
|
|
@@ -397,6 +444,7 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
397
444
|
orb44 uninstall [--purge]
|
|
398
445
|
orb44 lang [en|ru|ko|es]
|
|
399
446
|
orb44 status
|
|
447
|
+
orb44 update
|
|
400
448
|
orb44 logout
|
|
401
449
|
|
|
402
450
|
Login pregunta idioma (lo guarda) y luego demonio (por defecto no).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const CLI_PACKAGE = "@orb44/cli";
|
|
6
|
+
export const SAT_TREE_FILES = [
|
|
7
|
+
"bin/orb44.mjs",
|
|
8
|
+
"server/pulse.mjs",
|
|
9
|
+
"server/sat-i18n.mjs",
|
|
10
|
+
"server/cli-menu.mjs",
|
|
11
|
+
"server/sat-local.mjs",
|
|
12
|
+
"server/sat-http.mjs",
|
|
13
|
+
"server/sat-update.mjs",
|
|
14
|
+
"package.json",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const REGISTRY_LATEST = "https://registry.npmjs.org/@orb44%2fcli/latest";
|
|
18
|
+
|
|
19
|
+
export function cmpVer(a, b) {
|
|
20
|
+
const pa = String(a || "0").split(".").map((n) => Number(n) || 0);
|
|
21
|
+
const pb = String(b || "0").split(".").map((n) => Number(n) || 0);
|
|
22
|
+
const n = Math.max(pa.length, pb.length);
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
25
|
+
if (d) return d < 0 ? -1 : 1;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function looksLikeSat(root) {
|
|
31
|
+
const r = path.resolve(root);
|
|
32
|
+
if (!fs.existsSync(path.join(r, "bin", "orb44.mjs"))) return false;
|
|
33
|
+
if (!fs.existsSync(path.join(r, "server", "pulse.mjs"))) return false;
|
|
34
|
+
if (fs.existsSync(path.join(r, "server", "index.mjs"))) return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readCliVersion(scriptFile) {
|
|
39
|
+
const binDir = path.dirname(path.resolve(scriptFile));
|
|
40
|
+
const root = path.join(binDir, "..");
|
|
41
|
+
const candidates = [
|
|
42
|
+
path.join(root, "package.json"),
|
|
43
|
+
path.join(root, "packages", "orb44", "package.json"),
|
|
44
|
+
];
|
|
45
|
+
for (const p of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
48
|
+
if (j.name === CLI_PACKAGE && j.version) return String(j.version);
|
|
49
|
+
} catch {
|
|
50
|
+
/* next */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return "0.0.0";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function treeNeedsRefresh(root, latest, { force = false } = {}) {
|
|
57
|
+
if (force) return true;
|
|
58
|
+
const ver = readCliVersion(path.join(root, "bin", "orb44.mjs"));
|
|
59
|
+
if (cmpVer(ver, latest) < 0) return true;
|
|
60
|
+
try {
|
|
61
|
+
const pulse = fs.readFileSync(path.join(root, "server", "pulse.mjs"), "utf8");
|
|
62
|
+
if (!pulse.includes("function collapseListen")) return true;
|
|
63
|
+
if (!fs.existsSync(path.join(root, "server", "sat-http.mjs"))) return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function staleSatRoots(roots, latest, opts = {}) {
|
|
71
|
+
return (roots || []).filter((r) => treeNeedsRefresh(r, latest, opts));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function pickSatRoots(scriptFile, { systemLib = "/usr/lib/orb44-sat" } = {}) {
|
|
75
|
+
const here = path.resolve(path.dirname(scriptFile), "..");
|
|
76
|
+
const out = [];
|
|
77
|
+
if (looksLikeSat(here)) out.push(here);
|
|
78
|
+
const lib = systemLib ? path.resolve(systemLib) : "";
|
|
79
|
+
if (lib && looksLikeSat(lib) && !out.includes(lib)) out.push(lib);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyUpdateTree(srcPackageDir, destRoot) {
|
|
84
|
+
const src = path.resolve(srcPackageDir);
|
|
85
|
+
const dest = path.resolve(destRoot);
|
|
86
|
+
for (const rel of SAT_TREE_FILES) {
|
|
87
|
+
const from = path.join(src, rel);
|
|
88
|
+
if (!fs.existsSync(from)) {
|
|
89
|
+
if (rel === "package.json") continue;
|
|
90
|
+
throw new Error(`missing ${rel}`);
|
|
91
|
+
}
|
|
92
|
+
const to = path.join(dest, rel);
|
|
93
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
94
|
+
fs.copyFileSync(from, to);
|
|
95
|
+
fs.chmodSync(to, rel.endsWith("orb44.mjs") ? 0o755 : 0o644);
|
|
96
|
+
}
|
|
97
|
+
return dest;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function fetchLatestMeta(fetchFn = fetch) {
|
|
101
|
+
const r = await fetchFn(REGISTRY_LATEST, { headers: { Accept: "application/json" } });
|
|
102
|
+
if (!r?.ok) throw new Error(`registry ${r?.status || "fail"}`);
|
|
103
|
+
const j = await r.json();
|
|
104
|
+
const version = j?.version;
|
|
105
|
+
const tarball = j?.dist?.tarball;
|
|
106
|
+
if (!version || !tarball) throw new Error("registry meta");
|
|
107
|
+
return { version: String(version), tarball: String(tarball) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function unpackTarball(url, tmp, fetchFn = fetch) {
|
|
111
|
+
const r = await fetchFn(url);
|
|
112
|
+
if (!r?.ok) throw new Error(`tarball ${r?.status || "fail"}`);
|
|
113
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
114
|
+
const tgz = path.join(tmp, "pkg.tgz");
|
|
115
|
+
fs.writeFileSync(tgz, buf);
|
|
116
|
+
const unpack = path.join(tmp, "unpack");
|
|
117
|
+
fs.mkdirSync(unpack, { recursive: true });
|
|
118
|
+
const tar = spawnSync("tar", ["-xzf", tgz, "-C", unpack], { encoding: "utf8" });
|
|
119
|
+
if (tar.status !== 0) throw new Error((tar.stderr || tar.stdout || "tar").trim().slice(0, 200));
|
|
120
|
+
const pkg = path.join(unpack, "package");
|
|
121
|
+
if (!fs.existsSync(path.join(pkg, "bin", "orb44.mjs"))) throw new Error("bad tarball");
|
|
122
|
+
return pkg;
|
|
123
|
+
}
|