@omg-dev/cli 0.4.30 → 0.4.31

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 (3) hide show
  1. package/README.md +15 -9
  2. package/dist/omg.mjs +143 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,6 +34,7 @@ Create a new app with `omg create my-app`. The same official generator powers
34
34
  | `omg whoami` | Print the authenticated account without printing its credential. |
35
35
  | `omg logout` | Remove saved credentials. |
36
36
  | `omg apps` | List apps visible to the authenticated account. |
37
+ | `omg connect [--new] [--no-install]` | Connect this computer's local LFG to OMG's hosted relay. |
37
38
  | `omg deploy [--name X] [--dir .] [--no-wait]` | Upload source, build, and publish. |
38
39
  | `omg status [--dir .]` | Show deploy status for the linked directory. |
39
40
  | `omg link <slug> [--dir .]` | Link a directory to an app on the account. |
@@ -43,11 +44,19 @@ Credentials resolve in this order: `OMG_API_KEY`, then
43
44
  `~/.omg/credentials.json`. Never put an API key directly in a shell command or
44
45
  commit either credential file.
45
46
 
47
+ `omg connect` discovers OMG's relay, obtains a one-time pairing code through
48
+ the authenticated CLI API, and hands both directly to `lfg connect`; the code
49
+ never needs to touch the clipboard. It resumes an existing OMG binding when
50
+ one is already saved. If LFG is missing, the command runs LFG's standard setup
51
+ script first; pass `--no-install` to require an existing install, or `--new` to
52
+ deliberately replace the saved binding.
53
+
46
54
  ## Implementation notes
47
55
 
48
- `omg deploy` uploads source to a temporary sandbox, takes a `files_only`
49
- snapshot, deletes the staging sandbox, and asks the normal platform builder to
50
- publish it. The builder—not the laptop—runs the production build.
56
+ `omg deploy` uploads source to the authenticated control-plane broker. The
57
+ broker owns the temporary sandbox, takes a `files_only` snapshot, deletes the
58
+ staging sandbox, and asks the normal platform builder to publish it. The
59
+ builder—not the laptop—runs the production build.
51
60
 
52
61
  The first successful deploy writes `.omg/project.json`. Its `projectId` is the
53
62
  binding that makes the next deploy update the same slug.
@@ -57,9 +66,6 @@ The collector excludes generated and local state (`node_modules`, `dist`,
57
66
  refuses `.env` and `.env.*`, skips symlinks, and enforces a 2 MB per-file and
58
67
  40 MB total-source limit.
59
68
 
60
- Browser OAuth tokens authenticate the control-plane CLI surface. Source
61
- staging on `infra.omg.dev` currently accepts dashboard JWTs and `omg_sk_` API
62
- keys, so use the API-key flow in the public guide when exercising
63
- `omg deploy`. API keys also authenticate the final publish hop through the
64
- control-plane broker; no service credential is stored on the developer's
65
- machine.
69
+ Browser OAuth tokens and `omg_sk_` API keys both authenticate the control-plane
70
+ CLI surface. The control-plane holds the infra service credential; no sandbox
71
+ or service credential is stored on the developer's machine.
package/dist/omg.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6
- import { join as join4, resolve as resolve3, basename, dirname as dirname3 } from "path";
5
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
6
+ import { join as join5, resolve as resolve3, basename, dirname as dirname3 } from "path";
7
7
 
8
8
  // src/config.ts
9
9
  import { homedir } from "os";
@@ -28,7 +28,6 @@ var CLI_API_OAUTH_AUDIENCES = [
28
28
  ];
29
29
 
30
30
  // src/config.ts
31
- var INFRA_URL = process.env.OMG_INFRA_URL?.trim() || "https://infra.omg.dev";
32
31
  var CONTROL_PLANE_URL = process.env.OMG_API_URL?.trim() || "https://backend.omg.dev";
33
32
  var AUTH_URL = process.env.OMG_AUTH_URL?.trim() || "https://auth.omg.dev";
34
33
  var OAUTH_RESOURCE = process.env.OMG_OAUTH_RESOURCE?.trim() || CLI_OAUTH_RESOURCE;
@@ -431,46 +430,12 @@ async function request(base, path, token, init = {}) {
431
430
  return text;
432
431
  }
433
432
  }
434
- var infra = (path, token, init) => request(INFRA_URL, path, token, init);
435
433
  var controlPlane = (path, token, init) => request(CONTROL_PLANE_URL, path, token, init);
436
434
  function whoAmI(token) {
437
435
  return controlPlane("/api/cli/whoami", token);
438
436
  }
439
- async function createSandbox(token, templateId = "react-ts") {
440
- const sb = await infra("/v1/sandboxes", token, {
441
- method: "POST",
442
- body: JSON.stringify({ size: "small", templateId, skipAppProcesses: true })
443
- });
444
- return { id: sb.id ?? sb.sandboxId, status: sb.status };
445
- }
446
- async function waitForRunning(token, id, timeoutMs = 120000) {
447
- const deadline = Date.now() + timeoutMs;
448
- while (Date.now() < deadline) {
449
- const s = await infra(`/v1/sandboxes/${id}`, token);
450
- if (s.status === "running")
451
- return;
452
- if (s.status === "failed")
453
- throw new Error(`sandbox ${id} failed to boot`);
454
- await new Promise((r) => setTimeout(r, 2000));
455
- }
456
- throw new Error(`sandbox ${id} did not reach running within ${timeoutMs}ms`);
457
- }
458
- function writeFiles(token, id, files) {
459
- return infra(`/v1/sandboxes/${id}/files`, token, {
460
- method: "POST",
461
- body: JSON.stringify(files.map((f) => ({ ...f, encoding: "base64" })))
462
- });
463
- }
464
- function snapshotProject(token, id) {
465
- return infra(`/v1/sandboxes/${id}/tarball`, token, { method: "POST", body: "{}" });
466
- }
467
- async function deleteSandbox(token, id) {
468
- try {
469
- await infra(`/v1/sandboxes/${id}`, token, { method: "DELETE" });
470
- } catch {}
471
- }
472
- function deployFromSnapshot(token, body) {
473
- return controlPlane("/api/cli/apps/deploy", token, {
437
+ function deploySource(token, body) {
438
+ return controlPlane("/api/cli/apps/deploy-source", token, {
474
439
  method: "POST",
475
440
  body: JSON.stringify(body)
476
441
  });
@@ -481,6 +446,12 @@ function getStatus(token, slug) {
481
446
  function listApps(token) {
482
447
  return controlPlane("/api/cli/apps/list", token);
483
448
  }
449
+ function getComputerConnectConfig(token) {
450
+ return controlPlane("/api/cli/computer/connect", token);
451
+ }
452
+ function startComputerPairing(token) {
453
+ return controlPlane("/api/cli/computer/connect", token, { method: "POST" });
454
+ }
484
455
 
485
456
  // src/create.ts
486
457
  var DEFAULT_GENERATOR = "create-omg@latest";
@@ -496,9 +467,110 @@ async function runCreate(args) {
496
467
  return child.exited;
497
468
  }
498
469
 
470
+ // src/connect.ts
471
+ import { chmodSync as chmodSync2, mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
472
+ import { homedir as homedir2, tmpdir } from "os";
473
+ import { join as join2 } from "path";
474
+ var LFG_SETUP_URL = "https://raw.githubusercontent.com/BennyKok/lfg/main/scripts/setup.sh";
475
+ async function runCommand(command, options = {}) {
476
+ const capture = options.capture === true;
477
+ const child = Bun.spawn(command, {
478
+ env: options.env,
479
+ stdin: capture ? "ignore" : "inherit",
480
+ stdout: capture ? "pipe" : "inherit",
481
+ stderr: capture ? "pipe" : "inherit"
482
+ });
483
+ const [exitCode, stdout2, stderr] = await Promise.all([
484
+ child.exited,
485
+ capture ? new Response(child.stdout).text() : "",
486
+ capture ? new Response(child.stderr).text() : ""
487
+ ]);
488
+ return { exitCode, stdout: stdout2, stderr };
489
+ }
490
+ var defaultDependencies = {
491
+ which: (name) => Bun.which(name),
492
+ runCommand,
493
+ installLfg,
494
+ getConfig: getComputerConnectConfig,
495
+ createPairing: startComputerPairing
496
+ };
497
+ function canonicalUrl(value) {
498
+ try {
499
+ const url = new URL(value);
500
+ url.pathname = url.pathname.replace(/\/$/, "") || "/";
501
+ return url.href.replace(/\/$/, "");
502
+ } catch {
503
+ return value.trim().replace(/\/$/, "");
504
+ }
505
+ }
506
+ function relayUrlFromStatus(output) {
507
+ if (output.includes("lfg connect: not paired with any relay."))
508
+ return null;
509
+ const match = output.match(/^lfg connect: paired as .+ via (\S+) \(since /m);
510
+ return match?.[1];
511
+ }
512
+ async function installLfg() {
513
+ const response = await fetch(LFG_SETUP_URL);
514
+ if (!response.ok) {
515
+ throw new Error(`Could not download the LFG installer (${response.status}).`);
516
+ }
517
+ const directory = mkdtempSync(join2(tmpdir(), "omg-lfg-install-"));
518
+ const script = join2(directory, "setup.sh");
519
+ try {
520
+ writeFileSync2(script, await response.text(), { mode: 448 });
521
+ chmodSync2(script, 448);
522
+ const result = await runCommand(["bash", script]);
523
+ if (result.exitCode !== 0) {
524
+ throw new Error(`LFG setup exited with code ${result.exitCode}.`);
525
+ }
526
+ } finally {
527
+ rmSync(directory, { recursive: true, force: true });
528
+ }
529
+ return Bun.which("lfg") || join2(homedir2(), ".local", "bin", "lfg");
530
+ }
531
+ async function runConnect(options) {
532
+ const output = options.output ?? ((message) => process.stdout.write(message + `
533
+ `));
534
+ const dependencies = { ...defaultDependencies, ...options.dependencies };
535
+ let lfg = dependencies.which("lfg");
536
+ if (!lfg) {
537
+ if (options.install === false) {
538
+ throw new Error("LFG is not installed. Install it from https://github.com/BennyKok/lfg, then rerun `omg connect`.");
539
+ }
540
+ output("LFG is not installed; installing it now\u2026");
541
+ lfg = await dependencies.installLfg();
542
+ }
543
+ const { connectUrl } = await dependencies.getConfig(options.token);
544
+ if (!connectUrl?.trim())
545
+ throw new Error("OMG returned no relay URL.");
546
+ if (!options.fresh) {
547
+ const status = await dependencies.runCommand([lfg, "connect", "status"], { capture: true });
548
+ if (status.exitCode !== 0) {
549
+ throw new Error(status.stderr.trim() || `Could not read LFG connection status (${status.exitCode}).`);
550
+ }
551
+ const savedRelay = relayUrlFromStatus(status.stdout);
552
+ if (savedRelay === undefined) {
553
+ throw new Error("This LFG version returned an unrecognized connection status. Run `lfg setup`, then retry.");
554
+ }
555
+ if (savedRelay && canonicalUrl(savedRelay) === canonicalUrl(connectUrl)) {
556
+ output("Resuming this computer's existing OMG connection\u2026");
557
+ const result2 = await dependencies.runCommand([lfg, "connect"], { env: process.env });
558
+ return result2.exitCode;
559
+ }
560
+ }
561
+ const pairing = await dependencies.createPairing(options.token);
562
+ if (!pairing.code || !pairing.connectUrl)
563
+ throw new Error("OMG returned an incomplete pairing response.");
564
+ output("Pairing this computer with OMG\u2026");
565
+ const result = await dependencies.runCommand([lfg, "connect", pairing.code], {
566
+ env: { ...process.env, LFG_RELAY_URL: pairing.connectUrl }
567
+ });
568
+ return result.exitCode;
569
+ }
570
+
499
571
  // src/files.ts
500
572
  import { readdirSync, lstatSync, readFileSync as readFileSync2 } from "fs";
501
- import { join as join2, relative, sep } from "path";
573
+ import { join as join3, relative, sep } from "path";
502
574
  var ALWAYS_SKIP = new Set([
503
575
  "node_modules",
504
576
  "dist",
@@ -522,7 +594,7 @@ function collectProjectFiles(root) {
522
594
  for (const entry of readdirSync(dir)) {
523
595
  if (ALWAYS_SKIP.has(entry))
524
596
  continue;
525
- const abs = join2(dir, entry);
597
+ const abs = join3(dir, entry);
526
598
  const st = lstatSync(abs);
527
599
  if (st.isDirectory()) {
528
600
  walk(abs);
@@ -555,7 +627,7 @@ function collectProjectFiles(root) {
555
627
  }
556
628
  function assertDeployable(root) {
557
629
  try {
558
- const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
630
+ const pkg = JSON.parse(readFileSync2(join3(root, "package.json"), "utf8"));
559
631
  if (!pkg.scripts?.build) {
560
632
  throw new Error("package.json has no `build` script \u2014 the builder runs `bun run build`, so the deploy would fail.");
561
633
  }
@@ -587,24 +659,9 @@ async function deploy(opts) {
587
659
  for (const s of collected.skippedLarge)
588
660
  onProgress(`skipping ${s} \u2014 over the per-file limit`);
589
661
  onProgress(`uploading ${collected.files.length} files (${(collected.totalBytes / 1024).toFixed(0)} KB)`);
590
- const sandbox = await createSandbox(token);
591
- onProgress(`staging sandbox ${sandbox.id}`);
592
- let snapshotId;
593
- try {
594
- await waitForRunning(token, sandbox.id);
595
- await writeFiles(token, sandbox.id, collected.files);
596
- const snap = await snapshotProject(token, sandbox.id);
597
- if (!snap?.id)
598
- throw new Error("snapshot did not return an id");
599
- snapshotId = snap.id;
600
- onProgress(`snapshot ${snap.id}` + (snap.tarballSizeBytes ? ` (${(snap.tarballSizeBytes / 1e6).toFixed(2)} MB)` : ""));
601
- } finally {
602
- await deleteSandbox(token, sandbox.id);
603
- }
604
- onProgress("publishing");
605
- const result = await deployFromSnapshot(token, {
662
+ const result = await deploySource(token, {
606
663
  name: opts.name,
607
- snapshotId,
664
+ files: collected.files.map(({ path, content }) => ({ path, content })),
608
665
  projectId: opts.projectId
609
666
  });
610
667
  return result;
@@ -644,12 +701,12 @@ import {
644
701
  mkdirSync as mkdirSync2,
645
702
  readFileSync as readFileSync3,
646
703
  readdirSync as readdirSync2,
647
- rmSync,
704
+ rmSync as rmSync2,
648
705
  statSync,
649
- writeFileSync as writeFileSync2
706
+ writeFileSync as writeFileSync3
650
707
  } from "fs";
651
708
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
652
- import { dirname as dirname2, join as join3, relative as relative2, resolve, sep as sep2 } from "path";
709
+ import { dirname as dirname2, join as join4, relative as relative2, resolve, sep as sep2 } from "path";
653
710
  var jsonHeaders = { "content-type": "application/json" };
654
711
  function json(body, status = 200, headers) {
655
712
  return new Response(JSON.stringify(body), {
@@ -691,19 +748,19 @@ function safeKey(value) {
691
748
  class LocalCache {
692
749
  root;
693
750
  constructor(projectRoot) {
694
- this.root = join3(projectRoot, ".omg", "cache");
751
+ this.root = join4(projectRoot, ".omg", "cache");
695
752
  mkdirSync2(this.root, { recursive: true });
696
753
  }
697
754
  read(name, fallback) {
698
- const path = join3(this.root, name);
755
+ const path = join4(this.root, name);
699
756
  if (!existsSync(path))
700
757
  return fallback;
701
758
  return JSON.parse(readFileSync3(path, "utf8"));
702
759
  }
703
760
  write(name, value) {
704
- const path = join3(this.root, name);
761
+ const path = join4(this.root, name);
705
762
  mkdirSync2(dirname2(path), { recursive: true });
706
- writeFileSync2(path, JSON.stringify(value, null, 2) + `
763
+ writeFileSync3(path, JSON.stringify(value, null, 2) + `
707
764
  `);
708
765
  }
709
766
  }
@@ -766,7 +823,7 @@ class LocalDevBackend {
766
823
  }
767
824
  subscriberCount(topic) {
768
825
  try {
769
- const raw = JSON.parse(readFileSync3(join3(this.projectRoot, ".vibes", "triggers.json"), "utf8"));
826
+ const raw = JSON.parse(readFileSync3(join4(this.projectRoot, ".vibes", "triggers.json"), "utf8"));
770
827
  const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
771
828
  return triggers.filter((trigger) => trigger.kind === "event" && trigger.key === topic).length;
772
829
  } catch {
@@ -882,7 +939,7 @@ class LocalDevBackend {
882
939
  if (bytes.byteLength > 25 * 1024 * 1024)
883
940
  return error("object exceeds 25 MB", 413);
884
941
  mkdirSync2(dirname2(path), { recursive: true });
885
- writeFileSync2(path, bytes);
942
+ writeFileSync3(path, bytes);
886
943
  const metadata2 = this.cache.read("storage-content-types.json", {});
887
944
  metadata2[`${scope}/${userId}/${key}`] = url.searchParams.get("contentType") || req.headers.get("content-type") || "application/octet-stream";
888
945
  this.cache.write("storage-content-types.json", metadata2);
@@ -914,7 +971,7 @@ class LocalDevBackend {
914
971
  const files = [];
915
972
  const walk = (dir) => {
916
973
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
917
- const path = join3(dir, entry.name);
974
+ const path = join4(dir, entry.name);
918
975
  if (entry.isDirectory())
919
976
  walk(path);
920
977
  else if (entry.isFile()) {
@@ -944,7 +1001,7 @@ class LocalDevBackend {
944
1001
  try {
945
1002
  const input = await body(req);
946
1003
  const { scope, userId, key } = this.storageParts(input);
947
- rmSync(this.storagePath(scope, userId, key), { force: true });
1004
+ rmSync2(this.storagePath(scope, userId, key), { force: true });
948
1005
  return new Response(null, { status: 204 });
949
1006
  } catch (err) {
950
1007
  return error(err instanceof Error ? err.message : String(err));
@@ -1102,9 +1159,9 @@ class LocalDevBackend {
1102
1159
  const artifact = `${jobId}.svg`;
1103
1160
  const prompt = String(input.input?.prompt ?? model);
1104
1161
  const svg = this.placeholderSvg(model, prompt);
1105
- const artifactDir = join3(this.cache.root, "media");
1162
+ const artifactDir = join4(this.cache.root, "media");
1106
1163
  mkdirSync2(artifactDir, { recursive: true });
1107
- writeFileSync2(join3(artifactDir, artifact), svg);
1164
+ writeFileSync3(join4(artifactDir, artifact), svg);
1108
1165
  jobs[jobId] = {
1109
1166
  jobId,
1110
1167
  status: "succeeded",
@@ -1137,7 +1194,7 @@ class LocalDevBackend {
1137
1194
  }
1138
1195
  getMediaArtifact(name) {
1139
1196
  const safeName = safeSegment(name, "artifact");
1140
- const path = join3(this.cache.root, "media", safeName);
1197
+ const path = join4(this.cache.root, "media", safeName);
1141
1198
  if (!existsSync(path))
1142
1199
  return error("artifact not found", 404);
1143
1200
  return new Response(readFileSync3(path), { headers: { "content-type": "image/svg+xml" } });
@@ -1262,15 +1319,15 @@ var out = (msg = "") => process.stdout.write(msg + `
1262
1319
  var step = (msg) => out(` ${msg}`);
1263
1320
  function readLink(root) {
1264
1321
  try {
1265
- return JSON.parse(readFileSync4(join4(root, LINK_FILE), "utf8"));
1322
+ return JSON.parse(readFileSync4(join5(root, LINK_FILE), "utf8"));
1266
1323
  } catch {
1267
1324
  return null;
1268
1325
  }
1269
1326
  }
1270
1327
  function writeLink(root, link) {
1271
- const path = join4(root, LINK_FILE);
1328
+ const path = join5(root, LINK_FILE);
1272
1329
  mkdirSync3(dirname3(path), { recursive: true });
1273
- writeFileSync3(path, JSON.stringify(link, null, 2) + `
1330
+ writeFileSync4(path, JSON.stringify(link, null, 2) + `
1274
1331
  `);
1275
1332
  }
1276
1333
  var HELP = `omg \u2014 deploy a local project to omg.dev
@@ -1283,6 +1340,7 @@ var HELP = `omg \u2014 deploy a local project to omg.dev
1283
1340
  omg logout
1284
1341
  omg whoami
1285
1342
  omg apps
1343
+ omg connect [--new] [--no-install]
1286
1344
  omg dev [--dir <path>] [--agent-port <port>] [--cloud]
1287
1345
 
1288
1346
  Credentials: OMG_API_KEY, or ~/.omg/credentials.json via \`omg login\`.
@@ -1336,6 +1394,15 @@ async function main() {
1336
1394
  out(` ${a.slug.padEnd(32)} ${a.name ?? ""}`);
1337
1395
  return 0;
1338
1396
  }
1397
+ case "connect": {
1398
+ const token = await requireToken();
1399
+ return runConnect({
1400
+ token,
1401
+ fresh: has("new"),
1402
+ install: !has("no-install"),
1403
+ output: out
1404
+ });
1405
+ }
1339
1406
  case "link": {
1340
1407
  const slug = argv[1];
1341
1408
  if (!slug) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omg-dev/cli",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
4
4
  "description": "Deploy and develop omg apps from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {