@orb44/cli 0.1.5 → 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 +4 -4
- package/bin/orb44.mjs +35 -32
- package/package.json +2 -1
- package/server/pulse.mjs +14 -8
- package/server/sat-http.mjs +65 -0
- package/server/sat-i18n.mjs +16 -0
- package/server/sat-update.mjs +19 -0
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ 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@0.1.
|
|
17
|
-
npx @orb44/cli@0.1.
|
|
18
|
-
npx @orb44/cli@0.1.
|
|
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
19
|
orb44 update # after install: replace /usr/lib/orb44-sat from npm
|
|
20
|
-
npx @orb44/cli@0.1.
|
|
20
|
+
npx @orb44/cli@0.1.6 logout
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Do not run bare `npx @orb44/cli` — it can reuse an old cache. Pin the version or use `orb44` after install.
|
package/bin/orb44.mjs
CHANGED
|
@@ -9,7 +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, applyUpdateTree, fetchLatestMeta, unpackTarball } from "../server/sat-update.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";
|
|
13
14
|
|
|
14
15
|
const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
|
|
15
16
|
const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
|
|
@@ -82,16 +83,12 @@ function clearDevice() {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
body: json ? JSON.stringify(json) : undefined,
|
|
92
|
-
});
|
|
93
|
-
const body = await r.json().catch(() => ({}));
|
|
94
|
-
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);
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
function dim(s) {
|
|
@@ -259,7 +256,7 @@ async function cmdLogin(opts) {
|
|
|
259
256
|
}
|
|
260
257
|
const { out } = await sendPulse(live);
|
|
261
258
|
if (!out.ok) {
|
|
262
|
-
console.error(
|
|
259
|
+
console.error(failLine(out));
|
|
263
260
|
process.exit(1);
|
|
264
261
|
}
|
|
265
262
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -276,7 +273,7 @@ async function cmdLogin(opts) {
|
|
|
276
273
|
json: { hostname, name },
|
|
277
274
|
});
|
|
278
275
|
if (!begin.ok || !begin.body.pollToken) {
|
|
279
|
-
console.error(
|
|
276
|
+
console.error(failLine(begin, "pair_fail"));
|
|
280
277
|
process.exit(1);
|
|
281
278
|
}
|
|
282
279
|
console.log(`🔗 API ${url}`);
|
|
@@ -320,7 +317,7 @@ async function cmdLogin(opts) {
|
|
|
320
317
|
|
|
321
318
|
const { out } = await sendPulse(device);
|
|
322
319
|
if (!out.ok) {
|
|
323
|
-
console.error(
|
|
320
|
+
console.error(failLine(out));
|
|
324
321
|
process.exit(1);
|
|
325
322
|
}
|
|
326
323
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -350,10 +347,7 @@ async function cmdPulse() {
|
|
|
350
347
|
banner();
|
|
351
348
|
const { out } = await sendPulse(device);
|
|
352
349
|
if (!out.ok) {
|
|
353
|
-
console.error(
|
|
354
|
-
"⚠️ " +
|
|
355
|
-
(out.body.error === "bad_secret" || out.body.error === "not_found" ? t(lang, "key_revoked") : out.body.error || t(lang, "pulse_fail"))
|
|
356
|
-
);
|
|
350
|
+
console.error(failLine(out));
|
|
357
351
|
process.exit(1);
|
|
358
352
|
}
|
|
359
353
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -370,14 +364,17 @@ async function cmdDaemon(opts) {
|
|
|
370
364
|
const sec = intervalSec(opts.interval);
|
|
371
365
|
console.log(`🛰️ ${t(lang, "daemon_run", { sec, host: device.host })}`);
|
|
372
366
|
const tick = async () => {
|
|
373
|
-
const { out, pulse } = await sendPulse(device, { preview: false });
|
|
374
367
|
const hh = new Date().toISOString().slice(11, 19);
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
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)}`);
|
|
379
377
|
}
|
|
380
|
-
console.log(`📡 ${hh} ok load ${pulse.load1} ${pulse.gradeInside || "—"}`);
|
|
381
378
|
};
|
|
382
379
|
await tick();
|
|
383
380
|
const id = setInterval(tick, sec * 1000);
|
|
@@ -646,23 +643,24 @@ async function cmdUpdate() {
|
|
|
646
643
|
process.exit(1);
|
|
647
644
|
}
|
|
648
645
|
const pin = t(lang, "update_npx", { version: meta.version });
|
|
649
|
-
if (cmpVer(current, meta.version) >= 0 && !opts.force) {
|
|
650
|
-
console.log("✅ " + t(lang, "update_same", { version: current }));
|
|
651
|
-
console.log(dim(pin));
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
646
|
const roots = pickSatRoots(SCRIPT);
|
|
647
|
+
const stale = staleSatRoots(roots, meta.version, { force: Boolean(opts.force) });
|
|
655
648
|
if (!roots.length) {
|
|
656
649
|
console.error("⚠️ " + t(lang, "update_no_tree"));
|
|
657
650
|
console.log(dim(pin));
|
|
658
651
|
process.exit(1);
|
|
659
652
|
}
|
|
653
|
+
if (!stale.length) {
|
|
654
|
+
console.log("✅ " + t(lang, "update_same", { version: current || meta.version }));
|
|
655
|
+
console.log(dim(pin));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
660
658
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "orb44-upd-"));
|
|
661
659
|
let wrote = [];
|
|
662
660
|
try {
|
|
663
661
|
const pkg = await unpackTarball(meta.tarball, tmp);
|
|
664
662
|
const blocked = [];
|
|
665
|
-
for (const root of
|
|
663
|
+
for (const root of stale) {
|
|
666
664
|
try {
|
|
667
665
|
applyUpdateTree(pkg, root);
|
|
668
666
|
wrote.push(root);
|
|
@@ -713,7 +711,7 @@ async function cmdLogout() {
|
|
|
713
711
|
applyLang(opts);
|
|
714
712
|
const device = loadDevice();
|
|
715
713
|
if (device?.secret) {
|
|
716
|
-
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(() => {});
|
|
717
715
|
}
|
|
718
716
|
clearDevice();
|
|
719
717
|
console.log("✅ " + t(lang, "logged_out"));
|
|
@@ -769,4 +767,9 @@ if (!fn) {
|
|
|
769
767
|
help();
|
|
770
768
|
process.exit(1);
|
|
771
769
|
}
|
|
772
|
-
|
|
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": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"server/sat-i18n.mjs",
|
|
13
13
|
"server/cli-menu.mjs",
|
|
14
14
|
"server/sat-local.mjs",
|
|
15
|
+
"server/sat-http.mjs",
|
|
15
16
|
"server/sat-update.mjs"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
package/server/pulse.mjs
CHANGED
|
@@ -180,23 +180,29 @@ function okListenAddr(a) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export function collapseListen(rows = []) {
|
|
183
|
-
const
|
|
183
|
+
const world = [];
|
|
184
|
+
const rest = [];
|
|
184
185
|
for (const r of rows) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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);
|
|
188
194
|
}
|
|
189
195
|
const out = [];
|
|
190
|
-
for (const list of
|
|
196
|
+
for (const list of byPort.values()) {
|
|
191
197
|
const addrs = new Set(list.map((x) => x.addr));
|
|
192
|
-
const
|
|
198
|
+
const comm = list.find((x) => x.comm)?.comm || list[0].comm;
|
|
193
199
|
if (addrs.has("*") || (addrs.has("0.0.0.0") && addrs.has("::"))) {
|
|
194
|
-
out.push({ addr: "*", port: list[0].port, comm
|
|
195
|
-
out.push(...rest);
|
|
200
|
+
out.push({ addr: "*", port: list[0].port, comm });
|
|
196
201
|
} else {
|
|
197
202
|
out.push(...list);
|
|
198
203
|
}
|
|
199
204
|
}
|
|
205
|
+
out.push(...rest);
|
|
200
206
|
return out;
|
|
201
207
|
}
|
|
202
208
|
|
|
@@ -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",
|
|
@@ -139,6 +143,10 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
139
143
|
payload: "Что уходит в кабинет",
|
|
140
144
|
pulse_ok: "Пульс в кабинете.",
|
|
141
145
|
pulse_fail: "пульс не принят",
|
|
146
|
+
cabinet_down: "Кабинет недоступен ({url}). Поднимите кабинет или туннель к нему.",
|
|
147
|
+
cabinet_timeout: "Кабинет не ответил ({url}). Повторите позже.",
|
|
148
|
+
cabinet_tls: "TLS к кабинету не сошёлся ({url}).",
|
|
149
|
+
cabinet_dns: "Не резолвится хост кабинета ({url}).",
|
|
142
150
|
pair_fail: "не удалось начать пару",
|
|
143
151
|
ask_daemon: "Поставить Watch-демоном?\nБудет работать в фоне и подниматься после перезагрузки. Пульс каждые 5 мин.",
|
|
144
152
|
daemon_no: "Нет",
|
|
@@ -243,6 +251,10 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
243
251
|
payload: "콘솔로 보내는 내용",
|
|
244
252
|
pulse_ok: "펄스가 콘솔에 있습니다.",
|
|
245
253
|
pulse_fail: "펄스 거부",
|
|
254
|
+
cabinet_down: "콘솔에 연결할 수 없습니다 ({url}). 콘솔 또는 터널을 올리세요.",
|
|
255
|
+
cabinet_timeout: "콘솔이 응답하지 않습니다 ({url}). 나중에 다시 시도하세요.",
|
|
256
|
+
cabinet_tls: "콘솔 TLS 실패 ({url}).",
|
|
257
|
+
cabinet_dns: "콘솔 호스트를 찾지 못했습니다 ({url}).",
|
|
246
258
|
pair_fail: "페어링을 시작하지 못했습니다",
|
|
247
259
|
ask_daemon: "Watch 데몬으로 설치할까요?\n백그라운드에서 돌고 재부팅 후에도 올라옵니다. 5분마다 펄스.",
|
|
248
260
|
daemon_no: "아니요",
|
|
@@ -347,6 +359,10 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
347
359
|
payload: "Qué se envía al gabinete",
|
|
348
360
|
pulse_ok: "Pulso en el gabinete.",
|
|
349
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}).",
|
|
350
366
|
pair_fail: "no se pudo iniciar el emparejamiento",
|
|
351
367
|
ask_daemon: "¿Instalar como demonio Watch?\nCorre en segundo plano y arranca de nuevo tras el reinicio. Pulso cada 5 min.",
|
|
352
368
|
daemon_no: "No",
|
package/server/sat-update.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export const SAT_TREE_FILES = [
|
|
|
9
9
|
"server/sat-i18n.mjs",
|
|
10
10
|
"server/cli-menu.mjs",
|
|
11
11
|
"server/sat-local.mjs",
|
|
12
|
+
"server/sat-http.mjs",
|
|
12
13
|
"server/sat-update.mjs",
|
|
13
14
|
"package.json",
|
|
14
15
|
];
|
|
@@ -52,6 +53,24 @@ export function readCliVersion(scriptFile) {
|
|
|
52
53
|
return "0.0.0";
|
|
53
54
|
}
|
|
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
|
+
|
|
55
74
|
export function pickSatRoots(scriptFile, { systemLib = "/usr/lib/orb44-sat" } = {}) {
|
|
56
75
|
const here = path.resolve(path.dirname(scriptFile), "..");
|
|
57
76
|
const out = [];
|