@orb44/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # orb44
2
+
3
+ Watch satellite for [Orb44](https://github.com/ephemeral172/orb44). Sends a pulse (load, listeners, hardening) to the cabinet. The cabinet does not run commands on the host.
4
+
5
+ Node 18+. Linux or macOS.
6
+
7
+ ```bash
8
+ npx @orb44/cli login --url https://your-orb44
9
+ ```
10
+
11
+ 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`.
12
+
13
+ ```bash
14
+ npx @orb44/cli status
15
+ npx @orb44/cli pulse
16
+ npx @orb44/cli install # later: background pulse, starts after reboot
17
+ npx @orb44/cli logout
18
+ ```
19
+
20
+ Device key: `~/.config/orb44/device.json` (mode 0600). Not a shell token.
package/bin/orb44.mjs ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+ import os from "node:os";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+ import { collectPulse, formatPulsePreview } from "../server/pulse.mjs";
9
+ import { LANGS, LANG_LABEL, detectLang, normalizeLang, t } from "../server/sat-i18n.mjs";
10
+ import { pickFromList } from "../server/cli-menu.mjs";
11
+
12
+ const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
13
+ const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
14
+ const SCRIPT = fileURLToPath(import.meta.url);
15
+
16
+ function args() {
17
+ const out = { _: [] };
18
+ const argv = process.argv.slice(2);
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const a = argv[i];
21
+ if (a === "--yes" || a === "-y") out.yes = true;
22
+ else if (a === "--system") out.system = true;
23
+ else if (a === "--daemon") out.daemon = true;
24
+ else if (a === "--url" || a === "--code" || a === "--name" || a === "--interval" || a === "--lang") {
25
+ out[a.slice(2)] = argv[++i];
26
+ } else if (!a.startsWith("-")) out._.push(a);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ function intervalSec(raw) {
32
+ const n = Number(raw);
33
+ if (!Number.isFinite(n)) return 300;
34
+ return Math.max(60, Math.min(1800, Math.round(n)));
35
+ }
36
+
37
+ function loadCli() {
38
+ try {
39
+ return JSON.parse(fs.readFileSync(CLI_FILE, "utf8"));
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ function saveCli(row) {
46
+ fs.mkdirSync(path.dirname(CLI_FILE), { recursive: true });
47
+ fs.writeFileSync(CLI_FILE, JSON.stringify(row, null, 2), { mode: 0o600 });
48
+ }
49
+
50
+ function storedLang() {
51
+ return normalizeLang(loadCli().lang);
52
+ }
53
+
54
+ let lang = detectLang();
55
+
56
+ function storedOrFlag(opts) {
57
+ return normalizeLang(opts.lang) || storedLang() || detectLang();
58
+ }
59
+
60
+ function loadDevice() {
61
+ try {
62
+ return JSON.parse(fs.readFileSync(DEVICE_FILE, "utf8"));
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function saveDevice(row) {
69
+ fs.mkdirSync(path.dirname(DEVICE_FILE), { recursive: true });
70
+ fs.writeFileSync(DEVICE_FILE, JSON.stringify(row, null, 2), { mode: 0o600 });
71
+ }
72
+
73
+ function clearDevice() {
74
+ try {
75
+ fs.unlinkSync(DEVICE_FILE);
76
+ } catch {
77
+ /* missing */
78
+ }
79
+ }
80
+
81
+ async function api(url, pathname, { method = "GET", json, secret } = {}) {
82
+ const headers = { "Content-Type": "application/json", Accept: "application/json" };
83
+ if (secret) headers.Authorization = `Bearer ${secret}`;
84
+ const r = await fetch(`${url.replace(/\/$/, "")}${pathname}`, {
85
+ method,
86
+ headers,
87
+ body: json ? JSON.stringify(json) : undefined,
88
+ });
89
+ const body = await r.json().catch(() => ({}));
90
+ return { ok: r.ok, status: r.status, body };
91
+ }
92
+
93
+ function dim(s) {
94
+ return `\x1b[2m${s}\x1b[0m`;
95
+ }
96
+
97
+ function bold(s) {
98
+ return `\x1b[1m${s}\x1b[0m`;
99
+ }
100
+
101
+ function quote(p) {
102
+ const s = String(p);
103
+ return /[\s"$]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
104
+ }
105
+
106
+ function banner() {
107
+ console.log(`\nšŸ›°ļø ${bold(t(lang, "banner_title"))}`);
108
+ console.log(dim(`${t(lang, "banner_sub")}\n`));
109
+ }
110
+
111
+ async function ensureLang(opts) {
112
+ const flagged = normalizeLang(opts.lang);
113
+ if (flagged) {
114
+ lang = flagged;
115
+ saveCli({ ...loadCli(), lang });
116
+ return lang;
117
+ }
118
+ const saved = storedLang();
119
+ if (saved) {
120
+ lang = saved;
121
+ return lang;
122
+ }
123
+ lang = detectLang();
124
+ if (opts.yes || !input.isTTY || !output.isTTY) {
125
+ saveCli({ ...loadCli(), lang });
126
+ return lang;
127
+ }
128
+ const start = Math.max(0, LANGS.indexOf(lang));
129
+ const idx = await pickFromList({
130
+ title: t(lang, "lang_pick"),
131
+ items: LANGS.map((id) => LANG_LABEL[id]),
132
+ index: start,
133
+ hint: "↑↓ Enter",
134
+ stdin: input,
135
+ stdout: output,
136
+ });
137
+ lang = LANGS[idx] || lang;
138
+ saveCli({ ...loadCli(), lang });
139
+ console.log(dim(t(lang, "lang_saved", { label: LANG_LABEL[lang] })));
140
+ return lang;
141
+ }
142
+
143
+ async function wantDaemon(opts) {
144
+ if (opts.daemon) return true;
145
+ if (opts.yes) return false;
146
+ if (!input.isTTY || !output.isTTY) return false;
147
+ const idx = await pickFromList({
148
+ title: t(lang, "ask_daemon"),
149
+ items: [t(lang, "daemon_no"), t(lang, "daemon_yes")],
150
+ index: 0,
151
+ hint: "↑↓ Enter",
152
+ stdin: input,
153
+ stdout: output,
154
+ });
155
+ return idx === 1;
156
+ }
157
+
158
+ function printAdvice(notes) {
159
+ const list = Array.isArray(notes) ? notes.filter((n) => n?.title) : [];
160
+ if (!list.length) return;
161
+ console.log(`\n${bold("šŸ“‹ " + t(lang, "advice_title"))}`);
162
+ console.log(dim(t(lang, "advice_sub")));
163
+ for (const n of list.slice(0, 8)) {
164
+ console.log(` • ${n.title}`);
165
+ if (n.do) console.log(dim(` ${n.do}`));
166
+ }
167
+ }
168
+
169
+ async function sendPulse(device, { preview = true } = {}) {
170
+ const pulse = collectPulse();
171
+ if (preview) {
172
+ console.log(bold("šŸ“” " + t(lang, "payload")));
173
+ console.log(formatPulsePreview(pulse, lang));
174
+ }
175
+ const out = await api(device.api, `/api/satellites/${device.id}/pulse`, {
176
+ method: "POST",
177
+ secret: device.secret,
178
+ json: pulse,
179
+ });
180
+ return { out, pulse };
181
+ }
182
+
183
+ async function cmdLogin(opts) {
184
+ await ensureLang(opts);
185
+ banner();
186
+ const url = String(opts.url || process.env.ORB44_API || "http://127.0.0.1:8787").replace(/\/$/, "");
187
+ const prev = loadDevice();
188
+ if (prev?.secret) {
189
+ await api(prev.api, "/api/satellites/logout", { method: "POST", json: { secret: prev.secret } }).catch(() => {});
190
+ clearDevice();
191
+ }
192
+ const hostname = os.hostname();
193
+ const name = opts.name || hostname;
194
+ const begin = await api(url, "/api/satellites/pair/begin", {
195
+ method: "POST",
196
+ json: { hostname, name },
197
+ });
198
+ if (!begin.ok || !begin.body.pollToken) {
199
+ console.error(`āš ļø ${begin.body.error || t(lang, "pair_fail")}`);
200
+ process.exit(1);
201
+ }
202
+ console.log(`šŸ”— API ${url}`);
203
+ console.log(`🌐 ${t(lang, "open_link")}\n`);
204
+ console.log(` ${bold(begin.body.verifyUrl)}`);
205
+ console.log(dim(` ${t(lang, "code", { code: begin.body.userCode })}\n`));
206
+ console.log("ā³ " + t(lang, "waiting"));
207
+ const deadline = Date.now() + (Number(begin.body.expiresInSec) || 600) * 1000;
208
+ let issued = null;
209
+ while (Date.now() < deadline) {
210
+ await new Promise((r) => setTimeout(r, 1500));
211
+ const poll = await api(url, "/api/satellites/pair/poll", {
212
+ method: "POST",
213
+ json: { pollToken: begin.body.pollToken },
214
+ });
215
+ if (poll.body?.status === "done") {
216
+ issued = poll.body;
217
+ break;
218
+ }
219
+ if (poll.status === 404) {
220
+ console.error("\nāš ļø " + t(lang, "code_expired"));
221
+ process.exit(1);
222
+ }
223
+ process.stdout.write(".");
224
+ }
225
+ if (!issued?.secret) {
226
+ console.error("\nāš ļø " + t(lang, "no_confirm"));
227
+ process.exit(1);
228
+ }
229
+ const device = {
230
+ api: url,
231
+ id: issued.id,
232
+ secret: issued.secret,
233
+ host: issued.host,
234
+ name: issued.name,
235
+ pairedAt: Date.now(),
236
+ };
237
+ saveDevice(device);
238
+ console.log(`\n\nāœ… ${t(lang, "paired", { host: device.host, name: device.name })}`);
239
+ console.log(dim(`šŸ”‘ ${t(lang, "key_file", { file: DEVICE_FILE })}\n`));
240
+
241
+ const { out } = await sendPulse(device);
242
+ if (!out.ok) {
243
+ console.error(`āš ļø ${out.body.error || t(lang, "pulse_fail")}`);
244
+ process.exit(1);
245
+ }
246
+ console.log("\nšŸ“” " + t(lang, "pulse_ok"));
247
+ printAdvice(out.body?.notes);
248
+ if (await wantDaemon(opts)) {
249
+ if (cmdInstall(opts, { enable: true })) {
250
+ console.log("\n" + t(lang, "login_done"));
251
+ }
252
+ } else {
253
+ console.log(dim("\n" + t(lang, "daemon_skip")));
254
+ }
255
+ process.exit(0);
256
+ }
257
+
258
+ function applyLang(opts) {
259
+ lang = storedOrFlag(opts);
260
+ return lang;
261
+ }
262
+
263
+ async function cmdPulse() {
264
+ applyLang(opts);
265
+ const device = loadDevice();
266
+ if (!device?.secret) {
267
+ console.error("āš ļø " + t(lang, "need_login"));
268
+ process.exit(1);
269
+ }
270
+ banner();
271
+ const { out } = await sendPulse(device);
272
+ if (!out.ok) {
273
+ console.error(
274
+ "āš ļø " +
275
+ (out.body.error === "bad_secret" || out.body.error === "not_found" ? t(lang, "key_revoked") : out.body.error || t(lang, "pulse_fail"))
276
+ );
277
+ process.exit(1);
278
+ }
279
+ console.log("\nšŸ“” " + t(lang, "pulse_ok"));
280
+ printAdvice(out.body?.notes);
281
+ }
282
+
283
+ async function cmdDaemon(opts) {
284
+ applyLang(opts);
285
+ const device = loadDevice();
286
+ if (!device?.secret) {
287
+ console.error("āš ļø " + t(lang, "need_login"));
288
+ process.exit(1);
289
+ }
290
+ const sec = intervalSec(opts.interval);
291
+ console.log(`šŸ›°ļø ${t(lang, "daemon_run", { sec, host: device.host })}`);
292
+ const tick = async () => {
293
+ const { out, pulse } = await sendPulse(device, { preview: false });
294
+ const hh = new Date().toISOString().slice(11, 19);
295
+ if (!out.ok) {
296
+ const gone = out.body.error === "bad_secret" || out.body.error === "not_found";
297
+ console.error(`āš ļø ${hh} ${gone ? t(lang, "daemon_revoked") : out.body.error || t(lang, "pulse_fail")}`);
298
+ return;
299
+ }
300
+ console.log(`šŸ“” ${hh} ok load ${pulse.load1} ${pulse.gradeInside || "—"}`);
301
+ };
302
+ await tick();
303
+ const id = setInterval(tick, sec * 1000);
304
+ const stop = () => {
305
+ clearInterval(id);
306
+ process.exit(0);
307
+ };
308
+ process.on("SIGTERM", stop);
309
+ process.on("SIGINT", stop);
310
+ await new Promise(() => {});
311
+ }
312
+
313
+ function systemdUnit({ node, script, deviceFile, interval, user }) {
314
+ const lines = [
315
+ "[Unit]",
316
+ "Description=Orb44 satellite pulse",
317
+ "After=network-online.target",
318
+ "",
319
+ "[Service]",
320
+ "Type=simple",
321
+ ];
322
+ if (user) {
323
+ lines.push(`User=${user}`, `Group=${user}`);
324
+ }
325
+ lines.push(
326
+ `ExecStart=${quote(node)} ${quote(script)} daemon --interval ${interval}`,
327
+ "Restart=on-failure",
328
+ "RestartSec=20",
329
+ `Environment=ORB44_DEVICE_FILE=${deviceFile}`,
330
+ `Environment=ORB44_LANG=${lang}`,
331
+ "NoNewPrivileges=true",
332
+ "",
333
+ "[Install]",
334
+ `WantedBy=${user ? "multi-user.target" : "default.target"}`
335
+ );
336
+ return `${lines.join("\n")}\n`;
337
+ }
338
+
339
+ function satServerDir() {
340
+ return path.join(path.dirname(SCRIPT), "..", "server");
341
+ }
342
+
343
+ function installSystemTree() {
344
+ const lib = "/usr/lib/orb44-sat";
345
+ const serverSrc = satServerDir();
346
+ fs.mkdirSync(path.join(lib, "bin"), { recursive: true, mode: 0o755 });
347
+ fs.mkdirSync(path.join(lib, "server"), { recursive: true, mode: 0o755 });
348
+ const copies = [
349
+ [SCRIPT, path.join(lib, "bin", "orb44.mjs")],
350
+ [path.join(serverSrc, "pulse.mjs"), path.join(lib, "server", "pulse.mjs")],
351
+ [path.join(serverSrc, "sat-i18n.mjs"), path.join(lib, "server", "sat-i18n.mjs")],
352
+ [path.join(serverSrc, "cli-menu.mjs"), path.join(lib, "server", "cli-menu.mjs")],
353
+ ];
354
+ for (const [from, to] of copies) {
355
+ fs.copyFileSync(from, to);
356
+ fs.chmodSync(to, 0o644);
357
+ }
358
+ return path.join(lib, "bin", "orb44.mjs");
359
+ }
360
+
361
+ function nologinShell() {
362
+ for (const p of ["/usr/sbin/nologin", "/sbin/nologin", "/bin/false"]) {
363
+ if (fs.existsSync(p)) return p;
364
+ }
365
+ return "/bin/false";
366
+ }
367
+
368
+ function ensureSystemServiceAccount(srcDeviceFile) {
369
+ const home = "/var/lib/orb44";
370
+ const dest = path.join(home, "device.json");
371
+ const hasUser = spawnSync("id", ["-u", "orb44"], { encoding: "utf8" }).status === 0;
372
+ if (!hasUser) {
373
+ const add = spawnSync("useradd", ["-r", "-s", nologinShell(), "-d", home, "-M", "orb44"], { encoding: "utf8" });
374
+ if (add.status !== 0) {
375
+ const err = (add.stderr || add.stdout || "useradd failed").trim();
376
+ return { ok: false, err: err.slice(0, 200) };
377
+ }
378
+ }
379
+ fs.mkdirSync(home, { recursive: true, mode: 0o750 });
380
+ fs.copyFileSync(srcDeviceFile, dest);
381
+ fs.chmodSync(dest, 0o600);
382
+ const chown = spawnSync("chown", ["-R", "orb44:orb44", home], { encoding: "utf8" });
383
+ if (chown.status !== 0) {
384
+ return { ok: false, err: (chown.stderr || "chown failed").trim().slice(0, 200) };
385
+ }
386
+ try {
387
+ const script = installSystemTree();
388
+ return { ok: true, user: "orb44", deviceFile: dest, script };
389
+ } catch (e) {
390
+ return { ok: false, err: String(e.message || e).slice(0, 200) };
391
+ }
392
+ }
393
+
394
+ function serviceIsActive(asSystem) {
395
+ const args = asSystem ? ["is-active", "orb44-satellite"] : ["--user", "is-active", "orb44-satellite"];
396
+ const r = spawnSync("systemctl", args, { encoding: "utf8" });
397
+ return (r.stdout || "").trim() === "active";
398
+ }
399
+
400
+ function tryEnable(asSystem) {
401
+ const args = asSystem
402
+ ? [
403
+ ["daemon-reload"],
404
+ ["enable", "--now", "orb44-satellite"],
405
+ ]
406
+ : [
407
+ ["--user", "daemon-reload"],
408
+ ["--user", "enable", "--now", "orb44-satellite"],
409
+ ];
410
+ for (const a of args) {
411
+ const r = spawnSync("systemctl", a, { encoding: "utf8" });
412
+ if (r.status !== 0) {
413
+ const err = (r.stderr || r.stdout || r.error?.message || "").trim();
414
+ return { ok: false, err: err.slice(0, 240) };
415
+ }
416
+ }
417
+ const t0 = Date.now();
418
+ while (Date.now() - t0 < 2500) {
419
+ if (serviceIsActive(asSystem)) return { ok: true };
420
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
421
+ }
422
+ const st = spawnSync(
423
+ "systemctl",
424
+ asSystem ? ["status", "--no-pager", "-l", "orb44-satellite"] : ["--user", "status", "--no-pager", "-l", "orb44-satellite"],
425
+ { encoding: "utf8" }
426
+ );
427
+ return { ok: false, err: (st.stdout || st.stderr || "not active").trim().slice(0, 240) };
428
+ }
429
+
430
+ function cmdInstall(opts, { enable = false } = {}) {
431
+ applyLang(opts);
432
+ const device = loadDevice();
433
+ if (!device?.secret) {
434
+ console.error("āš ļø " + t(lang, "need_login"));
435
+ process.exit(1);
436
+ }
437
+ const asSystem = Boolean(opts.system) || process.getuid?.() === 0;
438
+ const interval = intervalSec(opts.interval);
439
+ let deviceFile = DEVICE_FILE;
440
+ let user = null;
441
+ let script = SCRIPT;
442
+ if (asSystem) {
443
+ const acct = ensureSystemServiceAccount(DEVICE_FILE);
444
+ if (acct.ok) {
445
+ deviceFile = acct.deviceFile;
446
+ user = acct.user;
447
+ script = acct.script;
448
+ console.log(dim(t(lang, "daemon_user", { user, file: deviceFile })));
449
+ } else {
450
+ console.log(dim(t(lang, "daemon_user_fail", { err: acct.err ? `: ${acct.err}` : "" })));
451
+ }
452
+ }
453
+ const unit = systemdUnit({
454
+ node: process.execPath,
455
+ script,
456
+ deviceFile,
457
+ interval,
458
+ user,
459
+ });
460
+ const unitPath = asSystem
461
+ ? "/etc/systemd/system/orb44-satellite.service"
462
+ : path.join(os.homedir(), ".config", "systemd", "user", "orb44-satellite.service");
463
+ fs.mkdirSync(path.dirname(unitPath), { recursive: true });
464
+ fs.writeFileSync(unitPath, unit, { mode: 0o644 });
465
+ console.log("āš™ļø " + t(lang, "unit_written", { path: unitPath }));
466
+ if (enable) {
467
+ const on = tryEnable(asSystem);
468
+ if (on.ok) {
469
+ console.log("āœ… " + t(lang, "daemon_on"));
470
+ return true;
471
+ }
472
+ console.log("āš ļø " + t(lang, "unit_enable_fail", { err: on.err ? `: ${on.err}` : "" }));
473
+ }
474
+ if (asSystem) {
475
+ console.log(dim(" " + t(lang, "install_root")));
476
+ console.log(dim(" useradd -r -s /usr/sbin/nologin -d /var/lib/orb44 orb44"));
477
+ console.log(dim(" " + t(lang, "copy_key")));
478
+ console.log(" systemctl daemon-reload && systemctl enable --now orb44-satellite");
479
+ } else {
480
+ console.log(" systemctl --user daemon-reload && systemctl --user enable --now orb44-satellite");
481
+ }
482
+ return false;
483
+ }
484
+
485
+ function cmdStatus() {
486
+ applyLang(opts);
487
+ const device = loadDevice();
488
+ banner();
489
+ if (!device) {
490
+ console.log("āš ļø " + t(lang, "not_paired"));
491
+ return;
492
+ }
493
+ console.log(`šŸ”— API ${device.api}`);
494
+ console.log(`🌐 ${device.host}`);
495
+ console.log(`šŸ“› ${device.name}`);
496
+ console.log(`šŸ†” ${device.id}`);
497
+ console.log(dim(`šŸ”‘ ${DEVICE_FILE}`));
498
+ const pulse = collectPulse();
499
+ console.log(`\n${bold("šŸ“” " + t(lang, "now_on_box"))}`);
500
+ console.log(formatPulsePreview(pulse, lang));
501
+ }
502
+
503
+ async function cmdLogout() {
504
+ applyLang(opts);
505
+ const device = loadDevice();
506
+ if (device?.secret) {
507
+ await api(device.api, "/api/satellites/logout", { method: "POST", json: { secret: device.secret } });
508
+ }
509
+ clearDevice();
510
+ console.log("āœ… " + t(lang, "logged_out"));
511
+ }
512
+
513
+ async function cmdLang(flags) {
514
+ const want = normalizeLang(flags._[1] || flags.lang);
515
+ if (want) {
516
+ lang = want;
517
+ saveCli({ ...loadCli(), lang });
518
+ console.log(t(lang, "lang_saved", { label: LANG_LABEL[lang] }));
519
+ return;
520
+ }
521
+ applyLang(flags);
522
+ if (input.isTTY && output.isTTY) {
523
+ const idx = await pickFromList({
524
+ title: t(lang, "lang_pick"),
525
+ items: LANGS.map((id) => LANG_LABEL[id]),
526
+ index: Math.max(0, LANGS.indexOf(lang)),
527
+ hint: "↑↓ Enter",
528
+ stdin: input,
529
+ stdout: output,
530
+ });
531
+ lang = LANGS[idx] || lang;
532
+ saveCli({ ...loadCli(), lang });
533
+ console.log(t(lang, "lang_saved", { label: LANG_LABEL[lang] }));
534
+ return;
535
+ }
536
+ console.log(t(lang, "lang_now", { label: LANG_LABEL[lang] }));
537
+ }
538
+
539
+ function help() {
540
+ applyLang(opts);
541
+ console.log(t(lang, "help", { file: DEVICE_FILE }));
542
+ }
543
+
544
+ const opts = args();
545
+ const cmd = opts._[0] || "help";
546
+ const run = {
547
+ login: () => cmdLogin(opts),
548
+ pulse: cmdPulse,
549
+ daemon: () => cmdDaemon(opts),
550
+ install: () => cmdInstall(opts, { enable: true }),
551
+ status: cmdStatus,
552
+ logout: cmdLogout,
553
+ lang: () => cmdLang(opts),
554
+ help,
555
+ };
556
+ const fn = run[cmd];
557
+ if (!fn) {
558
+ help();
559
+ process.exit(1);
560
+ }
561
+ await fn();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@orb44/cli",
3
+ "version": "0.1.0",
4
+ "description": "Orb44 Watch satellite: outgoing pulse from the host. Not a remote shell.",
5
+ "type": "module",
6
+ "bin": {
7
+ "orb44": "bin/orb44.mjs"
8
+ },
9
+ "files": [
10
+ "bin/orb44.mjs",
11
+ "server/pulse.mjs",
12
+ "server/sat-i18n.mjs",
13
+ "server/cli-menu.mjs"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ephemeral172/orb44.git",
21
+ "directory": "packages/orb44"
22
+ },
23
+ "homepage": "https://github.com/ephemeral172/orb44",
24
+ "bugs": {
25
+ "url": "https://github.com/ephemeral172/orb44/issues"
26
+ },
27
+ "keywords": [
28
+ "orb44",
29
+ "watch",
30
+ "satellite",
31
+ "pulse"
32
+ ],
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "prepack": "node ../../scripts/stage-orb44-npm.mjs"
39
+ }
40
+ }