@omg-dev/cli 0.4.30 → 0.4.32

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 +318 -74
  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 readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
6
+ import { join as join6, 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,55 +430,51 @@ 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, {
437
+ function deploySource(token, body) {
438
+ return controlPlane("/api/cli/apps/deploy-source", token, {
441
439
  method: "POST",
442
- body: JSON.stringify({ size: "small", templateId, skipAppProcesses: true })
440
+ body: JSON.stringify(body)
443
441
  });
444
- return { id: sb.id ?? sb.sandboxId, status: sb.status };
445
442
  }
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`);
443
+ function getStatus(token, slug) {
444
+ return controlPlane(`/api/cli/apps/status?slug=${encodeURIComponent(slug)}`, token);
457
445
  }
458
- function writeFiles(token, id, files) {
459
- return infra(`/v1/sandboxes/${id}/files`, token, {
446
+ function listApps(token) {
447
+ return controlPlane("/api/cli/apps/list", token);
448
+ }
449
+ function envList(token, slug) {
450
+ return controlPlane(`/api/cli/env/list?slug=${encodeURIComponent(slug)}`, token);
451
+ }
452
+ function envPull(token, slug) {
453
+ return controlPlane(`/api/cli/env/pull?slug=${encodeURIComponent(slug)}`, token);
454
+ }
455
+ function envSet(token, slug, vars) {
456
+ return controlPlane("/api/cli/env/set", token, {
460
457
  method: "POST",
461
- body: JSON.stringify(files.map((f) => ({ ...f, encoding: "base64" })))
458
+ body: JSON.stringify({ slug, vars })
462
459
  });
463
460
  }
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 {}
461
+ function envRemove(token, slug, keys) {
462
+ return controlPlane("/api/cli/env/rm", token, {
463
+ method: "POST",
464
+ body: JSON.stringify({ slug, keys })
465
+ });
471
466
  }
472
- function deployFromSnapshot(token, body) {
473
- return controlPlane("/api/cli/apps/deploy", token, {
467
+ function envImport(token, slug, contents) {
468
+ return controlPlane("/api/cli/env/import", token, {
474
469
  method: "POST",
475
- body: JSON.stringify(body)
470
+ body: JSON.stringify({ slug, contents })
476
471
  });
477
472
  }
478
- function getStatus(token, slug) {
479
- return controlPlane(`/api/cli/apps/status?slug=${encodeURIComponent(slug)}`, token);
473
+ function getComputerConnectConfig(token) {
474
+ return controlPlane("/api/cli/computer/connect", token);
480
475
  }
481
- function listApps(token) {
482
- return controlPlane("/api/cli/apps/list", token);
476
+ function startComputerPairing(token) {
477
+ return controlPlane("/api/cli/computer/connect", token, { method: "POST" });
483
478
  }
484
479
 
485
480
  // src/create.ts
@@ -496,9 +491,110 @@ async function runCreate(args) {
496
491
  return child.exited;
497
492
  }
498
493
 
494
+ // src/connect.ts
495
+ import { chmodSync as chmodSync2, mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
496
+ import { homedir as homedir2, tmpdir } from "os";
497
+ import { join as join2 } from "path";
498
+ var LFG_SETUP_URL = "https://raw.githubusercontent.com/BennyKok/lfg/main/scripts/setup.sh";
499
+ async function runCommand(command, options = {}) {
500
+ const capture = options.capture === true;
501
+ const child = Bun.spawn(command, {
502
+ env: options.env,
503
+ stdin: capture ? "ignore" : "inherit",
504
+ stdout: capture ? "pipe" : "inherit",
505
+ stderr: capture ? "pipe" : "inherit"
506
+ });
507
+ const [exitCode, stdout2, stderr] = await Promise.all([
508
+ child.exited,
509
+ capture ? new Response(child.stdout).text() : "",
510
+ capture ? new Response(child.stderr).text() : ""
511
+ ]);
512
+ return { exitCode, stdout: stdout2, stderr };
513
+ }
514
+ var defaultDependencies = {
515
+ which: (name) => Bun.which(name),
516
+ runCommand,
517
+ installLfg,
518
+ getConfig: getComputerConnectConfig,
519
+ createPairing: startComputerPairing
520
+ };
521
+ function canonicalUrl(value) {
522
+ try {
523
+ const url = new URL(value);
524
+ url.pathname = url.pathname.replace(/\/$/, "") || "/";
525
+ return url.href.replace(/\/$/, "");
526
+ } catch {
527
+ return value.trim().replace(/\/$/, "");
528
+ }
529
+ }
530
+ function relayUrlFromStatus(output) {
531
+ if (output.includes("lfg connect: not paired with any relay."))
532
+ return null;
533
+ const match = output.match(/^lfg connect: paired as .+ via (\S+) \(since /m);
534
+ return match?.[1];
535
+ }
536
+ async function installLfg() {
537
+ const response = await fetch(LFG_SETUP_URL);
538
+ if (!response.ok) {
539
+ throw new Error(`Could not download the LFG installer (${response.status}).`);
540
+ }
541
+ const directory = mkdtempSync(join2(tmpdir(), "omg-lfg-install-"));
542
+ const script = join2(directory, "setup.sh");
543
+ try {
544
+ writeFileSync2(script, await response.text(), { mode: 448 });
545
+ chmodSync2(script, 448);
546
+ const result = await runCommand(["bash", script]);
547
+ if (result.exitCode !== 0) {
548
+ throw new Error(`LFG setup exited with code ${result.exitCode}.`);
549
+ }
550
+ } finally {
551
+ rmSync(directory, { recursive: true, force: true });
552
+ }
553
+ return Bun.which("lfg") || join2(homedir2(), ".local", "bin", "lfg");
554
+ }
555
+ async function runConnect(options) {
556
+ const output = options.output ?? ((message) => process.stdout.write(message + `
557
+ `));
558
+ const dependencies = { ...defaultDependencies, ...options.dependencies };
559
+ let lfg = dependencies.which("lfg");
560
+ if (!lfg) {
561
+ if (options.install === false) {
562
+ throw new Error("LFG is not installed. Install it from https://github.com/BennyKok/lfg, then rerun `omg connect`.");
563
+ }
564
+ output("LFG is not installed; installing it now\u2026");
565
+ lfg = await dependencies.installLfg();
566
+ }
567
+ const { connectUrl } = await dependencies.getConfig(options.token);
568
+ if (!connectUrl?.trim())
569
+ throw new Error("OMG returned no relay URL.");
570
+ if (!options.fresh) {
571
+ const status = await dependencies.runCommand([lfg, "connect", "status"], { capture: true });
572
+ if (status.exitCode !== 0) {
573
+ throw new Error(status.stderr.trim() || `Could not read LFG connection status (${status.exitCode}).`);
574
+ }
575
+ const savedRelay = relayUrlFromStatus(status.stdout);
576
+ if (savedRelay === undefined) {
577
+ throw new Error("This LFG version returned an unrecognized connection status. Run `lfg setup`, then retry.");
578
+ }
579
+ if (savedRelay && canonicalUrl(savedRelay) === canonicalUrl(connectUrl)) {
580
+ output("Resuming this computer's existing OMG connection\u2026");
581
+ const result2 = await dependencies.runCommand([lfg, "connect"], { env: process.env });
582
+ return result2.exitCode;
583
+ }
584
+ }
585
+ const pairing = await dependencies.createPairing(options.token);
586
+ if (!pairing.code || !pairing.connectUrl)
587
+ throw new Error("OMG returned an incomplete pairing response.");
588
+ output("Pairing this computer with OMG\u2026");
589
+ const result = await dependencies.runCommand([lfg, "connect", pairing.code], {
590
+ env: { ...process.env, LFG_RELAY_URL: pairing.connectUrl }
591
+ });
592
+ return result.exitCode;
593
+ }
594
+
499
595
  // src/files.ts
500
596
  import { readdirSync, lstatSync, readFileSync as readFileSync2 } from "fs";
501
- import { join as join2, relative, sep } from "path";
597
+ import { join as join3, relative, sep } from "path";
502
598
  var ALWAYS_SKIP = new Set([
503
599
  "node_modules",
504
600
  "dist",
@@ -522,7 +618,7 @@ function collectProjectFiles(root) {
522
618
  for (const entry of readdirSync(dir)) {
523
619
  if (ALWAYS_SKIP.has(entry))
524
620
  continue;
525
- const abs = join2(dir, entry);
621
+ const abs = join3(dir, entry);
526
622
  const st = lstatSync(abs);
527
623
  if (st.isDirectory()) {
528
624
  walk(abs);
@@ -555,7 +651,7 @@ function collectProjectFiles(root) {
555
651
  }
556
652
  function assertDeployable(root) {
557
653
  try {
558
- const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
654
+ const pkg = JSON.parse(readFileSync2(join3(root, "package.json"), "utf8"));
559
655
  if (!pkg.scripts?.build) {
560
656
  throw new Error("package.json has no `build` script \u2014 the builder runs `bun run build`, so the deploy would fail.");
561
657
  }
@@ -582,29 +678,14 @@ async function deploy(opts) {
582
678
  if (collected.files.length === 0)
583
679
  throw new Error(`No files to deploy in ${root}`);
584
680
  for (const s of collected.skippedSecrets) {
585
- onProgress(`skipping ${s} \u2014 set secrets in project settings, not the artifact`);
681
+ onProgress(`skipping ${s} \u2014 import it with \`omg env push --file ${s}\``);
586
682
  }
587
683
  for (const s of collected.skippedLarge)
588
684
  onProgress(`skipping ${s} \u2014 over the per-file limit`);
589
685
  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, {
686
+ const result = await deploySource(token, {
606
687
  name: opts.name,
607
- snapshotId,
688
+ files: collected.files.map(({ path, content }) => ({ path, content })),
608
689
  projectId: opts.projectId
609
690
  });
610
691
  return result;
@@ -644,12 +725,12 @@ import {
644
725
  mkdirSync as mkdirSync2,
645
726
  readFileSync as readFileSync3,
646
727
  readdirSync as readdirSync2,
647
- rmSync,
728
+ rmSync as rmSync2,
648
729
  statSync,
649
- writeFileSync as writeFileSync2
730
+ writeFileSync as writeFileSync3
650
731
  } from "fs";
651
732
  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";
733
+ import { dirname as dirname2, join as join4, relative as relative2, resolve, sep as sep2 } from "path";
653
734
  var jsonHeaders = { "content-type": "application/json" };
654
735
  function json(body, status = 200, headers) {
655
736
  return new Response(JSON.stringify(body), {
@@ -691,19 +772,19 @@ function safeKey(value) {
691
772
  class LocalCache {
692
773
  root;
693
774
  constructor(projectRoot) {
694
- this.root = join3(projectRoot, ".omg", "cache");
775
+ this.root = join4(projectRoot, ".omg", "cache");
695
776
  mkdirSync2(this.root, { recursive: true });
696
777
  }
697
778
  read(name, fallback) {
698
- const path = join3(this.root, name);
779
+ const path = join4(this.root, name);
699
780
  if (!existsSync(path))
700
781
  return fallback;
701
782
  return JSON.parse(readFileSync3(path, "utf8"));
702
783
  }
703
784
  write(name, value) {
704
- const path = join3(this.root, name);
785
+ const path = join4(this.root, name);
705
786
  mkdirSync2(dirname2(path), { recursive: true });
706
- writeFileSync2(path, JSON.stringify(value, null, 2) + `
787
+ writeFileSync3(path, JSON.stringify(value, null, 2) + `
707
788
  `);
708
789
  }
709
790
  }
@@ -766,7 +847,7 @@ class LocalDevBackend {
766
847
  }
767
848
  subscriberCount(topic) {
768
849
  try {
769
- const raw = JSON.parse(readFileSync3(join3(this.projectRoot, ".vibes", "triggers.json"), "utf8"));
850
+ const raw = JSON.parse(readFileSync3(join4(this.projectRoot, ".vibes", "triggers.json"), "utf8"));
770
851
  const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
771
852
  return triggers.filter((trigger) => trigger.kind === "event" && trigger.key === topic).length;
772
853
  } catch {
@@ -882,7 +963,7 @@ class LocalDevBackend {
882
963
  if (bytes.byteLength > 25 * 1024 * 1024)
883
964
  return error("object exceeds 25 MB", 413);
884
965
  mkdirSync2(dirname2(path), { recursive: true });
885
- writeFileSync2(path, bytes);
966
+ writeFileSync3(path, bytes);
886
967
  const metadata2 = this.cache.read("storage-content-types.json", {});
887
968
  metadata2[`${scope}/${userId}/${key}`] = url.searchParams.get("contentType") || req.headers.get("content-type") || "application/octet-stream";
888
969
  this.cache.write("storage-content-types.json", metadata2);
@@ -914,7 +995,7 @@ class LocalDevBackend {
914
995
  const files = [];
915
996
  const walk = (dir) => {
916
997
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
917
- const path = join3(dir, entry.name);
998
+ const path = join4(dir, entry.name);
918
999
  if (entry.isDirectory())
919
1000
  walk(path);
920
1001
  else if (entry.isFile()) {
@@ -944,7 +1025,7 @@ class LocalDevBackend {
944
1025
  try {
945
1026
  const input = await body(req);
946
1027
  const { scope, userId, key } = this.storageParts(input);
947
- rmSync(this.storagePath(scope, userId, key), { force: true });
1028
+ rmSync2(this.storagePath(scope, userId, key), { force: true });
948
1029
  return new Response(null, { status: 204 });
949
1030
  } catch (err) {
950
1031
  return error(err instanceof Error ? err.message : String(err));
@@ -1102,9 +1183,9 @@ class LocalDevBackend {
1102
1183
  const artifact = `${jobId}.svg`;
1103
1184
  const prompt = String(input.input?.prompt ?? model);
1104
1185
  const svg = this.placeholderSvg(model, prompt);
1105
- const artifactDir = join3(this.cache.root, "media");
1186
+ const artifactDir = join4(this.cache.root, "media");
1106
1187
  mkdirSync2(artifactDir, { recursive: true });
1107
- writeFileSync2(join3(artifactDir, artifact), svg);
1188
+ writeFileSync3(join4(artifactDir, artifact), svg);
1108
1189
  jobs[jobId] = {
1109
1190
  jobId,
1110
1191
  status: "succeeded",
@@ -1137,7 +1218,7 @@ class LocalDevBackend {
1137
1218
  }
1138
1219
  getMediaArtifact(name) {
1139
1220
  const safeName = safeSegment(name, "artifact");
1140
- const path = join3(this.cache.root, "media", safeName);
1221
+ const path = join4(this.cache.root, "media", safeName);
1141
1222
  if (!existsSync(path))
1142
1223
  return error("artifact not found", 404);
1143
1224
  return new Response(readFileSync3(path), { headers: { "content-type": "image/svg+xml" } });
@@ -1249,6 +1330,145 @@ async function runDev(options) {
1249
1330
  }
1250
1331
  }
1251
1332
 
1333
+ // src/env.ts
1334
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync2 } from "fs";
1335
+ import { join as join5 } from "path";
1336
+ async function defaultReadStdin() {
1337
+ const chunks = [];
1338
+ for await (const chunk of process.stdin)
1339
+ chunks.push(chunk);
1340
+ return Buffer.concat(chunks).toString("utf8");
1341
+ }
1342
+ function formatList(vars, out) {
1343
+ if (!vars.length) {
1344
+ out("No env vars set.");
1345
+ out("");
1346
+ out(" omg env set KEY=value set one");
1347
+ out(" omg env push --file .env import a local .env");
1348
+ return;
1349
+ }
1350
+ const width = Math.max(...vars.map((v) => v.key.length));
1351
+ for (const v of vars)
1352
+ out(` ${v.key.padEnd(width)} ${v.preview}`);
1353
+ }
1354
+ async function collectAssignments(args, readStdin) {
1355
+ const vars = {};
1356
+ for (let i = 0;i < args.length; i++) {
1357
+ const arg = args[i];
1358
+ const eq = arg.indexOf("=");
1359
+ if (eq > 0) {
1360
+ vars[arg.slice(0, eq)] = arg.slice(eq + 1);
1361
+ continue;
1362
+ }
1363
+ if (args[i + 1] === "-") {
1364
+ vars[arg] = (await readStdin()).replace(/\n$/, "");
1365
+ i++;
1366
+ continue;
1367
+ }
1368
+ throw new Error(`Expected KEY=value (or \`${arg} -\` to read the value from stdin), got "${arg}"`);
1369
+ }
1370
+ return vars;
1371
+ }
1372
+ function toDotenv(env) {
1373
+ return Object.keys(env).sort().map((k) => {
1374
+ const v = env[k] ?? "";
1375
+ const needsQuotes = /[\s"'#$`\\]/.test(v) || v === "";
1376
+ return `${k}=${needsQuotes ? JSON.stringify(v) : v}`;
1377
+ }).join(`
1378
+ `) + `
1379
+ `;
1380
+ }
1381
+ async function runEnv(opts) {
1382
+ const { args, slug, token, root, flag, out } = opts;
1383
+ const readStdin = opts.readStdin ?? defaultReadStdin;
1384
+ const sub = args[0] ?? "list";
1385
+ const rest = args.slice(1);
1386
+ switch (sub) {
1387
+ case "list":
1388
+ case "ls": {
1389
+ const { vars } = await envList(token, slug);
1390
+ formatList(vars, out);
1391
+ return 0;
1392
+ }
1393
+ case "set": {
1394
+ if (!rest.length) {
1395
+ out("Usage: omg env set KEY=value [KEY2=value2 ...]");
1396
+ out(" omg env set KEY - read the value from stdin");
1397
+ return 1;
1398
+ }
1399
+ const vars = await collectAssignments(rest, readStdin);
1400
+ const res = await envSet(token, slug, vars);
1401
+ for (const k of res.created)
1402
+ out(` + ${k}`);
1403
+ for (const k of res.updated)
1404
+ out(` ~ ${k} (updated)`);
1405
+ out("");
1406
+ out("Applies on next `omg deploy`.");
1407
+ return 0;
1408
+ }
1409
+ case "rm":
1410
+ case "remove":
1411
+ case "unset": {
1412
+ if (!rest.length) {
1413
+ out("Usage: omg env rm KEY [KEY2 ...]");
1414
+ return 1;
1415
+ }
1416
+ const res = await envRemove(token, slug, rest);
1417
+ for (const k of res.removed)
1418
+ out(` - ${k}`);
1419
+ for (const k of res.missing)
1420
+ out(` ? ${k} was not set`);
1421
+ if (res.removed.length) {
1422
+ out("");
1423
+ out("Applies on next `omg deploy`.");
1424
+ }
1425
+ return res.removed.length === 0 && res.missing.length ? 1 : 0;
1426
+ }
1427
+ case "pull": {
1428
+ const target = join5(root, flag("out") ?? ".env");
1429
+ if (existsSync2(target) && !args.includes("--force")) {
1430
+ out(`${target} already exists. Re-run with --force to overwrite.`);
1431
+ return 1;
1432
+ }
1433
+ const { env } = await envPull(token, slug);
1434
+ const keys = Object.keys(env);
1435
+ if (!keys.length) {
1436
+ out(`No env vars set for ${slug} \u2014 nothing to pull.`);
1437
+ return 0;
1438
+ }
1439
+ writeFileSync4(target, toDotenv(env), { mode: 384 });
1440
+ out(`Wrote ${keys.length} var${keys.length === 1 ? "" : "s"} to ${target}`);
1441
+ out("Add it to .gitignore \u2014 it holds real secrets.");
1442
+ return 0;
1443
+ }
1444
+ case "push":
1445
+ case "import": {
1446
+ const source = join5(root, flag("file") ?? ".env");
1447
+ if (!existsSync2(source)) {
1448
+ out(`No file at ${source}. Point at one with --file <path>.`);
1449
+ return 1;
1450
+ }
1451
+ const res = await envImport(token, slug, readFileSync4(source, "utf8"));
1452
+ for (const k of res.created)
1453
+ out(` + ${k}`);
1454
+ for (const k of res.updated)
1455
+ out(` ~ ${k} (updated)`);
1456
+ out("");
1457
+ out("Applies on next `omg deploy`.");
1458
+ return 0;
1459
+ }
1460
+ default:
1461
+ out(`Unknown: omg env ${sub}`);
1462
+ out("");
1463
+ out(" omg env list (masked)");
1464
+ out(" omg env set KEY=value set one or many");
1465
+ out(" omg env rm KEY remove");
1466
+ out(" omg env pull [--out .env] write real values locally");
1467
+ out(" omg env push [--file .env] import a local .env");
1468
+ return 1;
1469
+ }
1470
+ }
1471
+
1252
1472
  // src/index.ts
1253
1473
  var argv = process.argv.slice(2);
1254
1474
  var cmd = argv[0] ?? "help";
@@ -1262,15 +1482,15 @@ var out = (msg = "") => process.stdout.write(msg + `
1262
1482
  var step = (msg) => out(` ${msg}`);
1263
1483
  function readLink(root) {
1264
1484
  try {
1265
- return JSON.parse(readFileSync4(join4(root, LINK_FILE), "utf8"));
1485
+ return JSON.parse(readFileSync5(join6(root, LINK_FILE), "utf8"));
1266
1486
  } catch {
1267
1487
  return null;
1268
1488
  }
1269
1489
  }
1270
1490
  function writeLink(root, link) {
1271
- const path = join4(root, LINK_FILE);
1491
+ const path = join6(root, LINK_FILE);
1272
1492
  mkdirSync3(dirname3(path), { recursive: true });
1273
- writeFileSync3(path, JSON.stringify(link, null, 2) + `
1493
+ writeFileSync5(path, JSON.stringify(link, null, 2) + `
1274
1494
  `);
1275
1495
  }
1276
1496
  var HELP = `omg \u2014 deploy a local project to omg.dev
@@ -1278,11 +1498,17 @@ var HELP = `omg \u2014 deploy a local project to omg.dev
1278
1498
  omg create <name> [--no-install]
1279
1499
  omg deploy [--name <name>] [--dir <path>] [--no-wait]
1280
1500
  omg status [--dir <path>]
1501
+ omg env list env vars (masked)
1502
+ omg env set KEY=value [KEY2=v2 ...] set (KEY - reads stdin)
1503
+ omg env rm KEY [KEY2 ...] remove
1504
+ omg env pull [--out .env] [--force] write real values locally
1505
+ omg env push [--file .env] import a local .env
1281
1506
  omg link <slug> [--dir <path>]
1282
1507
  omg login [--token <omg_sk_...>]
1283
1508
  omg logout
1284
1509
  omg whoami
1285
1510
  omg apps
1511
+ omg connect [--new] [--no-install]
1286
1512
  omg dev [--dir <path>] [--agent-port <port>] [--cloud]
1287
1513
 
1288
1514
  Credentials: OMG_API_KEY, or ~/.omg/credentials.json via \`omg login\`.
@@ -1336,6 +1562,15 @@ async function main() {
1336
1562
  out(` ${a.slug.padEnd(32)} ${a.name ?? ""}`);
1337
1563
  return 0;
1338
1564
  }
1565
+ case "connect": {
1566
+ const token = await requireToken();
1567
+ return runConnect({
1568
+ token,
1569
+ fresh: has("new"),
1570
+ install: !has("no-install"),
1571
+ output: out
1572
+ });
1573
+ }
1339
1574
  case "link": {
1340
1575
  const slug = argv[1];
1341
1576
  if (!slug) {
@@ -1368,6 +1603,15 @@ async function main() {
1368
1603
  out(`${link.slug}: ${status}`);
1369
1604
  return 0;
1370
1605
  }
1606
+ case "env": {
1607
+ const token = await requireToken();
1608
+ const link = readLink(root);
1609
+ if (!link?.slug) {
1610
+ out("Not linked to an app. Run `omg deploy` or `omg link <slug>` first.");
1611
+ return 1;
1612
+ }
1613
+ return runEnv({ args: argv.slice(1), slug: link.slug, token, root, flag, out });
1614
+ }
1371
1615
  case "deploy": {
1372
1616
  const token = await requireToken();
1373
1617
  const link = readLink(root);
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.32",
4
4
  "description": "Deploy and develop omg apps from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {