@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.
- package/README.md +15 -9
- package/dist/omg.mjs +318 -74
- 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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
6
|
-
import { join as
|
|
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
|
-
|
|
440
|
-
|
|
437
|
+
function deploySource(token, body) {
|
|
438
|
+
return controlPlane("/api/cli/apps/deploy-source", token, {
|
|
441
439
|
method: "POST",
|
|
442
|
-
body: JSON.stringify(
|
|
440
|
+
body: JSON.stringify(body)
|
|
443
441
|
});
|
|
444
|
-
return { id: sb.id ?? sb.sandboxId, status: sb.status };
|
|
445
442
|
}
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
459
|
-
return
|
|
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(
|
|
458
|
+
body: JSON.stringify({ slug, vars })
|
|
462
459
|
});
|
|
463
460
|
}
|
|
464
|
-
function
|
|
465
|
-
return
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
473
|
-
return controlPlane("/api/cli/
|
|
467
|
+
function envImport(token, slug, contents) {
|
|
468
|
+
return controlPlane("/api/cli/env/import", token, {
|
|
474
469
|
method: "POST",
|
|
475
|
-
body: JSON.stringify(
|
|
470
|
+
body: JSON.stringify({ slug, contents })
|
|
476
471
|
});
|
|
477
472
|
}
|
|
478
|
-
function
|
|
479
|
-
return controlPlane(
|
|
473
|
+
function getComputerConnectConfig(token) {
|
|
474
|
+
return controlPlane("/api/cli/computer/connect", token);
|
|
480
475
|
}
|
|
481
|
-
function
|
|
482
|
-
return controlPlane("/api/cli/
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
775
|
+
this.root = join4(projectRoot, ".omg", "cache");
|
|
695
776
|
mkdirSync2(this.root, { recursive: true });
|
|
696
777
|
}
|
|
697
778
|
read(name, fallback) {
|
|
698
|
-
const path =
|
|
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 =
|
|
785
|
+
const path = join4(this.root, name);
|
|
705
786
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
706
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
1186
|
+
const artifactDir = join4(this.cache.root, "media");
|
|
1106
1187
|
mkdirSync2(artifactDir, { recursive: true });
|
|
1107
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
1491
|
+
const path = join6(root, LINK_FILE);
|
|
1272
1492
|
mkdirSync3(dirname3(path), { recursive: true });
|
|
1273
|
-
|
|
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);
|