@tsconfigdotjson/vibe-os 0.2.0-rc.1 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +19 -847
  2. package/THIRD_PARTY_NOTICES.md +28 -0
  3. package/dist/web/assets/index-82MhMOB4.css +1 -0
  4. package/dist/web/assets/index-82MhMOB4.css.br +0 -0
  5. package/dist/web/assets/index-82MhMOB4.css.gz +0 -0
  6. package/dist/web/assets/index-DCB2nO0u.js +36 -0
  7. package/dist/web/assets/index-DCB2nO0u.js.br +0 -0
  8. package/dist/web/assets/index-DCB2nO0u.js.gz +0 -0
  9. package/dist/web/index.html +2 -2
  10. package/package.json +2 -1
  11. package/scripts/notices.ts +3 -1
  12. package/server/api.ts +449 -20
  13. package/server/assets.generated.ts +13 -13
  14. package/server/attach.ts +65 -16
  15. package/server/browser.ts +1 -65
  16. package/server/cli.ts +48 -150
  17. package/server/client.ts +440 -0
  18. package/server/config.ts +64 -2
  19. package/server/db.ts +19 -0
  20. package/server/doctor.ts +176 -123
  21. package/server/harness.ts +378 -590
  22. package/server/harnesses/claude.ts +248 -0
  23. package/server/harnesses/codex.ts +241 -0
  24. package/server/harnesses/cursor.ts +202 -0
  25. package/server/harnesses/hermes.ts +259 -0
  26. package/server/harnesses/util.ts +181 -0
  27. package/server/headless.ts +358 -0
  28. package/server/index.ts +47 -14
  29. package/server/memory.ts +418 -0
  30. package/server/profiles.ts +27 -15
  31. package/server/projects.ts +52 -3
  32. package/server/reaper.ts +5 -3
  33. package/server/screens.ts +372 -0
  34. package/server/session.ts +84 -95
  35. package/server/setup.ts +73 -7
  36. package/server/windows.ts +51 -1
  37. package/shared/blanks.ts +113 -0
  38. package/shared/harness.ts +375 -0
  39. package/shared/wire.ts +105 -94
  40. package/dist/web/assets/index-B77Qk8B5.js +0 -35
  41. package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
  42. package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
  43. package/dist/web/assets/index-CH5Em-Mo.css +0 -1
  44. package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
  45. package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
package/server/attach.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  // process that owns the pty is `dtach -n <socket> …` and every attachment is
21
21
  // `dtach -a <socket>`. They differ in argv, so the two are never confused.
22
22
 
23
- import { execFile } from "node:child_process";
23
+ import { execFile, spawn } from "node:child_process";
24
24
  import { readdir, rm } from "node:fs/promises";
25
25
  import { promisify } from "node:util";
26
26
  import { and, desc, eq } from "drizzle-orm";
@@ -84,6 +84,8 @@ export interface AttachTarget {
84
84
  project: string;
85
85
  /** The role the window was opened as, when it has one. */
86
86
  role: string | null;
87
+ /** The caller's own tag for the window. */
88
+ label: string | null;
87
89
  }
88
90
 
89
91
  // ── resolution ───────────────────────────────────────────────────────────────
@@ -114,6 +116,7 @@ const targetRows = (db: Db) =>
114
116
  lastOpenedAt: workspaces.lastOpenedAt,
115
117
  project: projects.name,
116
118
  role: profiles.name,
119
+ label: windows.label,
117
120
  })
118
121
  .from(windows)
119
122
  .innerJoin(workspaces, eq(windows.workspaceId, workspaces.id))
@@ -133,6 +136,7 @@ const toTarget = (r: TargetRow): AttachTarget => ({
133
136
  workspace: r.workspace,
134
137
  project: r.project,
135
138
  role: r.role,
139
+ label: r.label,
136
140
  });
137
141
 
138
142
  export function resolveTarget(
@@ -189,41 +193,86 @@ export function commandFor(
189
193
  db: Db,
190
194
  config: Config,
191
195
  target: AttachTarget,
196
+ opts?: { resume?: boolean },
192
197
  ): string | undefined {
193
198
  const profile = target.profileId
194
199
  ? getProfile(db, target.profileId)
195
200
  : undefined;
196
- return windowCommand(target.session, target.cwd, config, profile);
201
+ return windowCommand(target.session, target.cwd, config, profile, opts);
197
202
  }
198
203
 
199
204
  // ── talking to dtach ─────────────────────────────────────────────────────────
200
205
 
201
- /** Sessions with something still holding the other end of their socket. */
202
- export async function liveSessions(config: Config): Promise<Set<string>> {
206
+ /**
207
+ * Whether something still holds the other end of a session socket.
208
+ *
209
+ * Writing nothing to it succeeds only then, which is what separates a live
210
+ * session from a socket a killed one left behind. "Nothing" has to be an empty
211
+ * stdin: `dtach -p` sends stdin until EOF, and execFile hands it a pipe that is
212
+ * never closed, so every probe ran to its timeout and read as dead.
213
+ */
214
+ function answers(sock: string): Promise<boolean> {
215
+ return new Promise((resolve) => {
216
+ const child = spawn("dtach", ["-p", sock], {
217
+ stdio: "ignore",
218
+ timeout: 10_000,
219
+ });
220
+ child.on("error", () => resolve(false));
221
+ child.on("exit", (code) => resolve(code === 0));
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Every socket in the sessions directory, split by whether anything answers.
227
+ *
228
+ * A dead one is a session that died without its harness exiting: dtach removes
229
+ * its socket when the program underneath exits, and only a kill (a reboot, the
230
+ * OOM killer) leaves the file behind with nothing holding it.
231
+ */
232
+ async function probeSessions(
233
+ config: Config,
234
+ ): Promise<{ live: Set<string>; dead: Set<string> }> {
235
+ const live = new Set<string>();
236
+ const dead = new Set<string>();
203
237
  try {
204
238
  const dir = `${config.stateDir}/sessions`;
205
239
  const names = await readdir(dir);
206
- const live = new Set<string>();
207
240
  await Promise.all(
208
241
  names
209
242
  .filter((n) => n.endsWith(".sock"))
210
243
  .map(async (n) => {
211
- // Writing nothing to the socket succeeds only if something is still
212
- // holding the other end, which is what separates a live session from
213
- // a socket a crashed one left behind.
214
- const ok = await run("dtach", ["-p", `${dir}/${n}`], {
215
- timeout: 10_000,
216
- })
217
- .then(() => true)
218
- .catch(() => false);
219
- if (ok) live.add(n.replace(/\.sock$/, ""));
244
+ const ok = await answers(`${dir}/${n}`);
245
+ (ok ? live : dead).add(n.replace(/\.sock$/, ""));
220
246
  }),
221
247
  );
222
- return live;
223
248
  } catch {
224
249
  // No sessions directory yet: nothing has ever been opened.
225
- return new Set();
226
250
  }
251
+ return { live, dead };
252
+ }
253
+
254
+ /** Sessions with something still holding the other end of their socket. */
255
+ export async function liveSessions(config: Config): Promise<Set<string>> {
256
+ return (await probeSessions(config)).live;
257
+ }
258
+
259
+ /**
260
+ * Windows whose session died under them rather than ending.
261
+ *
262
+ * Meant for server start, before any browser logs in: the first login to a
263
+ * window removes its dead socket on the way to creating a new session, and
264
+ * with it the only evidence there was something to bring back.
265
+ */
266
+ export async function interruptedWindows(
267
+ db: Db,
268
+ config: Config,
269
+ ): Promise<string[]> {
270
+ if (!config.sessions) return [];
271
+ const { dead } = await probeSessions(config);
272
+ if (dead.size === 0) return [];
273
+ return listTargets(db)
274
+ .filter((t) => dead.has(t.session))
275
+ .map((t) => t.windowId);
227
276
  }
228
277
 
229
278
  /**
package/server/browser.ts CHANGED
@@ -167,10 +167,7 @@ export interface Unit {
167
167
  /**
168
168
  * Where the units are written, named once.
169
169
  *
170
- * `install-browser` writes them and three readers go looking: doctor, the Hermes
171
- * discovery in `harness.ts`, and `connect-hermes`. They had a path literal each
172
- * until the third one wanted the same file, which is one copy past the point
173
- * where they can be trusted to stay in step.
170
+ * `install-browser` writes them and doctor goes looking for them.
174
171
  */
175
172
  export const UNIT_DIR = "/etc/systemd/system";
176
173
  export const CHROME_UNIT_PATH = `${UNIT_DIR}/vibe-os-chrome.service`;
@@ -419,67 +416,6 @@ export function vncPasswordFileFromUnit(
419
416
  return match ? match[1] : null;
420
417
  }
421
418
 
422
- /**
423
- * What Hermes has to be told so its browser tools drive this Chrome.
424
- *
425
- * Hermes reads its CDP target from `browser.cdp_url` in `~/.hermes/config.yaml`
426
- * and offers no flag for it, so this is set once for the box rather than per
427
- * profile. `browser-use` is named explicitly even though it is already the
428
- * default, because the default is conditional on the CLI being runnable and a
429
- * config that says what it means survives someone else reading it.
430
- *
431
- * `cloud_provider: local` is what stops a Browserbase or Firecrawl key, set
432
- * later for something else, from quietly taking the browser back.
433
- *
434
- * Returned as pairs rather than written here: they are handed to
435
- * `hermes config set`, which owns that file and knows where a secret goes.
436
- */
437
- export function hermesBrowserSettings(
438
- cdpPort: number,
439
- ): { key: string; value: string }[] {
440
- return [
441
- { key: "browser.cdp_url", value: `http://127.0.0.1:${cdpPort}` },
442
- { key: "browser.backend", value: "browser-use" },
443
- { key: "browser.cloud_provider", value: "local" },
444
- ];
445
- }
446
-
447
- /**
448
- * Whether a configured `cdp_url` names this box's Chrome.
449
- *
450
- * Written by hand as often as by `connect-hermes`, so the comparison is on what
451
- * the URL means rather than on its spelling: `localhost` and `127.0.0.1` are the
452
- * same host, and `ws://` and `http://` are the same endpoint to a CDP client.
453
- * A bare `127.0.0.1:9222` is accepted too, because it is what people type.
454
- *
455
- * The port has to be explicit. A URL with no port is not a CDP endpoint that
456
- * would ever have worked, and inferring 80 from `http://` would report a
457
- * misconfigured box as connected.
458
- */
459
- export function cdpUrlMatches(
460
- configured: string | null | undefined,
461
- cdpPort: number,
462
- ): boolean {
463
- const raw = configured?.trim();
464
- if (!raw) return false;
465
- // A scheme is optional in what people write but not in what URL will parse.
466
- const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
467
- ? raw
468
- : `http://${raw}`;
469
- let url: URL;
470
- try {
471
- url = new URL(withScheme);
472
- } catch {
473
- return false;
474
- }
475
- if (!/^(https?|wss?):$/.test(url.protocol)) return false;
476
- if (url.port !== String(cdpPort)) return false;
477
- // Chrome binds the debug port to loopback, so anything else is a different
478
- // browser on a different machine and saying "connected" would be a guess.
479
- const host = url.hostname.replace(/^\[/, "").replace(/\]$/, "");
480
- return host === "127.0.0.1" || host === "localhost" || host === "::1";
481
- }
482
-
483
419
  export interface MemInfo {
484
420
  totalKb: number;
485
421
  swapKb: number;
package/server/cli.ts CHANGED
@@ -18,17 +18,14 @@ import {
18
18
  BROWSER_COMMANDS,
19
19
  browserServices,
20
20
  browserUnits,
21
- CHROME_UNIT_PATH,
22
- DEFAULT_CDP_PORT,
23
21
  defaultBrowserOptions,
24
- hermesBrowserSettings,
25
- portsFromUnits,
26
22
  type Unit,
27
23
  VNC_PASSWORD_LENGTH,
28
24
  vncPasswordFileFromUnit,
29
25
  vncPasswordPath,
30
26
  XVNC_UNIT_PATH,
31
27
  } from "./browser.ts";
28
+ import { runClient } from "./client.ts";
32
29
  import {
33
30
  type Config,
34
31
  parseCliArgs,
@@ -38,12 +35,13 @@ import {
38
35
  } from "./config.ts";
39
36
  import { openDb } from "./db.ts";
40
37
  import { type Check, homeFor, onPath, runDoctor } from "./doctor.ts";
41
- import { HERMES_INSTALL } from "./harness.ts";
38
+ import { loadHarnesses } from "./harness.ts";
42
39
  import { startServer } from "./index.ts";
43
40
  import { color, describeError, log } from "./log.ts";
44
41
  import { reapLeaks } from "./reaper.ts";
45
42
  import { ENTRY, FETCH_WASM, IS_COMPILED } from "./runtime.ts";
46
43
  import { confirmFirewall, runSetup } from "./setup.ts";
44
+ import { takeResume } from "./windows.ts";
47
45
 
48
46
  const run = promisify(execFile);
49
47
  const BUN = process.execPath;
@@ -60,9 +58,20 @@ const HELP = `
60
58
  vibe-os setup fix what doctor finds, asking first
61
59
  vibe-os install-service write and enable a systemd unit (needs root)
62
60
  vibe-os install-browser run one Chrome on a virtual display (needs root)
63
- vibe-os connect-hermes point Hermes' browser tools at that Chrome
64
61
  vibe-os fetch-wasm (re)download the SSH WASM runtime
65
62
 
63
+ vibe-os ls list windows, and what each is doing
64
+ vibe-os workspace new <project> [--from <branch>] [--name <name>]
65
+ cut a workspace
66
+ vibe-os workspace ls [project]
67
+ list workspaces
68
+ vibe-os open <workspace> [--profile <name>] [--prompt <text>]
69
+ [--blank label=value] [--submit] [--force] [--label <text>]
70
+ open a window and start it
71
+ vibe-os output <window> [--lines <n>]
72
+ print what a window shows
73
+ vibe-os close <window> close a window and end its session
74
+
66
75
  ${color.bold("Options")}
67
76
  --port <n> HTTP port (default 80)
68
77
  --host <addr> bind address (default 0.0.0.0)
@@ -86,6 +95,13 @@ const HELP = `
86
95
  one the browser reached the desktop on
87
96
  --user <name> unix user to log in as (default: current user)
88
97
  --no-sessions plain login shells instead of persistent dtach sessions
98
+ --memory-high <size>
99
+ throttle a window past this (default 40%, of RAM)
100
+ --memory-max <size> kill a window past this (default 50%)
101
+ --memory-swap-max <size>
102
+ swap a window may use (default 10%, of RAM). Sizes
103
+ are systemd's: 1500M, 2G, 40%, infinity
104
+ --no-memory-limit start windows without a memory scope
89
105
  --cert-ttl <secs> certificate lifetime (default 43200)
90
106
 
91
107
  --workspace <dir> root for projects and worktrees (default ~/workspace)
@@ -95,6 +111,21 @@ const HELP = `
95
111
  -h, --help show this
96
112
  -v, --version print the version
97
113
 
114
+ ${color.bold("ls, workspace, open, output, close")}
115
+ --url <url> the server to talk to (default VIBE_OS_URL, or on this
116
+ box the address start is listening on)
117
+ --token <value> its token (default VIBE_OS_TOKEN, or on this box the
118
+ one start remembered)
119
+ --json print JSON instead of text
120
+ --prompt <text> paste this into the window; - reads it from stdin
121
+ --blank <label=value>
122
+ fill a blank in the profile's prompt and paste that.
123
+ Repeatable
124
+ --submit press Enter after pasting
125
+ --force open even when the box is short of memory
126
+ --label <text> tag the window, to find it again in ls --json
127
+ --lines <n> how many lines output prints (default 50)
128
+
98
129
  ${color.bold("install-browser")}
99
130
  --geometry <WxH> virtual screen size (default 1600x900)
100
131
  --display <n> X display number (default 99)
@@ -109,10 +140,6 @@ const HELP = `
109
140
  this. An existing one is kept unless --no-vnc-password
110
141
  --no-vnc-password serve the display with no authentication
111
142
 
112
- ${color.bold("connect-hermes")}
113
- --cdp-port <n> the debug port to point Hermes at, when there is no
114
- installed browser unit to read it from
115
-
116
143
  ${color.bold("doctor")}
117
144
  --reap kill processes that outlived their workspace or session
118
145
 
@@ -308,7 +335,7 @@ async function installBrowser(
308
335
  ` ${color.cyan("sudo apt install -y tigervnc-standalone-server openbox x11-utils")}`,
309
336
  );
310
337
  console.log(
311
- ` ${color.dim("Chrome is not in the distro repos; see the README for the Google apt repo.")}`,
338
+ ` ${color.dim("Chrome is not in the distro repos; for the Google apt repo see https://github.com/tsconfigdotjson/vibe-os/blob/main/docs/deploying.md#an-always-on-browser")}`,
312
339
  );
313
340
  console.log("");
314
341
  return 1;
@@ -427,142 +454,6 @@ async function installBrowser(
427
454
  ` ${color.dim("Sign in to the extension once, through the viewer. The profile keeps it.")}`,
428
455
  );
429
456
 
430
- // Last, and quietly. Hermes usually arrives after the browser, so not finding
431
- // it is the common case rather than a failure worth interrupting the summary.
432
- if (await connectHermes(user, home, opts.cdpPort, true)) {
433
- console.log("");
434
- console.log(
435
- ` ${color.dim(`Hermes browser tools point at 127.0.0.1:${opts.cdpPort}.`)}`,
436
- );
437
- } else {
438
- console.log("");
439
- console.log(
440
- ` ${color.dim("For Hermes browser tools, install it and run: vibe-os connect-hermes")}`,
441
- );
442
- }
443
- console.log("");
444
- return 0;
445
- }
446
-
447
- /**
448
- * Points Hermes' browser tools at this box's Chrome.
449
- *
450
- * Every setting goes through `hermes config set` rather than into the YAML
451
- * directly. Hermes owns that file, knows which of its keys are secrets and
452
- * belong in `.env` instead, and migrates its own schema between versions —
453
- * three things a hand-written merge here would have to keep guessing at.
454
- *
455
- * ── Why this is a box-wide setting and not a profile one ─────────────────────
456
- * The CDP target lives at `browser.cdp_url` in `~/.hermes/config.yaml` and has
457
- * no command-line equivalent, so there is nothing a profile could carry. Doing
458
- * it per launch would mean two Python startups before every window and a global
459
- * file rewritten by whichever window opened last.
460
- *
461
- * Returns false when Hermes is not installed, which is not an error: plenty of
462
- * boxes run the browser for the Claude extension and nothing else.
463
- */
464
- async function connectHermes(
465
- user: string,
466
- home: string,
467
- cdpPort: number,
468
- /** True when this process is root and has to drop to the login user. */
469
- asUser: boolean,
470
- ): Promise<boolean> {
471
- /*
472
- * A login shell, deliberately.
473
- *
474
- * Hermes installs to `/usr/local/bin` when it can write there and
475
- * `~/.local/bin` when it cannot, and root's PATH reaches neither reliably
476
- * under systemd. `runuser -l` runs `.profile`, which is what put the second
477
- * one on PATH in the first place. runuser is util-linux, the same package
478
- * `flock` comes from, so it is present wherever the session lock already is.
479
- */
480
- const asLoginUser = (script: string): [string, string[]] =>
481
- asUser
482
- ? ["runuser", ["-u", user, "--", "/bin/sh", "-lc", script]]
483
- : ["/bin/sh", ["-lc", script]];
484
-
485
- const [probe, probeArgs] = asLoginUser("command -v hermes");
486
- const installed = await run(probe, probeArgs, {
487
- timeout: 10_000,
488
- env: { ...process.env, HOME: home },
489
- })
490
- .then(({ stdout }) => Boolean(stdout.trim()))
491
- .catch(() => false);
492
- if (!installed) return false;
493
-
494
- for (const { key, value } of hermesBrowserSettings(cdpPort)) {
495
- // Quoted as a single argument: a value never contains a space today, and
496
- // the day one does is not the day to find out this was a bare expansion.
497
- const [cmd, args] = asLoginUser(
498
- `hermes config set ${JSON.stringify(key)} ${JSON.stringify(value)}`,
499
- );
500
- try {
501
- await run(cmd, args, {
502
- timeout: 60_000,
503
- env: { ...process.env, HOME: home },
504
- });
505
- log.ok(`hermes ${key} = ${value}`);
506
- } catch (err) {
507
- log.error(`could not set hermes ${key}: ${describeError(err)}`);
508
- return false;
509
- }
510
- }
511
- return true;
512
- }
513
-
514
- /**
515
- * `vibe-os connect-hermes` — the same thing, for a Hermes installed later.
516
- *
517
- * `install-browser` does this as its last step, but it is the browser's
518
- * installer and Hermes usually arrives after it. No root: it writes one file in
519
- * your own home directory, through a command you own.
520
- */
521
- async function connectHermesCommand(
522
- config: Config,
523
- values: RawOptions,
524
- ): Promise<number> {
525
- const user = process.env.SUDO_USER ?? config.user;
526
- const home = (await homeFor(user)) ?? os.homedir();
527
-
528
- // What the unit actually opened, not what the default is. install-browser may
529
- // have been given --cdp-port long ago, and writing the default into Hermes
530
- // would point it at a port nothing is listening on.
531
- const unit = await readFile(CHROME_UNIT_PATH, "utf8").catch(() => null);
532
- const cdpPort = values["cdp-port"]
533
- ? numeric(values["cdp-port"], "cdp-port")
534
- : unit
535
- ? portsFromUnits({ chrome: unit }).cdpPort
536
- : DEFAULT_CDP_PORT;
537
-
538
- if (!unit && !values["cdp-port"]) {
539
- log.warn(
540
- `no browser installed here, so this assumes the default port ${cdpPort}`,
541
- );
542
- log.warn("run sudo vibe-os install-browser first, or pass --cdp-port");
543
- }
544
-
545
- const connected = await connectHermes(
546
- user,
547
- home,
548
- cdpPort,
549
- process.getuid?.() === 0,
550
- );
551
- if (!connected) {
552
- log.error(`hermes is not installed for ${user}`);
553
- console.log("");
554
- console.log(` ${color.cyan(HERMES_INSTALL)}`);
555
- console.log("");
556
- return 1;
557
- }
558
-
559
- console.log("");
560
- console.log(
561
- ` Hermes browser tools will drive the box's Chrome on 127.0.0.1:${cdpPort}.`,
562
- );
563
- console.log(
564
- ` ${color.dim("Watch it work: ssh -L 5900:127.0.0.1:5900, then any VNC viewer.")}`,
565
- );
566
457
  console.log("");
567
458
  return 0;
568
459
  }
@@ -714,7 +605,9 @@ async function attach(
714
605
  }
715
606
  if (!target) return 0;
716
607
 
717
- const command = commandFor(db, config, target);
608
+ const command = commandFor(db, config, target, {
609
+ resume: takeResume(db, target.windowId),
610
+ });
718
611
  if (!command) {
719
612
  log.error(`could not build a command for ${target.ref}`);
720
613
  return 1;
@@ -795,6 +688,7 @@ export async function main(argv: string[]): Promise<number> {
795
688
  let config: Awaited<ReturnType<typeof resolveConfig>>;
796
689
  try {
797
690
  config = await resolveConfig(values);
691
+ loadHarnesses(config.stateDir);
798
692
  } catch (err) {
799
693
  log.error(describeError(err));
800
694
  console.log(HELP);
@@ -828,8 +722,6 @@ export async function main(argv: string[]): Promise<number> {
828
722
  return installService(config, argv);
829
723
  case "install-browser":
830
724
  return installBrowser(config, values);
831
- case "connect-hermes":
832
- return connectHermesCommand(config, values);
833
725
  case "fetch-wasm": {
834
726
  // A standalone binary carries the wasm inside it: there is no script on
835
727
  // disk to run (FETCH_WASM points into the virtual /$bunfs root) and
@@ -846,6 +738,12 @@ export async function main(argv: string[]): Promise<number> {
846
738
  process.stdout.write(stdout);
847
739
  return 0;
848
740
  }
741
+ case "ls":
742
+ case "workspace":
743
+ case "open":
744
+ case "output":
745
+ case "close":
746
+ return runClient(command, positionals.slice(1), values, config);
849
747
  case "help":
850
748
  console.log(HELP);
851
749
  return 0;