@orb44/cli 0.1.0 → 0.1.4
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 +6 -4
- package/bin/orb44.mjs +172 -13
- package/package.json +7 -5
- package/server/pulse.mjs +63 -26
- package/server/sat-i18n.mjs +132 -8
- package/server/sat-local.mjs +46 -0
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
# orb44
|
|
1
|
+
# @orb44/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Orb44](https://orb44.com) is a street snapshot of your own site: how the shop, payments, and perimeter look from the internet — not a week-long pentest.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This package is the **Watch satellite**. On a verified host it sends a pulse (load, listeners, hardening) to the Orb44 cabinet. The cabinet does not run commands on the machine.
|
|
6
|
+
|
|
7
|
+
Node 18+. Linux or macOS. Product: [orb44.com](https://orb44.com)
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npx @orb44/cli login --url https://
|
|
10
|
+
npx @orb44/cli login --url https://orb44.com
|
|
9
11
|
```
|
|
10
12
|
|
|
11
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`.
|
package/bin/orb44.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
8
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
|
+
import { SYSTEM_DEVICE, parseDeviceJson, hasExistingInstall, pickLiveDevice, deviceSearchPaths, loadFirstDevice, stripDevicePath } from "../server/sat-local.mjs";
|
|
11
12
|
|
|
12
13
|
const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
|
|
13
14
|
const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
|
|
@@ -21,6 +22,8 @@ function args() {
|
|
|
21
22
|
if (a === "--yes" || a === "-y") out.yes = true;
|
|
22
23
|
else if (a === "--system") out.system = true;
|
|
23
24
|
else if (a === "--daemon") out.daemon = true;
|
|
25
|
+
else if (a === "--force") out.force = true;
|
|
26
|
+
else if (a === "--purge") out.purge = true;
|
|
24
27
|
else if (a === "--url" || a === "--code" || a === "--name" || a === "--interval" || a === "--lang") {
|
|
25
28
|
out[a.slice(2)] = argv[++i];
|
|
26
29
|
} else if (!a.startsWith("-")) out._.push(a);
|
|
@@ -57,12 +60,12 @@ function storedOrFlag(opts) {
|
|
|
57
60
|
return normalizeLang(opts.lang) || storedLang() || detectLang();
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function loadDeviceRecord() {
|
|
64
|
+
return loadFirstDevice((p) => fs.readFileSync(p, "utf8"), deviceSearchPaths(DEVICE_FILE));
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
function loadDevice() {
|
|
61
|
-
|
|
62
|
-
return JSON.parse(fs.readFileSync(DEVICE_FILE, "utf8"));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
68
|
+
return stripDevicePath(loadDeviceRecord());
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
function saveDevice(row) {
|
|
@@ -180,14 +183,90 @@ async function sendPulse(device, { preview = true } = {}) {
|
|
|
180
183
|
return { out, pulse };
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
async function wantReplace(found, opts) {
|
|
187
|
+
if (opts.force) return true;
|
|
188
|
+
if (opts.yes || !input.isTTY || !output.isTTY) return false;
|
|
189
|
+
const lines = [t(lang, "already_title")];
|
|
190
|
+
for (const d of found.devices) {
|
|
191
|
+
lines.push(t(lang, "already_host", { host: d.host || "—", name: d.name || "—" }));
|
|
192
|
+
lines.push(t(lang, "already_key", { file: d.path }));
|
|
193
|
+
}
|
|
194
|
+
if (found.daemon) lines.push(t(lang, "already_daemon"));
|
|
195
|
+
else if (found.unit) lines.push(t(lang, "already_unit"));
|
|
196
|
+
const idx = await pickFromList({
|
|
197
|
+
title: lines.join("\n"),
|
|
198
|
+
items: [t(lang, "already_keep"), t(lang, "already_replace")],
|
|
199
|
+
index: 0,
|
|
200
|
+
hint: "↑↓ Enter",
|
|
201
|
+
stdin: input,
|
|
202
|
+
stdout: output,
|
|
203
|
+
});
|
|
204
|
+
return idx === 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function existingOnBox() {
|
|
208
|
+
const paths = [...new Set([DEVICE_FILE, SYSTEM_DEVICE])];
|
|
209
|
+
const devices = [];
|
|
210
|
+
for (const p of paths) {
|
|
211
|
+
try {
|
|
212
|
+
const row = parseDeviceJson(fs.readFileSync(p, "utf8"), p);
|
|
213
|
+
if (row) devices.push(row);
|
|
214
|
+
} catch {
|
|
215
|
+
/* missing or unreadable */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const unit =
|
|
219
|
+
fs.existsSync("/etc/systemd/system/orb44-satellite.service") ||
|
|
220
|
+
fs.existsSync(path.join(os.homedir(), ".config", "systemd", "user", "orb44-satellite.service"));
|
|
221
|
+
let daemon = false;
|
|
222
|
+
try {
|
|
223
|
+
daemon = serviceIsActive(true) || serviceIsActive(false);
|
|
224
|
+
} catch {
|
|
225
|
+
daemon = false;
|
|
226
|
+
}
|
|
227
|
+
return { devices, daemon, unit };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function revokeLocalDevices(devices, fallbackApi) {
|
|
231
|
+
for (const d of devices) {
|
|
232
|
+
const apiBase = d.api || fallbackApi;
|
|
233
|
+
if (!apiBase || !d.secret) continue;
|
|
234
|
+
await api(apiBase, "/api/satellites/logout", { method: "POST", json: { secret: d.secret } }).catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
clearDevice();
|
|
237
|
+
if (SYSTEM_DEVICE !== DEVICE_FILE) {
|
|
238
|
+
try {
|
|
239
|
+
fs.unlinkSync(SYSTEM_DEVICE);
|
|
240
|
+
} catch {
|
|
241
|
+
/* missing */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
183
246
|
async function cmdLogin(opts) {
|
|
184
247
|
await ensureLang(opts);
|
|
185
248
|
banner();
|
|
186
249
|
const url = String(opts.url || process.env.ORB44_API || "http://127.0.0.1:8787").replace(/\/$/, "");
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
250
|
+
const found = existingOnBox();
|
|
251
|
+
if (hasExistingInstall(found)) {
|
|
252
|
+
const replace = await wantReplace(found, opts);
|
|
253
|
+
if (!replace) {
|
|
254
|
+
const live = pickLiveDevice(found.devices, { daemon: found.daemon });
|
|
255
|
+
if (!live?.secret) {
|
|
256
|
+
console.log(t(lang, "already_no_key"));
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
const { out } = await sendPulse(live);
|
|
260
|
+
if (!out.ok) {
|
|
261
|
+
console.error(`⚠️ ${out.body.error || t(lang, "pulse_fail")}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
265
|
+
printAdvice(out.body?.notes);
|
|
266
|
+
console.log(dim("\n" + t(lang, "already_kept")));
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
await revokeLocalDevices(found.devices, url);
|
|
191
270
|
}
|
|
192
271
|
const hostname = os.hostname();
|
|
193
272
|
const name = opts.name || hostname;
|
|
@@ -336,6 +415,13 @@ function systemdUnit({ node, script, deviceFile, interval, user }) {
|
|
|
336
415
|
return `${lines.join("\n")}\n`;
|
|
337
416
|
}
|
|
338
417
|
|
|
418
|
+
const CLI_LINK = "/usr/local/bin/orb44";
|
|
419
|
+
|
|
420
|
+
function linkSystemCli(node, script) {
|
|
421
|
+
const body = `#!/bin/sh\nexec ${quote(node)} ${quote(script)} "$@"\n`;
|
|
422
|
+
fs.writeFileSync(CLI_LINK, body, { mode: 0o755 });
|
|
423
|
+
}
|
|
424
|
+
|
|
339
425
|
function satServerDir() {
|
|
340
426
|
return path.join(path.dirname(SCRIPT), "..", "server");
|
|
341
427
|
}
|
|
@@ -350,6 +436,7 @@ function installSystemTree() {
|
|
|
350
436
|
[path.join(serverSrc, "pulse.mjs"), path.join(lib, "server", "pulse.mjs")],
|
|
351
437
|
[path.join(serverSrc, "sat-i18n.mjs"), path.join(lib, "server", "sat-i18n.mjs")],
|
|
352
438
|
[path.join(serverSrc, "cli-menu.mjs"), path.join(lib, "server", "cli-menu.mjs")],
|
|
439
|
+
[path.join(serverSrc, "sat-local.mjs"), path.join(lib, "server", "sat-local.mjs")],
|
|
353
440
|
];
|
|
354
441
|
for (const [from, to] of copies) {
|
|
355
442
|
fs.copyFileSync(from, to);
|
|
@@ -377,7 +464,9 @@ function ensureSystemServiceAccount(srcDeviceFile) {
|
|
|
377
464
|
}
|
|
378
465
|
}
|
|
379
466
|
fs.mkdirSync(home, { recursive: true, mode: 0o750 });
|
|
380
|
-
|
|
467
|
+
if (path.resolve(srcDeviceFile) !== path.resolve(dest)) {
|
|
468
|
+
fs.copyFileSync(srcDeviceFile, dest);
|
|
469
|
+
}
|
|
381
470
|
fs.chmodSync(dest, 0o600);
|
|
382
471
|
const chown = spawnSync("chown", ["-R", "orb44:orb44", home], { encoding: "utf8" });
|
|
383
472
|
if (chown.status !== 0) {
|
|
@@ -429,18 +518,22 @@ function tryEnable(asSystem) {
|
|
|
429
518
|
|
|
430
519
|
function cmdInstall(opts, { enable = false } = {}) {
|
|
431
520
|
applyLang(opts);
|
|
432
|
-
const
|
|
433
|
-
if (!
|
|
521
|
+
const rec = loadDeviceRecord();
|
|
522
|
+
if (!rec?.secret) {
|
|
434
523
|
console.error("⚠️ " + t(lang, "need_login"));
|
|
435
524
|
process.exit(1);
|
|
436
525
|
}
|
|
526
|
+
const srcPath = rec.path || DEVICE_FILE;
|
|
527
|
+
const device = stripDevicePath(rec);
|
|
528
|
+
if (opts.url) device.api = String(opts.url).replace(/\/$/, "");
|
|
529
|
+
if (srcPath !== DEVICE_FILE) saveDevice(device);
|
|
437
530
|
const asSystem = Boolean(opts.system) || process.getuid?.() === 0;
|
|
438
531
|
const interval = intervalSec(opts.interval);
|
|
439
532
|
let deviceFile = DEVICE_FILE;
|
|
440
533
|
let user = null;
|
|
441
534
|
let script = SCRIPT;
|
|
442
535
|
if (asSystem) {
|
|
443
|
-
const acct = ensureSystemServiceAccount(
|
|
536
|
+
const acct = ensureSystemServiceAccount(srcPath);
|
|
444
537
|
if (acct.ok) {
|
|
445
538
|
deviceFile = acct.deviceFile;
|
|
446
539
|
user = acct.user;
|
|
@@ -463,6 +556,14 @@ function cmdInstall(opts, { enable = false } = {}) {
|
|
|
463
556
|
fs.mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
464
557
|
fs.writeFileSync(unitPath, unit, { mode: 0o644 });
|
|
465
558
|
console.log("⚙️ " + t(lang, "unit_written", { path: unitPath }));
|
|
559
|
+
if (asSystem) {
|
|
560
|
+
try {
|
|
561
|
+
linkSystemCli(process.execPath, script);
|
|
562
|
+
console.log(dim(t(lang, "cli_link", { path: CLI_LINK })));
|
|
563
|
+
} catch {
|
|
564
|
+
/* /usr/local/bin missing */
|
|
565
|
+
}
|
|
566
|
+
}
|
|
466
567
|
if (enable) {
|
|
467
568
|
const on = tryEnable(asSystem);
|
|
468
569
|
if (on.ok) {
|
|
@@ -482,6 +583,63 @@ function cmdInstall(opts, { enable = false } = {}) {
|
|
|
482
583
|
return false;
|
|
483
584
|
}
|
|
484
585
|
|
|
586
|
+
function tryDisable(asSystem) {
|
|
587
|
+
const args = asSystem
|
|
588
|
+
? [
|
|
589
|
+
["disable", "--now", "orb44-satellite"],
|
|
590
|
+
["daemon-reload"],
|
|
591
|
+
]
|
|
592
|
+
: [
|
|
593
|
+
["--user", "disable", "--now", "orb44-satellite"],
|
|
594
|
+
["--user", "daemon-reload"],
|
|
595
|
+
];
|
|
596
|
+
for (const a of args) {
|
|
597
|
+
spawnSync("systemctl", a, { encoding: "utf8" });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function rmTree(p) {
|
|
602
|
+
try {
|
|
603
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
604
|
+
} catch {
|
|
605
|
+
/* missing */
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function cmdUninstall(opts) {
|
|
610
|
+
applyLang(opts);
|
|
611
|
+
const asSystem = Boolean(opts.system) || process.getuid?.() === 0;
|
|
612
|
+
tryDisable(asSystem);
|
|
613
|
+
tryDisable(!asSystem);
|
|
614
|
+
const unitPaths = [
|
|
615
|
+
"/etc/systemd/system/orb44-satellite.service",
|
|
616
|
+
path.join(os.homedir(), ".config", "systemd", "user", "orb44-satellite.service"),
|
|
617
|
+
];
|
|
618
|
+
for (const p of unitPaths) {
|
|
619
|
+
try {
|
|
620
|
+
fs.unlinkSync(p);
|
|
621
|
+
} catch {
|
|
622
|
+
/* missing */
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
rmTree("/usr/lib/orb44-sat");
|
|
626
|
+
try {
|
|
627
|
+
fs.unlinkSync(CLI_LINK);
|
|
628
|
+
} catch {
|
|
629
|
+
/* missing */
|
|
630
|
+
}
|
|
631
|
+
if (opts.purge) {
|
|
632
|
+
const found = existingOnBox();
|
|
633
|
+
await revokeLocalDevices(found.devices, found.devices[0]?.api);
|
|
634
|
+
rmTree("/var/lib/orb44");
|
|
635
|
+
spawnSync("userdel", ["orb44"], { encoding: "utf8" });
|
|
636
|
+
console.log("✅ " + t(lang, "uninstall_purged"));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
console.log("✅ " + t(lang, "uninstall_ok"));
|
|
640
|
+
console.log(dim(t(lang, "uninstall_key_kept", { file: SYSTEM_DEVICE })));
|
|
641
|
+
}
|
|
642
|
+
|
|
485
643
|
function cmdStatus() {
|
|
486
644
|
applyLang(opts);
|
|
487
645
|
const device = loadDevice();
|
|
@@ -548,6 +706,7 @@ const run = {
|
|
|
548
706
|
pulse: cmdPulse,
|
|
549
707
|
daemon: () => cmdDaemon(opts),
|
|
550
708
|
install: () => cmdInstall(opts, { enable: true }),
|
|
709
|
+
uninstall: () => cmdUninstall(opts),
|
|
551
710
|
status: cmdStatus,
|
|
552
711
|
logout: cmdLogout,
|
|
553
712
|
lang: () => cmdLang(opts),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orb44/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Orb44 Watch satellite
|
|
3
|
+
"version": "0.1.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": {
|
|
7
7
|
"orb44": "bin/orb44.mjs"
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"bin/orb44.mjs",
|
|
11
11
|
"server/pulse.mjs",
|
|
12
12
|
"server/sat-i18n.mjs",
|
|
13
|
-
"server/cli-menu.mjs"
|
|
13
|
+
"server/cli-menu.mjs",
|
|
14
|
+
"server/sat-local.mjs"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=18"
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
"url": "git+https://github.com/ephemeral172/orb44.git",
|
|
21
22
|
"directory": "packages/orb44"
|
|
22
23
|
},
|
|
23
|
-
"homepage": "https://
|
|
24
|
+
"homepage": "https://orb44.com",
|
|
24
25
|
"bugs": {
|
|
25
26
|
"url": "https://github.com/ephemeral172/orb44/issues"
|
|
26
27
|
},
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
"orb44",
|
|
29
30
|
"watch",
|
|
30
31
|
"satellite",
|
|
31
|
-
"pulse"
|
|
32
|
+
"pulse",
|
|
33
|
+
"perimeter"
|
|
32
34
|
],
|
|
33
35
|
"license": "MIT",
|
|
34
36
|
"publishConfig": {
|
package/server/pulse.mjs
CHANGED
|
@@ -672,33 +672,70 @@ export function collectPulse() {
|
|
|
672
672
|
});
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
+
function clip(s, n) {
|
|
676
|
+
const t = String(s ?? "");
|
|
677
|
+
if (t.length <= n) return t;
|
|
678
|
+
return `${t.slice(0, Math.max(1, n - 1))}…`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function asciiTable(headers, rows) {
|
|
682
|
+
const cells = [headers, ...rows].map((r) => r.map((c) => String(c ?? "")));
|
|
683
|
+
const widths = headers.map((_, i) => Math.max(1, ...cells.map((r) => [...(r[i] || "")].length)));
|
|
684
|
+
const fill = (ch) => widths.map((w) => ch.repeat(w + 2)).join("┼");
|
|
685
|
+
const line = (row) =>
|
|
686
|
+
"│ " +
|
|
687
|
+
row
|
|
688
|
+
.map((c, i) => {
|
|
689
|
+
const s = clip(c, widths[i]);
|
|
690
|
+
return s + " ".repeat(Math.max(0, widths[i] - [...s].length));
|
|
691
|
+
})
|
|
692
|
+
.join(" │ ") +
|
|
693
|
+
" │";
|
|
694
|
+
return [`┌${fill("─").replaceAll("┼", "┬")}┐`, line(headers), `├${fill("─")}┤`, ...rows.map(line), `└${fill("─").replaceAll("┼", "┴")}┘`].join("\n");
|
|
695
|
+
}
|
|
696
|
+
|
|
675
697
|
export function formatPulsePreview(pulse, lang = "en") {
|
|
676
698
|
const gb = (n) => (Number(n) / 1024 / 1024 / 1024).toFixed(1);
|
|
677
699
|
const none = t(lang, "preview_none");
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
700
|
+
const h = pulse.hardening || {};
|
|
701
|
+
const grade = pulse.gradeInside || gradeInside(pulse) || "—";
|
|
702
|
+
const ssh = h.sshPassword ? t(lang, "preview_ssh_pass") : h.sshPassword === false ? t(lang, "preview_ssh_key") : "—";
|
|
703
|
+
const machine = asciiTable(
|
|
704
|
+
[t(lang, "preview_col_check"), t(lang, "preview_col_value")],
|
|
705
|
+
[
|
|
706
|
+
[t(lang, "preview_row_host"), pulse.hostname || "—"],
|
|
707
|
+
[t(lang, "preview_row_load"), Number(pulse.load1).toFixed(2)],
|
|
708
|
+
[t(lang, "preview_row_ram"), `${gb(pulse.memUsed)} / ${gb(pulse.memTotal)}`],
|
|
709
|
+
[t(lang, "preview_row_disk"), `${pulse.diskUsedPct ?? "—"}%`],
|
|
710
|
+
[t(lang, "preview_row_grade"), grade],
|
|
711
|
+
]
|
|
712
|
+
);
|
|
713
|
+
const listenRows = [...(pulse.listen || [])]
|
|
714
|
+
.sort((a, b) => {
|
|
715
|
+
const rank = (addr) => (addr === "0.0.0.0" || addr === "::" ? 0 : String(addr).startsWith("127.") || addr === "::1" ? 2 : 1);
|
|
716
|
+
return rank(a.addr) - rank(b.addr) || Number(a.port) - Number(b.port);
|
|
717
|
+
})
|
|
718
|
+
.slice(0, 16)
|
|
719
|
+
.map((r) => [r.addr || "—", String(r.port ?? ""), r.comm || "—"]);
|
|
720
|
+
const listen = asciiTable(
|
|
721
|
+
[t(lang, "preview_col_bind"), t(lang, "preview_col_port"), t(lang, "preview_col_proc")],
|
|
722
|
+
listenRows.length ? listenRows : [[none, "", ""]]
|
|
723
|
+
);
|
|
724
|
+
const topRows = (pulse.top || []).slice(0, 6).map((r) => [r.comm || "—", `${r.cpuPct}%`, `${r.rssMb}M`]);
|
|
725
|
+
const top = asciiTable(
|
|
726
|
+
[t(lang, "preview_col_proc"), t(lang, "preview_col_cpu"), t(lang, "preview_col_rss")],
|
|
727
|
+
topRows.length ? topRows : [[none, "", ""]]
|
|
728
|
+
);
|
|
729
|
+
const guard = asciiTable(
|
|
730
|
+
[t(lang, "preview_col_check"), t(lang, "preview_col_value")],
|
|
731
|
+
[
|
|
732
|
+
[t(lang, "preview_row_fw"), h.firewall || t(lang, "preview_fw_no")],
|
|
733
|
+
[t(lang, "preview_row_ban"), h.fail2ban || t(lang, "preview_ban_no")],
|
|
734
|
+
[t(lang, "preview_row_upd"), h.updates ? t(lang, "preview_yes") : t(lang, "preview_no")],
|
|
735
|
+
[t(lang, "preview_row_sync"), h.timesync || t(lang, "preview_no")],
|
|
736
|
+
[t(lang, "preview_row_ssh"), ssh],
|
|
737
|
+
]
|
|
738
|
+
);
|
|
739
|
+
const extra = pulse.limited ? `\n${t(lang, "preview_limited")}` : "";
|
|
740
|
+
return `${machine}\n\n${listen}\n\n${top}\n\n${guard}${extra}`;
|
|
704
741
|
}
|
package/server/sat-i18n.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export function detectLang(env = process.env) {
|
|
|
21
21
|
const STR = {
|
|
22
22
|
en: {
|
|
23
23
|
banner_title: "Orb44 satellite",
|
|
24
|
-
banner_sub: "
|
|
24
|
+
banner_sub: "Inside analysis of this host: load, listeners, hardening.",
|
|
25
25
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
26
26
|
lang_saved: "Language saved: {label}",
|
|
27
27
|
lang_now: "CLI language: {label} (--lang en|ru|ko|es)",
|
|
@@ -45,6 +45,7 @@ const STR = {
|
|
|
45
45
|
daemon_run: "daemon every {sec}s · {host}",
|
|
46
46
|
daemon_revoked: "key revoked",
|
|
47
47
|
unit_written: "unit {path}",
|
|
48
|
+
cli_link: "command {path}",
|
|
48
49
|
daemon_on: "daemon enabled: background pulse, starts again after reboot.",
|
|
49
50
|
login_done: "Done. You can close this session — the daemon is already in the background.",
|
|
50
51
|
daemon_user: "service user {user} · key {file}",
|
|
@@ -55,6 +56,18 @@ const STR = {
|
|
|
55
56
|
not_paired: "Not paired. orb44 login",
|
|
56
57
|
now_on_box: "On the machine now",
|
|
57
58
|
logged_out: "Disconnected. Cabinet dropped the device.",
|
|
59
|
+
uninstall_ok: "Daemon stopped and unit removed. Pairing key kept.",
|
|
60
|
+
uninstall_purged: "Daemon, key, and service user orb44 removed. Cabinet dropped the device.",
|
|
61
|
+
uninstall_key_kept: "Key still at {file}. Later: npx @orb44/cli install",
|
|
62
|
+
already_title: "Already on this machine",
|
|
63
|
+
already_host: "{host} ({name})",
|
|
64
|
+
already_key: "key {file}",
|
|
65
|
+
already_daemon: "Watch daemon is already running",
|
|
66
|
+
already_unit: "systemd unit is already installed",
|
|
67
|
+
already_keep: "Keep — do not pair again",
|
|
68
|
+
already_replace: "Replace — new login, old device is revoked",
|
|
69
|
+
already_kept: "Left as is. Pulse sent. Same device in the cabinet.",
|
|
70
|
+
already_no_key: "Daemon is on, but this user cannot read the key. Try: sudo orb44 status",
|
|
58
71
|
advice_title: "What to do on this machine",
|
|
59
72
|
advice_sub: "The cabinet will not run this — a leaked key must not become a remote shell.",
|
|
60
73
|
preview_host: "host {h}",
|
|
@@ -62,6 +75,23 @@ const STR = {
|
|
|
62
75
|
preview_listen: "listening: {list}",
|
|
63
76
|
preview_top: "top: {list}",
|
|
64
77
|
preview_none: "not visible",
|
|
78
|
+
preview_col_check: "check",
|
|
79
|
+
preview_col_value: "value",
|
|
80
|
+
preview_col_bind: "bind",
|
|
81
|
+
preview_col_port: "port",
|
|
82
|
+
preview_col_proc: "process",
|
|
83
|
+
preview_col_cpu: "CPU",
|
|
84
|
+
preview_col_rss: "RSS",
|
|
85
|
+
preview_row_host: "host",
|
|
86
|
+
preview_row_load: "load",
|
|
87
|
+
preview_row_ram: "RAM",
|
|
88
|
+
preview_row_disk: "disk",
|
|
89
|
+
preview_row_grade: "grade",
|
|
90
|
+
preview_row_fw: "firewall",
|
|
91
|
+
preview_row_ban: "fail2ban",
|
|
92
|
+
preview_row_upd: "updates",
|
|
93
|
+
preview_row_sync: "time",
|
|
94
|
+
preview_row_ssh: "SSH",
|
|
65
95
|
preview_guard: "guard: firewall {fw} · {ban} · unattended-upgrades {upd} · time {sync} · SSH {ssh} · grade {grade}",
|
|
66
96
|
preview_fw_no: "none",
|
|
67
97
|
preview_ban_no: "no fail2ban",
|
|
@@ -72,10 +102,11 @@ const STR = {
|
|
|
72
102
|
preview_limited: "limited: cannot see which process owns the ports",
|
|
73
103
|
help: `Orb44 satellite — admin device.
|
|
74
104
|
|
|
75
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
105
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
76
106
|
orb44 pulse
|
|
77
107
|
orb44 daemon [--interval 300]
|
|
78
108
|
orb44 install [--system] [--interval 300]
|
|
109
|
+
orb44 uninstall [--purge]
|
|
79
110
|
orb44 lang [en|ru|ko|es]
|
|
80
111
|
orb44 status
|
|
81
112
|
orb44 logout
|
|
@@ -86,7 +117,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
86
117
|
},
|
|
87
118
|
ru: {
|
|
88
119
|
banner_title: "Orb44 сателлит",
|
|
89
|
-
banner_sub: "
|
|
120
|
+
banner_sub: "Анализ хоста изнутри: нагрузка, слушатели, hardening.",
|
|
90
121
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
91
122
|
lang_saved: "Язык сохранён: {label}",
|
|
92
123
|
lang_now: "Язык консоли: {label} (--lang en|ru|ko|es)",
|
|
@@ -110,6 +141,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
110
141
|
daemon_run: "демон каждые {sec}с · {host}",
|
|
111
142
|
daemon_revoked: "ключ отозван",
|
|
112
143
|
unit_written: "unit {path}",
|
|
144
|
+
cli_link: "command {path}",
|
|
113
145
|
daemon_on: "демон включён: пульс в фоне, поднимается после перезагрузки.",
|
|
114
146
|
login_done: "Готово. Эту сессию можно закрыть — демон уже в фоне.",
|
|
115
147
|
daemon_user: "пользователь службы {user} · ключ {file}",
|
|
@@ -120,6 +152,18 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
120
152
|
not_paired: "Не связано. orb44 login",
|
|
121
153
|
now_on_box: "Сейчас на машине",
|
|
122
154
|
logged_out: "Отключено. В кабинете устройство снято.",
|
|
155
|
+
uninstall_ok: "Демон остановлен, unit снят. Ключ пары оставлен.",
|
|
156
|
+
uninstall_purged: "Демон, ключ и пользователь orb44 сняты. В кабинете устройство отозвано.",
|
|
157
|
+
uninstall_key_kept: "Ключ на месте: {file}. Потом: npx @orb44/cli install",
|
|
158
|
+
already_title: "На этой машине уже стоит",
|
|
159
|
+
already_host: "{host} ({name})",
|
|
160
|
+
already_key: "ключ {file}",
|
|
161
|
+
already_daemon: "Демон Watch уже работает",
|
|
162
|
+
already_unit: "systemd unit уже установлен",
|
|
163
|
+
already_keep: "Оставить — не логиниться заново",
|
|
164
|
+
already_replace: "Заменить — новый login, старое устройство отзовётся",
|
|
165
|
+
already_kept: "Оставили как есть. Пульс ушёл. В кабинете то же устройство.",
|
|
166
|
+
already_no_key: "Демон работает, но этот пользователь не читает ключ. Попробуйте: sudo orb44 status",
|
|
123
167
|
advice_title: "Что сделать на этой машине",
|
|
124
168
|
advice_sub: "Кабинет это не выполнит — иначе утечка ключа была бы удалённым шеллом.",
|
|
125
169
|
preview_host: "хост {h}",
|
|
@@ -127,6 +171,23 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
127
171
|
preview_listen: "слушает: {list}",
|
|
128
172
|
preview_top: "топ: {list}",
|
|
129
173
|
preview_none: "не видно",
|
|
174
|
+
preview_col_check: "проверка",
|
|
175
|
+
preview_col_value: "значение",
|
|
176
|
+
preview_col_bind: "адрес",
|
|
177
|
+
preview_col_port: "порт",
|
|
178
|
+
preview_col_proc: "процесс",
|
|
179
|
+
preview_col_cpu: "CPU",
|
|
180
|
+
preview_col_rss: "RSS",
|
|
181
|
+
preview_row_host: "хост",
|
|
182
|
+
preview_row_load: "load",
|
|
183
|
+
preview_row_ram: "RAM",
|
|
184
|
+
preview_row_disk: "диск",
|
|
185
|
+
preview_row_grade: "грейд",
|
|
186
|
+
preview_row_fw: "файрвол",
|
|
187
|
+
preview_row_ban: "fail2ban",
|
|
188
|
+
preview_row_upd: "обновления",
|
|
189
|
+
preview_row_sync: "время",
|
|
190
|
+
preview_row_ssh: "SSH",
|
|
130
191
|
preview_guard: "защита: файрвол {fw} · {ban} · автообновления {upd} · время {sync} · SSH {ssh} · грейд {grade}",
|
|
131
192
|
preview_fw_no: "нет",
|
|
132
193
|
preview_ban_no: "fail2ban нет",
|
|
@@ -137,10 +198,11 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
137
198
|
preview_limited: "ограничено: не видно, какой процесс слушает порты",
|
|
138
199
|
help: `Orb44 сателлит — устройство админа.
|
|
139
200
|
|
|
140
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
201
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
141
202
|
orb44 pulse
|
|
142
203
|
orb44 daemon [--interval 300]
|
|
143
204
|
orb44 install [--system] [--interval 300]
|
|
205
|
+
orb44 uninstall [--purge]
|
|
144
206
|
orb44 lang [en|ru|ko|es]
|
|
145
207
|
orb44 status
|
|
146
208
|
orb44 logout
|
|
@@ -151,7 +213,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
151
213
|
},
|
|
152
214
|
ko: {
|
|
153
215
|
banner_title: "Orb44 위성",
|
|
154
|
-
banner_sub: "
|
|
216
|
+
banner_sub: "이 호스트를 안에서 분석합니다: 부하, 리스너, hardening.",
|
|
155
217
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
156
218
|
lang_saved: "언어 저장됨: {label}",
|
|
157
219
|
lang_now: "CLI 언어: {label} (--lang en|ru|ko|es)",
|
|
@@ -175,6 +237,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
175
237
|
daemon_run: "데몬 {sec}초마다 · {host}",
|
|
176
238
|
daemon_revoked: "키 취소됨",
|
|
177
239
|
unit_written: "unit {path}",
|
|
240
|
+
cli_link: "command {path}",
|
|
178
241
|
daemon_on: "데몬이 켜졌습니다: 백그라운드 펄스, 재부팅 후에도 올라옵니다.",
|
|
179
242
|
login_done: "끝났습니다. 이 세션을 닫아도 됩니다 — 데몬은 이미 백그라운드에 있습니다.",
|
|
180
243
|
daemon_user: "서비스 사용자 {user} · 키 {file}",
|
|
@@ -185,6 +248,18 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
185
248
|
not_paired: "연결되지 않음. orb44 login",
|
|
186
249
|
now_on_box: "지금 이 기기",
|
|
187
250
|
logged_out: "해제됨. 콘솔에서 장치가 제거되었습니다.",
|
|
251
|
+
uninstall_ok: "데몬을 멈추고 unit을 제거했습니다. 페어링 키는 남겼습니다.",
|
|
252
|
+
uninstall_purged: "데몬, 키, 사용자 orb44를 제거했습니다. 콘솔에서 장치를 취소했습니다.",
|
|
253
|
+
uninstall_key_kept: "키는 그대로입니다: {file}. 나중에: npx @orb44/cli install",
|
|
254
|
+
already_title: "이 기기에 이미 있습니다",
|
|
255
|
+
already_host: "{host} ({name})",
|
|
256
|
+
already_key: "키 {file}",
|
|
257
|
+
already_daemon: "Watch 데몬이 이미 실행 중입니다",
|
|
258
|
+
already_unit: "systemd unit이 이미 설치되어 있습니다",
|
|
259
|
+
already_keep: "유지 — 다시 페어링하지 않음",
|
|
260
|
+
already_replace: "교체 — 새 login, 이전 장치는 취소됨",
|
|
261
|
+
already_kept: "그대로 두었습니다. 펄스를 보냈습니다. 콘솔의 장치는 같습니다.",
|
|
262
|
+
already_no_key: "데몬은 켜져 있지만 이 사용자는 키를 읽지 못합니다. sudo orb44 status",
|
|
188
263
|
advice_title: "이 기기에서 할 일",
|
|
189
264
|
advice_sub: "콘솔은 실행하지 않습니다. 키 유출이 원격 셸이 되면 안 됩니다.",
|
|
190
265
|
preview_host: "호스트 {h}",
|
|
@@ -192,6 +267,23 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
192
267
|
preview_listen: "수신: {list}",
|
|
193
268
|
preview_top: "top: {list}",
|
|
194
269
|
preview_none: "없음",
|
|
270
|
+
preview_col_check: "항목",
|
|
271
|
+
preview_col_value: "값",
|
|
272
|
+
preview_col_bind: "주소",
|
|
273
|
+
preview_col_port: "포트",
|
|
274
|
+
preview_col_proc: "프로세스",
|
|
275
|
+
preview_col_cpu: "CPU",
|
|
276
|
+
preview_col_rss: "RSS",
|
|
277
|
+
preview_row_host: "호스트",
|
|
278
|
+
preview_row_load: "load",
|
|
279
|
+
preview_row_ram: "RAM",
|
|
280
|
+
preview_row_disk: "디스크",
|
|
281
|
+
preview_row_grade: "등급",
|
|
282
|
+
preview_row_fw: "방화벽",
|
|
283
|
+
preview_row_ban: "fail2ban",
|
|
284
|
+
preview_row_upd: "업데이트",
|
|
285
|
+
preview_row_sync: "시간",
|
|
286
|
+
preview_row_ssh: "SSH",
|
|
195
287
|
preview_guard: "보호: 방화벽 {fw} · {ban} · 자동업데이트 {upd} · 시간 {sync} · SSH {ssh} · 등급 {grade}",
|
|
196
288
|
preview_fw_no: "없음",
|
|
197
289
|
preview_ban_no: "fail2ban 없음",
|
|
@@ -202,10 +294,11 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
202
294
|
preview_limited: "제한: 포트를 연 프로세스를 볼 수 없음",
|
|
203
295
|
help: `Orb44 위성 — 관리자 장치.
|
|
204
296
|
|
|
205
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
297
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
206
298
|
orb44 pulse
|
|
207
299
|
orb44 daemon [--interval 300]
|
|
208
300
|
orb44 install [--system] [--interval 300]
|
|
301
|
+
orb44 uninstall [--purge]
|
|
209
302
|
orb44 lang [en|ru|ko|es]
|
|
210
303
|
orb44 status
|
|
211
304
|
orb44 logout
|
|
@@ -216,7 +309,7 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
216
309
|
},
|
|
217
310
|
es: {
|
|
218
311
|
banner_title: "Satélite Orb44",
|
|
219
|
-
banner_sub: "
|
|
312
|
+
banner_sub: "Análisis del host desde dentro: carga, listeners, hardening.",
|
|
220
313
|
lang_pick: "Language · Язык · 언어 · Idioma",
|
|
221
314
|
lang_saved: "Idioma guardado: {label}",
|
|
222
315
|
lang_now: "Idioma de la consola: {label} (--lang en|ru|ko|es)",
|
|
@@ -240,6 +333,7 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
240
333
|
daemon_run: "demonio cada {sec}s · {host}",
|
|
241
334
|
daemon_revoked: "clave revocada",
|
|
242
335
|
unit_written: "unit {path}",
|
|
336
|
+
cli_link: "command {path}",
|
|
243
337
|
daemon_on: "demonio activado: pulso en segundo plano, arranca de nuevo tras el reinicio.",
|
|
244
338
|
login_done: "Listo. Puede cerrar esta sesión: el demonio ya está en segundo plano.",
|
|
245
339
|
daemon_user: "usuario del servicio {user} · clave {file}",
|
|
@@ -250,6 +344,18 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
250
344
|
not_paired: "Sin vincular. orb44 login",
|
|
251
345
|
now_on_box: "Ahora en la máquina",
|
|
252
346
|
logged_out: "Desconectado. El gabinete quitó el dispositivo.",
|
|
347
|
+
uninstall_ok: "Demonio parado y unit quitado. Se guarda la clave de emparejamiento.",
|
|
348
|
+
uninstall_purged: "Demonio, clave y usuario orb44 eliminados. El gabinete revocó el dispositivo.",
|
|
349
|
+
uninstall_key_kept: "La clave sigue en {file}. Luego: npx @orb44/cli install",
|
|
350
|
+
already_title: "Ya está en esta máquina",
|
|
351
|
+
already_host: "{host} ({name})",
|
|
352
|
+
already_key: "clave {file}",
|
|
353
|
+
already_daemon: "El demonio Watch ya está en marcha",
|
|
354
|
+
already_unit: "El unit systemd ya está instalado",
|
|
355
|
+
already_keep: "Dejar — no volver a emparejar",
|
|
356
|
+
already_replace: "Sustituir — login nuevo, se revoca el dispositivo anterior",
|
|
357
|
+
already_kept: "Se dejó igual. Pulso enviado. El mismo dispositivo en el gabinete.",
|
|
358
|
+
already_no_key: "El demonio está activo, pero este usuario no lee la clave. Pruebe: sudo orb44 status",
|
|
253
359
|
advice_title: "Qué hacer en esta máquina",
|
|
254
360
|
advice_sub: "El gabinete no lo ejecutará: una clave filtrada no debe ser un shell remoto.",
|
|
255
361
|
preview_host: "host {h}",
|
|
@@ -257,6 +363,23 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
257
363
|
preview_listen: "escucha: {list}",
|
|
258
364
|
preview_top: "top: {list}",
|
|
259
365
|
preview_none: "no se ve",
|
|
366
|
+
preview_col_check: "dato",
|
|
367
|
+
preview_col_value: "valor",
|
|
368
|
+
preview_col_bind: "bind",
|
|
369
|
+
preview_col_port: "puerto",
|
|
370
|
+
preview_col_proc: "proceso",
|
|
371
|
+
preview_col_cpu: "CPU",
|
|
372
|
+
preview_col_rss: "RSS",
|
|
373
|
+
preview_row_host: "host",
|
|
374
|
+
preview_row_load: "load",
|
|
375
|
+
preview_row_ram: "RAM",
|
|
376
|
+
preview_row_disk: "disco",
|
|
377
|
+
preview_row_grade: "grado",
|
|
378
|
+
preview_row_fw: "cortafuegos",
|
|
379
|
+
preview_row_ban: "fail2ban",
|
|
380
|
+
preview_row_upd: "updates",
|
|
381
|
+
preview_row_sync: "hora",
|
|
382
|
+
preview_row_ssh: "SSH",
|
|
260
383
|
preview_guard: "defensa: cortafuegos {fw} · {ban} · actualizaciones {upd} · hora {sync} · SSH {ssh} · grado {grade}",
|
|
261
384
|
preview_fw_no: "no",
|
|
262
385
|
preview_ban_no: "sin fail2ban",
|
|
@@ -267,10 +390,11 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
267
390
|
preview_limited: "limitado: no se ve qué proceso tiene los puertos",
|
|
268
391
|
help: `Satélite Orb44 — dispositivo de admin.
|
|
269
392
|
|
|
270
|
-
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--lang en|ru|ko|es]
|
|
393
|
+
orb44 login [--url http://127.0.0.1:8787] [--daemon] [--force] [--lang en|ru|ko|es]
|
|
271
394
|
orb44 pulse
|
|
272
395
|
orb44 daemon [--interval 300]
|
|
273
396
|
orb44 install [--system] [--interval 300]
|
|
397
|
+
orb44 uninstall [--purge]
|
|
274
398
|
orb44 lang [en|ru|ko|es]
|
|
275
399
|
orb44 status
|
|
276
400
|
orb44 logout
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const SYSTEM_DEVICE = "/var/lib/orb44/device.json";
|
|
2
|
+
|
|
3
|
+
export function parseDeviceJson(raw, filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const row = JSON.parse(raw);
|
|
6
|
+
if (!row?.secret) return null;
|
|
7
|
+
return { ...row, path: filePath };
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deviceSearchPaths(homeFile, systemdPath = SYSTEM_DEVICE) {
|
|
14
|
+
return [...new Set([homeFile, systemdPath].filter(Boolean))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadFirstDevice(readFile, paths) {
|
|
18
|
+
for (const p of paths) {
|
|
19
|
+
try {
|
|
20
|
+
const row = parseDeviceJson(readFile(p), p);
|
|
21
|
+
if (row) return row;
|
|
22
|
+
} catch {
|
|
23
|
+
/* missing or unreadable */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stripDevicePath(row) {
|
|
30
|
+
if (!row) return null;
|
|
31
|
+
const { path: _p, ...rest } = row;
|
|
32
|
+
return rest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasExistingInstall({ devices = [], daemon = false, unit = false } = {}) {
|
|
36
|
+
return Boolean((devices && devices.length) || daemon || unit);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pickLiveDevice(devices, { daemon = false, systemdPath = SYSTEM_DEVICE } = {}) {
|
|
40
|
+
const list = Array.isArray(devices) ? devices.filter((d) => d?.secret) : [];
|
|
41
|
+
if (daemon) {
|
|
42
|
+
const sys = list.find((d) => d.path === systemdPath);
|
|
43
|
+
if (sys) return sys;
|
|
44
|
+
}
|
|
45
|
+
return list[0] || null;
|
|
46
|
+
}
|