@orb44/cli 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- npx @orb44/cli logout
16
+ npx @orb44/cli@0.1.5 status
17
+ npx @orb44/cli@0.1.5 pulse
18
+ npx @orb44/cli@0.1.5 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.5 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,7 @@ 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
13
 
13
14
  const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
14
15
  const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
@@ -422,25 +423,19 @@ function linkSystemCli(node, script) {
422
423
  fs.writeFileSync(CLI_LINK, body, { mode: 0o755 });
423
424
  }
424
425
 
425
- function satServerDir() {
426
- return path.join(path.dirname(SCRIPT), "..", "server");
427
- }
428
-
429
426
  function installSystemTree() {
430
427
  const lib = "/usr/lib/orb44-sat";
431
- const serverSrc = satServerDir();
428
+ const srcRoot = path.join(path.dirname(SCRIPT), "..");
432
429
  fs.mkdirSync(path.join(lib, "bin"), { recursive: true, mode: 0o755 });
433
430
  fs.mkdirSync(path.join(lib, "server"), { recursive: true, mode: 0o755 });
434
- const copies = [
435
- [SCRIPT, path.join(lib, "bin", "orb44.mjs")],
436
- [path.join(serverSrc, "pulse.mjs"), path.join(lib, "server", "pulse.mjs")],
437
- [path.join(serverSrc, "sat-i18n.mjs"), path.join(lib, "server", "sat-i18n.mjs")],
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")],
440
- ];
441
- for (const [from, to] of copies) {
442
- fs.copyFileSync(from, to);
443
- fs.chmodSync(to, 0o644);
431
+ applyUpdateTree(srcRoot, lib);
432
+ const ver = readCliVersion(SCRIPT);
433
+ if (ver && ver !== "0.0.0") {
434
+ fs.writeFileSync(
435
+ path.join(lib, "package.json"),
436
+ JSON.stringify({ name: CLI_PACKAGE, version: ver, type: "module", private: true }, null, 2) + "\n",
437
+ { mode: 0o644 }
438
+ );
444
439
  }
445
440
  return path.join(lib, "bin", "orb44.mjs");
446
441
  }
@@ -640,6 +635,62 @@ async function cmdUninstall(opts) {
640
635
  console.log(dim(t(lang, "uninstall_key_kept", { file: SYSTEM_DEVICE })));
641
636
  }
642
637
 
638
+ async function cmdUpdate() {
639
+ applyLang(opts);
640
+ const current = readCliVersion(SCRIPT);
641
+ let meta;
642
+ try {
643
+ meta = await fetchLatestMeta();
644
+ } catch (e) {
645
+ console.error("⚠️ " + t(lang, "update_fail", { err: `: ${String(e.message || e).slice(0, 160)}` }));
646
+ process.exit(1);
647
+ }
648
+ 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
+ const roots = pickSatRoots(SCRIPT);
655
+ if (!roots.length) {
656
+ console.error("⚠️ " + t(lang, "update_no_tree"));
657
+ console.log(dim(pin));
658
+ process.exit(1);
659
+ }
660
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "orb44-upd-"));
661
+ let wrote = [];
662
+ try {
663
+ const pkg = await unpackTarball(meta.tarball, tmp);
664
+ const blocked = [];
665
+ for (const root of roots) {
666
+ try {
667
+ applyUpdateTree(pkg, root);
668
+ wrote.push(root);
669
+ } catch (e) {
670
+ blocked.push({ root, err: String(e.message || e).slice(0, 160) });
671
+ }
672
+ }
673
+ if (!wrote.length) {
674
+ 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}` : "" }));
675
+ process.exit(1);
676
+ }
677
+ } finally {
678
+ try {
679
+ fs.rmSync(tmp, { recursive: true, force: true });
680
+ } catch {
681
+ /* tmp */
682
+ }
683
+ }
684
+ for (const root of wrote) {
685
+ console.log("✅ " + t(lang, "update_wrote", { version: meta.version, path: root }));
686
+ }
687
+ if (wrote.includes("/usr/lib/orb44-sat") && serviceIsActive(true)) {
688
+ spawnSync("systemctl", ["try-restart", "orb44-satellite"], { encoding: "utf8" });
689
+ console.log(dim(t(lang, "update_restarted")));
690
+ }
691
+ console.log(dim(pin));
692
+ }
693
+
643
694
  function cmdStatus() {
644
695
  applyLang(opts);
645
696
  const device = loadDevice();
@@ -708,6 +759,7 @@ const run = {
708
759
  install: () => cmdInstall(opts, { enable: true }),
709
760
  uninstall: () => cmdUninstall(opts),
710
761
  status: cmdStatus,
762
+ update: cmdUpdate,
711
763
  logout: cmdLogout,
712
764
  lang: () => cmdLang(opts),
713
765
  help,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orb44/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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,8 @@
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-update.mjs"
15
16
  ],
16
17
  "engines": {
17
18
  "node": ">=18"
package/server/pulse.mjs CHANGED
@@ -174,11 +174,32 @@ 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 groups = new Map();
184
+ for (const r of rows) {
185
+ const key = `${Number(r.port)}|${r.comm || ""}`;
186
+ if (!groups.has(key)) groups.set(key, []);
187
+ groups.get(key).push(r);
188
+ }
189
+ const out = [];
190
+ for (const list of groups.values()) {
191
+ const addrs = new Set(list.map((x) => x.addr));
192
+ const rest = list.filter((x) => x.addr !== "0.0.0.0" && x.addr !== "::" && x.addr !== "*");
193
+ if (addrs.has("*") || (addrs.has("0.0.0.0") && addrs.has("::"))) {
194
+ out.push({ addr: "*", port: list[0].port, comm: list[0].comm });
195
+ out.push(...rest);
196
+ } else {
197
+ out.push(...list);
198
+ }
199
+ }
200
+ return out;
201
+ }
202
+
182
203
  export function sanitizePulse(raw = {}) {
183
204
  const top = Array.isArray(raw.top)
184
205
  ? raw.top
@@ -190,16 +211,18 @@ export function sanitizePulse(raw = {}) {
190
211
  }))
191
212
  .filter((r) => r.comm)
192
213
  : [];
193
- const listen = Array.isArray(raw.listen)
194
- ? raw.listen
195
- .slice(0, 40)
196
- .map((r) => ({
197
- addr: String(r.addr || "").slice(0, 45),
198
- port: Number(r.port) || 0,
199
- comm: sanitizeComm(r.comm),
200
- }))
201
- .filter((r) => r.port > 0 && r.port < 65536 && okListenAddr(r.addr))
202
- : [];
214
+ const listen = collapseListen(
215
+ Array.isArray(raw.listen)
216
+ ? raw.listen
217
+ .slice(0, 40)
218
+ .map((r) => ({
219
+ addr: String(r.addr || "").slice(0, 45),
220
+ port: Number(r.port) || 0,
221
+ comm: sanitizeComm(r.comm),
222
+ }))
223
+ .filter((r) => r.port > 0 && r.port < 65536 && okListenAddr(r.addr))
224
+ : []
225
+ );
203
226
  const originA = Array.isArray(raw.originA)
204
227
  ? raw.originA.map((x) => String(x || "")).filter((x) => /^\d{1,3}(\.\d{1,3}){3}$/.test(x)).slice(0, 8)
205
228
  : [];
@@ -710,9 +733,10 @@ export function formatPulsePreview(pulse, lang = "en") {
710
733
  [t(lang, "preview_row_grade"), grade],
711
734
  ]
712
735
  );
713
- const listenRows = [...(pulse.listen || [])]
736
+ const listenRows = collapseListen([...(pulse.listen || [])])
714
737
  .sort((a, b) => {
715
- const rank = (addr) => (addr === "0.0.0.0" || addr === "::" ? 0 : String(addr).startsWith("127.") || addr === "::1" ? 2 : 1);
738
+ const rank = (addr) =>
739
+ addr === "0.0.0.0" || addr === "::" || addr === "*" ? 0 : String(addr).startsWith("127.") || addr === "::1" ? 2 : 1;
716
740
  return rank(a.addr) - rank(b.addr) || Number(a.port) - Number(b.port);
717
741
  })
718
742
  .slice(0, 16)
@@ -68,6 +68,13 @@ const STR = {
68
68
  already_replace: "Replace — new login, old device is revoked",
69
69
  already_kept: "Left as is. Pulse sent. Same device in the cabinet.",
70
70
  already_no_key: "Daemon is on, but this user cannot read the key. Try: sudo orb44 status",
71
+ update_same: "Already {version}.",
72
+ update_wrote: "Updated to {version} → {path}",
73
+ update_need_root: "Cannot write the install tree. Run: sudo orb44 update",
74
+ update_no_tree: "No satellite install to overwrite. After install: sudo orb44 update. Until then pin npx.",
75
+ update_fail: "Update failed{err}",
76
+ update_npx: "Do not run bare npx @orb44/cli — pin: npx @orb44/cli@{version}",
77
+ update_restarted: "Restarted orb44-satellite.",
71
78
  advice_title: "What to do on this machine",
72
79
  advice_sub: "The cabinet will not run this — a leaked key must not become a remote shell.",
73
80
  preview_host: "host {h}",
@@ -109,6 +116,7 @@ const STR = {
109
116
  orb44 uninstall [--purge]
110
117
  orb44 lang [en|ru|ko|es]
111
118
  orb44 status
119
+ orb44 update
112
120
  orb44 logout
113
121
 
114
122
  Login asks language (saved) then daemon (default no).
@@ -164,6 +172,13 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
164
172
  already_replace: "Заменить — новый login, старое устройство отзовётся",
165
173
  already_kept: "Оставили как есть. Пульс ушёл. В кабинете то же устройство.",
166
174
  already_no_key: "Демон работает, но этот пользователь не читает ключ. Попробуйте: sudo orb44 status",
175
+ update_same: "Уже {version}.",
176
+ update_wrote: "Обновлено до {version} → {path}",
177
+ update_need_root: "Нельзя записать дерево установки. Запустите: sudo orb44 update",
178
+ update_no_tree: "Нет дерева сателлита для перезаписи. После install: sudo orb44 update. До этого — pin в npx.",
179
+ update_fail: "Обновление не вышло{err}",
180
+ update_npx: "Не вызывайте голый npx @orb44/cli — pin: npx @orb44/cli@{version}",
181
+ update_restarted: "Демон orb44-satellite перезапущен.",
167
182
  advice_title: "Что сделать на этой машине",
168
183
  advice_sub: "Кабинет это не выполнит — иначе утечка ключа была бы удалённым шеллом.",
169
184
  preview_host: "хост {h}",
@@ -205,6 +220,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
205
220
  orb44 uninstall [--purge]
206
221
  orb44 lang [en|ru|ko|es]
207
222
  orb44 status
223
+ orb44 update
208
224
  orb44 logout
209
225
 
210
226
  После login спрашивает язык (запоминает), затем демон — по умолчанию нет.
@@ -260,6 +276,13 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
260
276
  already_replace: "교체 — 새 login, 이전 장치는 취소됨",
261
277
  already_kept: "그대로 두었습니다. 펄스를 보냈습니다. 콘솔의 장치는 같습니다.",
262
278
  already_no_key: "데몬은 켜져 있지만 이 사용자는 키를 읽지 못합니다. sudo orb44 status",
279
+ update_same: "이미 {version}.",
280
+ update_wrote: "{version}(으)로 갱신 → {path}",
281
+ update_need_root: "설치 트리를 쓸 수 없습니다. sudo orb44 update",
282
+ update_no_tree: "덮어쓸 위성 설치가 없습니다. install 후 sudo orb44 update. 그 전에는 npx를 pin 하세요.",
283
+ update_fail: "업데이트 실패{err}",
284
+ update_npx: "버전 없는 npx @orb44/cli 는 쓰지 마세요 — pin: npx @orb44/cli@{version}",
285
+ update_restarted: "orb44-satellite 를 재시작했습니다.",
263
286
  advice_title: "이 기기에서 할 일",
264
287
  advice_sub: "콘솔은 실행하지 않습니다. 키 유출이 원격 셸이 되면 안 됩니다.",
265
288
  preview_host: "호스트 {h}",
@@ -301,6 +324,7 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
301
324
  orb44 uninstall [--purge]
302
325
  orb44 lang [en|ru|ko|es]
303
326
  orb44 status
327
+ orb44 update
304
328
  orb44 logout
305
329
 
306
330
  login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니다.
@@ -356,6 +380,13 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
356
380
  already_replace: "Sustituir — login nuevo, se revoca el dispositivo anterior",
357
381
  already_kept: "Se dejó igual. Pulso enviado. El mismo dispositivo en el gabinete.",
358
382
  already_no_key: "El demonio está activo, pero este usuario no lee la clave. Pruebe: sudo orb44 status",
383
+ update_same: "Ya está {version}.",
384
+ update_wrote: "Actualizado a {version} → {path}",
385
+ update_need_root: "No se puede escribir el árbol. Ejecute: sudo orb44 update",
386
+ update_no_tree: "No hay instalación del satélite que sobrescribir. Tras install: sudo orb44 update. Mientras tanto, fije npx.",
387
+ update_fail: "La actualización falló{err}",
388
+ update_npx: "No use npx @orb44/cli sin versión — fije: npx @orb44/cli@{version}",
389
+ update_restarted: "orb44-satellite reiniciado.",
359
390
  advice_title: "Qué hacer en esta máquina",
360
391
  advice_sub: "El gabinete no lo ejecutará: una clave filtrada no debe ser un shell remoto.",
361
392
  preview_host: "host {h}",
@@ -397,6 +428,7 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
397
428
  orb44 uninstall [--purge]
398
429
  orb44 lang [en|ru|ko|es]
399
430
  orb44 status
431
+ orb44 update
400
432
  orb44 logout
401
433
 
402
434
  Login pregunta idioma (lo guarda) y luego demonio (por defecto no).
@@ -0,0 +1,104 @@
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-update.mjs",
13
+ "package.json",
14
+ ];
15
+
16
+ const REGISTRY_LATEST = "https://registry.npmjs.org/@orb44%2fcli/latest";
17
+
18
+ export function cmpVer(a, b) {
19
+ const pa = String(a || "0").split(".").map((n) => Number(n) || 0);
20
+ const pb = String(b || "0").split(".").map((n) => Number(n) || 0);
21
+ const n = Math.max(pa.length, pb.length);
22
+ for (let i = 0; i < n; i++) {
23
+ const d = (pa[i] || 0) - (pb[i] || 0);
24
+ if (d) return d < 0 ? -1 : 1;
25
+ }
26
+ return 0;
27
+ }
28
+
29
+ export function looksLikeSat(root) {
30
+ const r = path.resolve(root);
31
+ if (!fs.existsSync(path.join(r, "bin", "orb44.mjs"))) return false;
32
+ if (!fs.existsSync(path.join(r, "server", "pulse.mjs"))) return false;
33
+ if (fs.existsSync(path.join(r, "server", "index.mjs"))) return false;
34
+ return true;
35
+ }
36
+
37
+ export function readCliVersion(scriptFile) {
38
+ const binDir = path.dirname(path.resolve(scriptFile));
39
+ const root = path.join(binDir, "..");
40
+ const candidates = [
41
+ path.join(root, "package.json"),
42
+ path.join(root, "packages", "orb44", "package.json"),
43
+ ];
44
+ for (const p of candidates) {
45
+ try {
46
+ const j = JSON.parse(fs.readFileSync(p, "utf8"));
47
+ if (j.name === CLI_PACKAGE && j.version) return String(j.version);
48
+ } catch {
49
+ /* next */
50
+ }
51
+ }
52
+ return "0.0.0";
53
+ }
54
+
55
+ export function pickSatRoots(scriptFile, { systemLib = "/usr/lib/orb44-sat" } = {}) {
56
+ const here = path.resolve(path.dirname(scriptFile), "..");
57
+ const out = [];
58
+ if (looksLikeSat(here)) out.push(here);
59
+ const lib = systemLib ? path.resolve(systemLib) : "";
60
+ if (lib && looksLikeSat(lib) && !out.includes(lib)) out.push(lib);
61
+ return out;
62
+ }
63
+
64
+ export function applyUpdateTree(srcPackageDir, destRoot) {
65
+ const src = path.resolve(srcPackageDir);
66
+ const dest = path.resolve(destRoot);
67
+ for (const rel of SAT_TREE_FILES) {
68
+ const from = path.join(src, rel);
69
+ if (!fs.existsSync(from)) {
70
+ if (rel === "package.json") continue;
71
+ throw new Error(`missing ${rel}`);
72
+ }
73
+ const to = path.join(dest, rel);
74
+ fs.mkdirSync(path.dirname(to), { recursive: true });
75
+ fs.copyFileSync(from, to);
76
+ fs.chmodSync(to, rel.endsWith("orb44.mjs") ? 0o755 : 0o644);
77
+ }
78
+ return dest;
79
+ }
80
+
81
+ export async function fetchLatestMeta(fetchFn = fetch) {
82
+ const r = await fetchFn(REGISTRY_LATEST, { headers: { Accept: "application/json" } });
83
+ if (!r?.ok) throw new Error(`registry ${r?.status || "fail"}`);
84
+ const j = await r.json();
85
+ const version = j?.version;
86
+ const tarball = j?.dist?.tarball;
87
+ if (!version || !tarball) throw new Error("registry meta");
88
+ return { version: String(version), tarball: String(tarball) };
89
+ }
90
+
91
+ export async function unpackTarball(url, tmp, fetchFn = fetch) {
92
+ const r = await fetchFn(url);
93
+ if (!r?.ok) throw new Error(`tarball ${r?.status || "fail"}`);
94
+ const buf = Buffer.from(await r.arrayBuffer());
95
+ const tgz = path.join(tmp, "pkg.tgz");
96
+ fs.writeFileSync(tgz, buf);
97
+ const unpack = path.join(tmp, "unpack");
98
+ fs.mkdirSync(unpack, { recursive: true });
99
+ const tar = spawnSync("tar", ["-xzf", tgz, "-C", unpack], { encoding: "utf8" });
100
+ if (tar.status !== 0) throw new Error((tar.stderr || tar.stdout || "tar").trim().slice(0, 200));
101
+ const pkg = path.join(unpack, "package");
102
+ if (!fs.existsSync(path.join(pkg, "bin", "orb44.mjs"))) throw new Error("bad tarball");
103
+ return pkg;
104
+ }