@omg-dev/cli 0.4.30

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 +65 -0
  2. package/dist/omg.mjs +1426 -0
  3. package/package.json +37 -0
package/dist/omg.mjs ADDED
@@ -0,0 +1,1426 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
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";
7
+
8
+ // src/config.ts
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+
12
+ // ../auth/src/oauth.ts
13
+ var MCP_OAUTH_RESOURCE = "https://mcp.omg.dev";
14
+ var MCP_OAUTH_AUDIENCES = [
15
+ MCP_OAUTH_RESOURCE,
16
+ `${MCP_OAUTH_RESOURCE}/`,
17
+ `${MCP_OAUTH_RESOURCE}/mcp`
18
+ ];
19
+ var CLI_OAUTH_RESOURCE = "https://backend.omg.dev/api/cli";
20
+ var CLI_OAUTH_AUDIENCES = [CLI_OAUTH_RESOURCE];
21
+ var OAUTH_PROVIDER_AUDIENCES = [
22
+ ...MCP_OAUTH_AUDIENCES,
23
+ ...CLI_OAUTH_AUDIENCES
24
+ ];
25
+ var CLI_API_OAUTH_AUDIENCES = [
26
+ ...MCP_OAUTH_AUDIENCES,
27
+ ...CLI_OAUTH_AUDIENCES
28
+ ];
29
+
30
+ // src/config.ts
31
+ var INFRA_URL = process.env.OMG_INFRA_URL?.trim() || "https://infra.omg.dev";
32
+ var CONTROL_PLANE_URL = process.env.OMG_API_URL?.trim() || "https://backend.omg.dev";
33
+ var AUTH_URL = process.env.OMG_AUTH_URL?.trim() || "https://auth.omg.dev";
34
+ var OAUTH_RESOURCE = process.env.OMG_OAUTH_RESOURCE?.trim() || CLI_OAUTH_RESOURCE;
35
+ var CREDENTIALS_PATH = join(homedir(), ".omg", "credentials.json");
36
+ var LINK_DIR = ".omg";
37
+ var LINK_FILE = join(LINK_DIR, "project.json");
38
+
39
+ // src/auth.ts
40
+ import {
41
+ chmodSync,
42
+ mkdirSync,
43
+ readFileSync,
44
+ renameSync,
45
+ unlinkSync,
46
+ writeFileSync
47
+ } from "fs";
48
+ import { createHash, randomBytes, randomUUID } from "crypto";
49
+ import { createInterface } from "readline/promises";
50
+ import { dirname } from "path";
51
+ import { stdin, stdout } from "process";
52
+ var REFRESH_WINDOW_MS = 5 * 60 * 1000;
53
+ var OAUTH_SCOPES = "openid email omg:apps offline_access";
54
+
55
+ class AuthError extends Error {
56
+ }
57
+ function isNodeError(error, code) {
58
+ return error instanceof Error && "code" in error && error.code === code;
59
+ }
60
+ function loadCredentials(path = CREDENTIALS_PATH) {
61
+ const envKey = process.env.OMG_API_KEY?.trim();
62
+ if (envKey)
63
+ return { token: envKey, kind: "api-key" };
64
+ try {
65
+ const raw = JSON.parse(readFileSync(path, "utf8"));
66
+ if (!raw?.token)
67
+ return null;
68
+ return raw;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function saveCredentials(creds, path = CREDENTIALS_PATH) {
74
+ const directory = dirname(path);
75
+ mkdirSync(directory, { recursive: true, mode: 448 });
76
+ chmodSync(directory, 448);
77
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
78
+ try {
79
+ writeFileSync(temporaryPath, JSON.stringify(creds, null, 2), { mode: 384 });
80
+ renameSync(temporaryPath, path);
81
+ chmodSync(path, 384);
82
+ } catch (error) {
83
+ try {
84
+ unlinkSync(temporaryPath);
85
+ } catch {}
86
+ throw error;
87
+ }
88
+ }
89
+ function clearCredentials(path = CREDENTIALS_PATH) {
90
+ try {
91
+ unlinkSync(path);
92
+ return true;
93
+ } catch (error) {
94
+ if (isNodeError(error, "ENOENT"))
95
+ return false;
96
+ throw error;
97
+ }
98
+ }
99
+ function base64Url(bytes) {
100
+ return Buffer.from(bytes).toString("base64url");
101
+ }
102
+ function pkceChallenge(verifier) {
103
+ return createHash("sha256").update(verifier).digest("base64url");
104
+ }
105
+ async function oauthError(response, action) {
106
+ const text = await response.text();
107
+ let detail = text;
108
+ try {
109
+ const body = JSON.parse(text);
110
+ detail = body.error_description || body.error || text;
111
+ } catch {}
112
+ return new AuthError(`${action} failed (${response.status}): ${detail || response.statusText}`);
113
+ }
114
+ async function registerOAuthClient(redirectUri, options = {}) {
115
+ const authUrl = options.authUrl ?? AUTH_URL;
116
+ const fetchFn = options.fetch ?? fetch;
117
+ const response = await fetchFn(`${authUrl}/api/auth/oauth2/register`, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
120
+ body: JSON.stringify({
121
+ client_name: "omg CLI",
122
+ redirect_uris: [redirectUri],
123
+ grant_types: ["authorization_code", "refresh_token"],
124
+ response_types: ["code"],
125
+ token_endpoint_auth_method: "none"
126
+ })
127
+ });
128
+ if (!response.ok)
129
+ throw await oauthError(response, "Client registration");
130
+ const client = await response.json();
131
+ if (!client.client_id)
132
+ throw new AuthError("Client registration returned no client_id.");
133
+ return { client_id: client.client_id };
134
+ }
135
+ async function requestToken(params, options) {
136
+ const authUrl = options.authUrl ?? AUTH_URL;
137
+ const fetchFn = options.fetch ?? fetch;
138
+ const response = await fetchFn(`${authUrl}/api/auth/oauth2/token`, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/x-www-form-urlencoded",
142
+ Accept: "application/json"
143
+ },
144
+ body: params
145
+ });
146
+ if (!response.ok)
147
+ throw await oauthError(response, options.action);
148
+ const token = await response.json();
149
+ if (!token.access_token || !Number.isFinite(token.expires_in) || Number(token.expires_in) <= 0) {
150
+ throw new AuthError(`${options.action} returned an incomplete token response.`);
151
+ }
152
+ return {
153
+ access_token: token.access_token,
154
+ refresh_token: token.refresh_token,
155
+ expires_in: Number(token.expires_in)
156
+ };
157
+ }
158
+ function exchangeAuthorizationCode(args, options = {}) {
159
+ return requestToken(new URLSearchParams({
160
+ grant_type: "authorization_code",
161
+ code: args.code,
162
+ redirect_uri: args.redirectUri,
163
+ client_id: args.clientId,
164
+ code_verifier: args.verifier,
165
+ resource: args.resource ?? OAUTH_RESOURCE
166
+ }), { ...options, action: "Token exchange" });
167
+ }
168
+ function isHeadless() {
169
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY)
170
+ return true;
171
+ return process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
172
+ }
173
+ function openSystemBrowser(url) {
174
+ if (isHeadless())
175
+ return false;
176
+ let command;
177
+ if (process.platform === "darwin") {
178
+ command = ["open", url];
179
+ } else if (process.platform === "win32") {
180
+ command = ["cmd", "/c", "start", "", url];
181
+ } else {
182
+ command = ["xdg-open", url];
183
+ }
184
+ if (process.platform !== "win32" && !Bun.which(command[0]))
185
+ return false;
186
+ try {
187
+ const child = Bun.spawn(command, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
188
+ child.unref();
189
+ return true;
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+ function callbackCode(value, expectedState) {
195
+ const pasted = value.trim();
196
+ if (!pasted)
197
+ throw new AuthError("No authorization code was provided.");
198
+ if (!/^https?:\/\//i.test(pasted))
199
+ return pasted;
200
+ let url;
201
+ try {
202
+ url = new URL(pasted);
203
+ } catch {
204
+ throw new AuthError("The pasted callback URL is invalid.");
205
+ }
206
+ const error = url.searchParams.get("error");
207
+ if (error) {
208
+ throw new AuthError(`Authorization failed: ${url.searchParams.get("error_description") || error}`);
209
+ }
210
+ if (url.searchParams.get("state") !== expectedState) {
211
+ throw new AuthError("Authorization state did not match. Run `omg login` again.");
212
+ }
213
+ const code = url.searchParams.get("code");
214
+ if (!code)
215
+ throw new AuthError("The callback URL contained no authorization code.");
216
+ return code;
217
+ }
218
+ function callbackPage(ok, message) {
219
+ const title = ok ? "Signed in to omg" : "Could not sign in";
220
+ const safeMessage = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
221
+ return new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${title}</title><body style="font:16px system-ui;max-width:36rem;margin:15vh auto;padding:0 1.5rem"><h1>${title}</h1><p>${safeMessage}</p></body>`, {
222
+ status: ok ? 200 : 400,
223
+ headers: { "Content-Type": "text/html; charset=utf-8" }
224
+ });
225
+ }
226
+ function createLoopbackReceiver(expectedState) {
227
+ let resolveCode;
228
+ let rejectCode;
229
+ let settled = false;
230
+ const code = new Promise((resolve, reject) => {
231
+ resolveCode = resolve;
232
+ rejectCode = reject;
233
+ });
234
+ const server = Bun.serve({
235
+ hostname: "127.0.0.1",
236
+ port: 0,
237
+ fetch(request) {
238
+ const url = new URL(request.url);
239
+ if (url.pathname !== "/callback")
240
+ return new Response(`not found
241
+ `, { status: 404 });
242
+ try {
243
+ const value = callbackCode(url.toString(), expectedState);
244
+ if (!settled) {
245
+ settled = true;
246
+ resolveCode(value);
247
+ }
248
+ return callbackPage(true, "You can close this window and return to your terminal.");
249
+ } catch (error) {
250
+ const authError = error instanceof Error ? error : new AuthError("The authorization callback failed.");
251
+ if (!settled) {
252
+ settled = true;
253
+ rejectCode(authError);
254
+ }
255
+ return callbackPage(false, authError.message);
256
+ }
257
+ }
258
+ });
259
+ return {
260
+ redirectUri: `http://127.0.0.1:${server.port}/callback`,
261
+ code,
262
+ stop: () => server.stop(true)
263
+ };
264
+ }
265
+ async function readCodeFromTerminal(authorizeUrl, signal) {
266
+ if (!stdin.isTTY) {
267
+ throw new AuthError(`No browser or interactive terminal is available. Open this URL elsewhere and rerun in an interactive shell:
268
+ ${authorizeUrl}`);
269
+ }
270
+ const reader = createInterface({ input: stdin, output: stdout });
271
+ try {
272
+ return await reader.question("Paste the callback URL or code here: ", { signal });
273
+ } finally {
274
+ reader.close();
275
+ }
276
+ }
277
+ async function loginWithBrowser(options = {}) {
278
+ const authUrl = options.authUrl ?? AUTH_URL;
279
+ const resource = options.resource ?? OAUTH_RESOURCE;
280
+ const fetchFn = options.fetch ?? fetch;
281
+ const output = options.output ?? console.log;
282
+ const state = base64Url(randomBytes(32));
283
+ const verifier = base64Url(randomBytes(64));
284
+ const receiver = createLoopbackReceiver(state);
285
+ try {
286
+ const client = await registerOAuthClient(receiver.redirectUri, { authUrl, fetch: fetchFn });
287
+ const authorizeUrl = new URL(`${authUrl}/api/auth/oauth2/authorize`);
288
+ authorizeUrl.search = new URLSearchParams({
289
+ response_type: "code",
290
+ client_id: client.client_id,
291
+ redirect_uri: receiver.redirectUri,
292
+ scope: OAUTH_SCOPES,
293
+ code_challenge: pkceChallenge(verifier),
294
+ code_challenge_method: "S256",
295
+ state,
296
+ resource
297
+ }).toString();
298
+ const openBrowser = options.openBrowser ?? openSystemBrowser;
299
+ const browserOpened = await openBrowser(authorizeUrl.toString());
300
+ if (browserOpened) {
301
+ output("Opening your browser to sign in\u2026");
302
+ } else {
303
+ output("Open this URL in a browser:");
304
+ output(authorizeUrl.toString());
305
+ }
306
+ let code;
307
+ if (browserOpened) {
308
+ code = await receiver.code;
309
+ } else {
310
+ const terminalRead = new AbortController;
311
+ const pastedCode = (async () => {
312
+ const pasted = options.readCode ? await options.readCode(authorizeUrl.toString()) : await readCodeFromTerminal(authorizeUrl.toString(), terminalRead.signal);
313
+ return callbackCode(pasted, state);
314
+ })();
315
+ try {
316
+ code = await Promise.race([receiver.code, pastedCode]);
317
+ } finally {
318
+ terminalRead.abort();
319
+ receiver.code.catch(() => {});
320
+ }
321
+ }
322
+ const token = await exchangeAuthorizationCode({
323
+ code,
324
+ clientId: client.client_id,
325
+ redirectUri: receiver.redirectUri,
326
+ verifier,
327
+ resource
328
+ }, { authUrl, fetch: fetchFn });
329
+ if (!token.refresh_token) {
330
+ throw new AuthError("The authorization server returned no refresh token. Run `omg login` again.");
331
+ }
332
+ const credentials = {
333
+ token: token.access_token,
334
+ refreshToken: token.refresh_token,
335
+ clientId: client.client_id,
336
+ expiresAt: Date.now() + token.expires_in * 1000,
337
+ authUrl,
338
+ resource,
339
+ kind: "oauth"
340
+ };
341
+ saveCredentials(credentials, options.credentialPath);
342
+ return credentials;
343
+ } finally {
344
+ receiver.stop();
345
+ }
346
+ }
347
+ async function refreshOAuthCredentials(credentials, options = {}) {
348
+ if (credentials.kind !== "oauth" || !credentials.refreshToken || !credentials.clientId) {
349
+ throw new AuthError("Session cannot be refreshed. Run `omg login` again.");
350
+ }
351
+ const authUrl = credentials.authUrl ?? AUTH_URL;
352
+ const resource = credentials.resource ?? OAUTH_RESOURCE;
353
+ const token = await requestToken(new URLSearchParams({
354
+ grant_type: "refresh_token",
355
+ refresh_token: credentials.refreshToken,
356
+ client_id: credentials.clientId,
357
+ resource
358
+ }), {
359
+ authUrl,
360
+ fetch: options.fetch,
361
+ action: "Session refresh"
362
+ });
363
+ const refreshed = {
364
+ ...credentials,
365
+ token: token.access_token,
366
+ refreshToken: token.refresh_token ?? credentials.refreshToken,
367
+ expiresAt: (options.now ?? Date.now()) + token.expires_in * 1000
368
+ };
369
+ saveCredentials(refreshed, options.credentialPath);
370
+ return refreshed;
371
+ }
372
+ async function requireToken(options = {}) {
373
+ const creds = loadCredentials(options.credentialPath);
374
+ if (!creds) {
375
+ throw new AuthError("Not signed in. Run `omg login`, set OMG_API_KEY, or use `omg login --token omg_sk_...`.");
376
+ }
377
+ if (creds.kind === "api-key" || !creds.expiresAt)
378
+ return creds.token;
379
+ const now = options.now ?? Date.now();
380
+ if (creds.kind === "oauth" && creds.expiresAt <= now + REFRESH_WINDOW_MS) {
381
+ const refreshed = await refreshOAuthCredentials(creds, {
382
+ credentialPath: options.credentialPath,
383
+ fetch: options.fetch,
384
+ now
385
+ });
386
+ return refreshed.token;
387
+ }
388
+ if (creds.expiresAt <= now) {
389
+ throw new AuthError("Session expired. Run `omg login` again.");
390
+ }
391
+ return creds.token;
392
+ }
393
+
394
+ // src/api.ts
395
+ class ApiError extends Error {
396
+ status;
397
+ path;
398
+ body;
399
+ constructor(status, path, body) {
400
+ let detail = body;
401
+ try {
402
+ const parsed = JSON.parse(body);
403
+ if (parsed?.error)
404
+ detail = parsed.error;
405
+ } catch {}
406
+ super(`${path} \u2192 ${status}: ${detail.slice(0, 400)}`);
407
+ this.status = status;
408
+ this.path = path;
409
+ this.body = body;
410
+ }
411
+ }
412
+ var UA = "omg-cli/0.4.27";
413
+ async function request(base, path, token, init = {}) {
414
+ const res = await fetch(base + path, {
415
+ ...init,
416
+ headers: {
417
+ Authorization: `Bearer ${token}`,
418
+ "User-Agent": UA,
419
+ "Content-Type": "application/json",
420
+ ...init.headers ?? {}
421
+ }
422
+ });
423
+ const text = await res.text();
424
+ if (!res.ok)
425
+ throw new ApiError(res.status, path, text);
426
+ if (!text)
427
+ return null;
428
+ try {
429
+ return JSON.parse(text);
430
+ } catch {
431
+ return text;
432
+ }
433
+ }
434
+ var infra = (path, token, init) => request(INFRA_URL, path, token, init);
435
+ var controlPlane = (path, token, init) => request(CONTROL_PLANE_URL, path, token, init);
436
+ function whoAmI(token) {
437
+ return controlPlane("/api/cli/whoami", token);
438
+ }
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, {
474
+ method: "POST",
475
+ body: JSON.stringify(body)
476
+ });
477
+ }
478
+ function getStatus(token, slug) {
479
+ return controlPlane(`/api/cli/apps/status?slug=${encodeURIComponent(slug)}`, token);
480
+ }
481
+ function listApps(token) {
482
+ return controlPlane("/api/cli/apps/list", token);
483
+ }
484
+
485
+ // src/create.ts
486
+ var DEFAULT_GENERATOR = "create-omg@latest";
487
+ function createCommand(args, generator = process.env.OMG_CREATE_PACKAGE?.trim() || DEFAULT_GENERATOR) {
488
+ return ["bunx", "--package", generator, "create-omg", ...args];
489
+ }
490
+ async function runCreate(args) {
491
+ const child = Bun.spawn(createCommand(args), {
492
+ stdin: "inherit",
493
+ stdout: "inherit",
494
+ stderr: "inherit"
495
+ });
496
+ return child.exited;
497
+ }
498
+
499
+ // src/files.ts
500
+ import { readdirSync, lstatSync, readFileSync as readFileSync2 } from "fs";
501
+ import { join as join2, relative, sep } from "path";
502
+ var ALWAYS_SKIP = new Set([
503
+ "node_modules",
504
+ "dist",
505
+ ".vibes",
506
+ ".git",
507
+ ".omg",
508
+ ".DS_Store",
509
+ ".next",
510
+ ".turbo",
511
+ "coverage"
512
+ ]);
513
+ var GUEST_PROJECT_ROOT = "/home/user/project";
514
+ var MAX_FILE_BYTES = 2000000;
515
+ var MAX_TOTAL_BYTES = 40000000;
516
+ function collectProjectFiles(root) {
517
+ const files = [];
518
+ const skippedLarge = [];
519
+ const skippedSecrets = [];
520
+ let totalBytes = 0;
521
+ const walk = (dir) => {
522
+ for (const entry of readdirSync(dir)) {
523
+ if (ALWAYS_SKIP.has(entry))
524
+ continue;
525
+ const abs = join2(dir, entry);
526
+ const st = lstatSync(abs);
527
+ if (st.isDirectory()) {
528
+ walk(abs);
529
+ continue;
530
+ }
531
+ if (!st.isFile())
532
+ continue;
533
+ const rel = relative(root, abs);
534
+ if (entry === ".env" || entry.startsWith(".env.")) {
535
+ skippedSecrets.push(rel);
536
+ continue;
537
+ }
538
+ if (st.size > MAX_FILE_BYTES) {
539
+ skippedLarge.push(rel);
540
+ continue;
541
+ }
542
+ totalBytes += st.size;
543
+ if (totalBytes > MAX_TOTAL_BYTES) {
544
+ throw new Error(`Project exceeds ${(MAX_TOTAL_BYTES / 1e6).toFixed(0)} MB of source. ` + `Large assets belong in storage, not the repo.`);
545
+ }
546
+ files.push({
547
+ path: `${GUEST_PROJECT_ROOT}/${rel.split(sep).join("/")}`,
548
+ content: readFileSync2(abs).toString("base64"),
549
+ bytes: st.size
550
+ });
551
+ }
552
+ };
553
+ walk(root);
554
+ return { files, totalBytes, skippedLarge, skippedSecrets };
555
+ }
556
+ function assertDeployable(root) {
557
+ try {
558
+ const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
559
+ if (!pkg.scripts?.build) {
560
+ throw new Error("package.json has no `build` script \u2014 the builder runs `bun run build`, so the deploy would fail.");
561
+ }
562
+ } catch (err) {
563
+ if (err instanceof SyntaxError)
564
+ throw new Error("package.json is not valid JSON");
565
+ if (err instanceof Error && err.message.includes("build` script"))
566
+ throw err;
567
+ throw new Error(`No package.json in ${root} \u2014 run \`omg deploy\` from your project root.`);
568
+ }
569
+ }
570
+
571
+ // src/deploy.ts
572
+ var READY = new Set(["ready", "succeeded"]);
573
+ var FAILED = new Set(["failed", "error"]);
574
+ async function deploy(opts) {
575
+ const { root, token, onProgress = () => {} } = opts;
576
+ const ingress = opts.ingress ?? "source";
577
+ if (ingress === "artifact") {
578
+ throw new Error("The artifact ingress is not implemented yet \u2014 it needs POST /v1/artifacts on infra " + "(see apps/infra/EXTERNAL_DEPLOY.md). Use the default source ingress.");
579
+ }
580
+ assertDeployable(root);
581
+ const collected = collectProjectFiles(root);
582
+ if (collected.files.length === 0)
583
+ throw new Error(`No files to deploy in ${root}`);
584
+ for (const s of collected.skippedSecrets) {
585
+ onProgress(`skipping ${s} \u2014 set secrets in project settings, not the artifact`);
586
+ }
587
+ for (const s of collected.skippedLarge)
588
+ onProgress(`skipping ${s} \u2014 over the per-file limit`);
589
+ 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, {
606
+ name: opts.name,
607
+ snapshotId,
608
+ projectId: opts.projectId
609
+ });
610
+ return result;
611
+ }
612
+ async function waitForDeploy(token, slug, onProgress = () => {}, timeoutMs = 8 * 60000) {
613
+ const deadline = Date.now() + timeoutMs;
614
+ let last = "";
615
+ while (Date.now() < deadline) {
616
+ let st;
617
+ try {
618
+ st = await getStatus(token, slug);
619
+ } catch {
620
+ await new Promise((r) => setTimeout(r, 4000));
621
+ continue;
622
+ }
623
+ const status = String(st?.status ?? st?.deployStatus ?? "building");
624
+ if (status !== last) {
625
+ onProgress(status + (st?.phase ? ` (${st.phase})` : ""));
626
+ last = status;
627
+ }
628
+ if (READY.has(status))
629
+ return status;
630
+ if (FAILED.has(status)) {
631
+ throw new Error(`build failed: ${st?.buildError || st?.error || "no detail returned"}`);
632
+ }
633
+ await new Promise((r) => setTimeout(r, 4000));
634
+ }
635
+ throw new Error(`still building after ${Math.round(timeoutMs / 60000)}m \u2014 check \`omg status\` or the dashboard`);
636
+ }
637
+
638
+ // src/dev.ts
639
+ import { resolve as resolve2 } from "path";
640
+
641
+ // src/dev-backend.ts
642
+ import {
643
+ existsSync,
644
+ mkdirSync as mkdirSync2,
645
+ readFileSync as readFileSync3,
646
+ readdirSync as readdirSync2,
647
+ rmSync,
648
+ statSync,
649
+ writeFileSync as writeFileSync2
650
+ } from "fs";
651
+ 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";
653
+ var jsonHeaders = { "content-type": "application/json" };
654
+ function json(body, status = 200, headers) {
655
+ return new Response(JSON.stringify(body), {
656
+ status,
657
+ headers: { ...jsonHeaders, ...headers }
658
+ });
659
+ }
660
+ function error(message, status = 400) {
661
+ return json({ error: message }, status);
662
+ }
663
+ async function body(req) {
664
+ try {
665
+ return await req.json();
666
+ } catch {
667
+ throw new Error("invalid JSON body");
668
+ }
669
+ }
670
+ function stableId(prefix, value) {
671
+ const hash = createHash2("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 20);
672
+ return `${prefix}_${hash}`;
673
+ }
674
+ function safeSegment(value, label) {
675
+ const segment = String(value ?? "");
676
+ if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\")) {
677
+ throw new Error(`${label} is invalid`);
678
+ }
679
+ return segment;
680
+ }
681
+ function safeKey(value) {
682
+ const key = String(value ?? "").replace(/^\/+/, "");
683
+ if (!/^[a-zA-Z0-9._\-/]{1,256}$/.test(key))
684
+ throw new Error("invalid key");
685
+ const parts = key.split("/");
686
+ if (parts.some((part) => !part || part === "." || part === ".."))
687
+ throw new Error("invalid key");
688
+ return key;
689
+ }
690
+
691
+ class LocalCache {
692
+ root;
693
+ constructor(projectRoot) {
694
+ this.root = join3(projectRoot, ".omg", "cache");
695
+ mkdirSync2(this.root, { recursive: true });
696
+ }
697
+ read(name, fallback) {
698
+ const path = join3(this.root, name);
699
+ if (!existsSync(path))
700
+ return fallback;
701
+ return JSON.parse(readFileSync3(path, "utf8"));
702
+ }
703
+ write(name, value) {
704
+ const path = join3(this.root, name);
705
+ mkdirSync2(dirname2(path), { recursive: true });
706
+ writeFileSync2(path, JSON.stringify(value, null, 2) + `
707
+ `);
708
+ }
709
+ }
710
+
711
+ class LocalDevBackend {
712
+ name = "local";
713
+ cache;
714
+ projectRoot;
715
+ constructor(projectRoot) {
716
+ this.projectRoot = resolve(projectRoot);
717
+ this.cache = new LocalCache(this.projectRoot);
718
+ }
719
+ async handle(req, url, origin) {
720
+ const path = url.pathname;
721
+ if (req.method === "GET" && path === "/health") {
722
+ return json({ ok: true, backend: this.name });
723
+ }
724
+ if (req.method === "POST" && path === "/_emit")
725
+ return this.emit(req);
726
+ if (req.method === "POST" && path === "/_schedule")
727
+ return this.schedule(req);
728
+ if (req.method === "DELETE" && path.startsWith("/_schedule/")) {
729
+ return this.cancelSchedule(decodeURIComponent(path.slice("/_schedule/".length)));
730
+ }
731
+ if (req.method === "POST" && path === "/_workflow/start")
732
+ return this.startWorkflow(req);
733
+ if (req.method === "POST" && path === "/_storage/presign") {
734
+ return this.presignStorage(req, origin);
735
+ }
736
+ if (req.method === "POST" && path === "/_storage/list")
737
+ return this.listStorage(req);
738
+ if (req.method === "POST" && path === "/_storage/delete")
739
+ return this.deleteStorage(req);
740
+ if ((req.method === "PUT" || req.method === "GET") && path === "/_storage/object") {
741
+ return this.storageObject(req, url);
742
+ }
743
+ if (path.startsWith("/_billing/"))
744
+ return this.billing(req, url);
745
+ if (req.method === "GET" && path.startsWith("/_checkout/")) {
746
+ return new Response("<!doctype html><title>omg dev checkout</title><h1>Checkout simulated locally</h1><p>No payment was made.</p>", { headers: { "content-type": "text/html; charset=utf-8" } });
747
+ }
748
+ if (req.method === "POST" && (path === "/_sandbox/create" || path === "/_sandbox/fork")) {
749
+ return this.sandbox(req, path.endsWith("/fork"));
750
+ }
751
+ if (req.method === "POST" && path === "/media/submit")
752
+ return this.submitMedia(req);
753
+ if (req.method === "GET" && path.startsWith("/media/jobs/")) {
754
+ return this.getMediaJob(decodeURIComponent(path.slice("/media/jobs/".length)), origin);
755
+ }
756
+ if (req.method === "GET" && path.startsWith("/media/artifacts/")) {
757
+ return this.getMediaArtifact(path.slice("/media/artifacts/".length));
758
+ }
759
+ if (path === "/auth/token" && req.method === "POST") {
760
+ return json({ error: "No local omg session. Authenticated cloud bridging is not available in the local backend." }, 401, { "X-Vibes-Auth-Proxy": "1" });
761
+ }
762
+ if (path === "/auth/get-session" && req.method === "GET") {
763
+ return json(null, 200, { "X-Vibes-Auth-Proxy": "1" });
764
+ }
765
+ return;
766
+ }
767
+ subscriberCount(topic) {
768
+ try {
769
+ const raw = JSON.parse(readFileSync3(join3(this.projectRoot, ".vibes", "triggers.json"), "utf8"));
770
+ const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
771
+ return triggers.filter((trigger) => trigger.kind === "event" && trigger.key === topic).length;
772
+ } catch {
773
+ return 0;
774
+ }
775
+ }
776
+ async emit(req) {
777
+ try {
778
+ const input = await body(req);
779
+ const topic = String(input.topic ?? "");
780
+ if (!topic || topic.length > 200)
781
+ return error("topic required (1-200 chars)");
782
+ const eventId = `evt_local_${randomUUID2()}`;
783
+ const events = this.cache.read("events.json", []);
784
+ events.push({ eventId, topic, payload: input.payload ?? null, status: "emitted" });
785
+ this.cache.write("events.json", events);
786
+ return json({ eventId, subscriberCount: this.subscriberCount(topic) });
787
+ } catch (err) {
788
+ return error(err instanceof Error ? err.message : String(err));
789
+ }
790
+ }
791
+ async schedule(req) {
792
+ try {
793
+ const input = await body(req);
794
+ const topic = String(input.topic ?? "");
795
+ const runAt = Number(input.runAt);
796
+ if (!topic || topic.length > 200)
797
+ return error("topic required (1-200 chars)");
798
+ if (!Number.isFinite(runAt) || runAt <= 0)
799
+ return error("runAt required (unix ms)");
800
+ const eventId = `evt_local_${randomUUID2()}`;
801
+ const events = this.cache.read("events.json", []);
802
+ events.push({ eventId, topic, payload: input.payload ?? null, runAt, status: "scheduled" });
803
+ this.cache.write("events.json", events);
804
+ return json({ eventId, subscriberCount: this.subscriberCount(topic), runAt });
805
+ } catch (err) {
806
+ return error(err instanceof Error ? err.message : String(err));
807
+ }
808
+ }
809
+ cancelSchedule(eventId) {
810
+ const events = this.cache.read("events.json", []);
811
+ const event = events.find((item) => item.eventId === eventId && item.status === "scheduled");
812
+ if (!event)
813
+ return json({ cancelled: 0 });
814
+ event.status = "cancelled";
815
+ this.cache.write("events.json", events);
816
+ return json({ cancelled: 1 });
817
+ }
818
+ async startWorkflow(req) {
819
+ try {
820
+ const input = await body(req);
821
+ const workflow = String(input.workflow ?? "");
822
+ if (!workflow || workflow.length > 200)
823
+ return error("workflow required (1-200 chars)");
824
+ const key = input.idempotencyKey ? String(input.idempotencyKey) : randomUUID2();
825
+ const runId = stableId("wfr_local", { workflow, key });
826
+ const workflows = this.cache.read("workflows.json", {});
827
+ workflows[runId] = {
828
+ runId,
829
+ workflow,
830
+ payload: input.payload ?? null,
831
+ idempotencyKey: input.idempotencyKey,
832
+ status: "accepted"
833
+ };
834
+ this.cache.write("workflows.json", workflows);
835
+ return json({ runId });
836
+ } catch (err) {
837
+ return error(err instanceof Error ? err.message : String(err));
838
+ }
839
+ }
840
+ storageParts(input) {
841
+ const scope = input.scope === "app" ? "app" : "user";
842
+ const userId = scope === "user" ? safeSegment(input.userId, "userId") : "shared";
843
+ const key = safeKey(input.key);
844
+ return { scope, userId, key };
845
+ }
846
+ storagePath(scope, userId, key) {
847
+ const base = resolve(this.cache.root, "storage", scope, userId);
848
+ const target = resolve(base, key);
849
+ if (target !== base && !target.startsWith(base + sep2))
850
+ throw new Error("invalid key");
851
+ return target;
852
+ }
853
+ async presignStorage(req, origin) {
854
+ try {
855
+ const input = await body(req);
856
+ const action = String(input.action ?? "");
857
+ if (action !== "put" && action !== "get")
858
+ return error('action must be "put" or "get"');
859
+ const { scope, userId, key } = this.storageParts(input);
860
+ const params = new URLSearchParams({ scope, userId, key });
861
+ if (input.contentType)
862
+ params.set("contentType", String(input.contentType));
863
+ return json({
864
+ url: `${origin}/_storage/object?${params}`,
865
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
866
+ key: `local/${scope}/${userId}/${key}`,
867
+ method: action === "put" ? "PUT" : "GET",
868
+ maxBytes: 25 * 1024 * 1024
869
+ });
870
+ } catch (err) {
871
+ return error(err instanceof Error ? err.message : String(err));
872
+ }
873
+ }
874
+ async storageObject(req, url) {
875
+ try {
876
+ const scope = safeSegment(url.searchParams.get("scope"), "scope");
877
+ const userId = safeSegment(url.searchParams.get("userId"), "userId");
878
+ const key = safeKey(url.searchParams.get("key"));
879
+ const path = this.storagePath(scope, userId, key);
880
+ if (req.method === "PUT") {
881
+ const bytes = Buffer.from(await req.arrayBuffer());
882
+ if (bytes.byteLength > 25 * 1024 * 1024)
883
+ return error("object exceeds 25 MB", 413);
884
+ mkdirSync2(dirname2(path), { recursive: true });
885
+ writeFileSync2(path, bytes);
886
+ const metadata2 = this.cache.read("storage-content-types.json", {});
887
+ metadata2[`${scope}/${userId}/${key}`] = url.searchParams.get("contentType") || req.headers.get("content-type") || "application/octet-stream";
888
+ this.cache.write("storage-content-types.json", metadata2);
889
+ return new Response(null, { status: 204 });
890
+ }
891
+ if (!existsSync(path))
892
+ return error("object not found", 404);
893
+ const metadata = this.cache.read("storage-content-types.json", {});
894
+ return new Response(readFileSync3(path), {
895
+ headers: {
896
+ "content-type": metadata[`${scope}/${userId}/${key}`] || "application/octet-stream"
897
+ }
898
+ });
899
+ } catch (err) {
900
+ return error(err instanceof Error ? err.message : String(err));
901
+ }
902
+ }
903
+ async listStorage(req) {
904
+ try {
905
+ const input = await body(req);
906
+ const scope = input.scope === "app" ? "app" : "user";
907
+ const userId = scope === "user" ? safeSegment(input.userId, "userId") : "shared";
908
+ const prefix = input.prefix ? safeKey(input.prefix) : "";
909
+ const limit = Math.min(1000, Math.max(1, Number(input.limit) || 200));
910
+ const root = resolve(this.cache.root, "storage", scope, userId);
911
+ if (!existsSync(root))
912
+ return json([]);
913
+ const metadata = this.cache.read("storage-content-types.json", {});
914
+ const files = [];
915
+ const walk = (dir) => {
916
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
917
+ const path = join3(dir, entry.name);
918
+ if (entry.isDirectory())
919
+ walk(path);
920
+ else if (entry.isFile()) {
921
+ const relPath = relative2(root, path).split(sep2).join("/");
922
+ if (prefix && !relPath.startsWith(prefix))
923
+ continue;
924
+ const stat = statSync(path);
925
+ files.push({
926
+ key: `local/${scope}/${userId}/${relPath}`,
927
+ relPath,
928
+ size: stat.size,
929
+ lastModified: stat.mtime.toISOString(),
930
+ contentType: metadata[`${scope}/${userId}/${relPath}`]
931
+ });
932
+ }
933
+ if (files.length >= limit)
934
+ return;
935
+ }
936
+ };
937
+ walk(root);
938
+ return json(files.slice(0, limit));
939
+ } catch (err) {
940
+ return error(err instanceof Error ? err.message : String(err));
941
+ }
942
+ }
943
+ async deleteStorage(req) {
944
+ try {
945
+ const input = await body(req);
946
+ const { scope, userId, key } = this.storageParts(input);
947
+ rmSync(this.storagePath(scope, userId, key), { force: true });
948
+ return new Response(null, { status: 204 });
949
+ } catch (err) {
950
+ return error(err instanceof Error ? err.message : String(err));
951
+ }
952
+ }
953
+ billingState() {
954
+ return this.cache.read("billing.json", {
955
+ customers: {},
956
+ idempotency: {},
957
+ grants: {}
958
+ });
959
+ }
960
+ ensureCustomer(state, externalRef, plan) {
961
+ const existing = state.customers[externalRef];
962
+ if (existing)
963
+ return { customer: existing, created: false };
964
+ const customer = {
965
+ customerId: stableId("cus_local", externalRef),
966
+ plan: plan || "free",
967
+ balances: {}
968
+ };
969
+ state.customers[externalRef] = customer;
970
+ return { customer, created: true };
971
+ }
972
+ async billing(req, url) {
973
+ const route = url.pathname.slice("/_billing/".length);
974
+ const state = this.billingState();
975
+ try {
976
+ if (req.method === "POST" && route === "customers") {
977
+ const input = await body(req);
978
+ const externalRef = String(input.externalRef ?? "");
979
+ if (!externalRef)
980
+ return error("externalRef required");
981
+ const { customer, created } = this.ensureCustomer(state, externalRef, input.plan ? String(input.plan) : undefined);
982
+ this.cache.write("billing.json", state);
983
+ return json({ customerId: customer.customerId, plan: customer.plan, created });
984
+ }
985
+ if (req.method === "POST" && route === "check") {
986
+ const input = await body(req);
987
+ const externalRef = String(input.externalRef ?? "");
988
+ const feature = String(input.feature ?? "");
989
+ if (!externalRef || !feature)
990
+ return error("feature and externalRef required");
991
+ const { customer } = this.ensureCustomer(state, externalRef);
992
+ this.cache.write("billing.json", state);
993
+ const balanceMicros = customer.balances[feature] ?? 0;
994
+ return json({
995
+ allow: balanceMicros > 0,
996
+ balanceMicros,
997
+ plan: customer.plan,
998
+ reason: balanceMicros > 0 ? "" : "insufficient_credit"
999
+ });
1000
+ }
1001
+ if (req.method === "POST" && (route === "track" || route === "grant")) {
1002
+ const input = await body(req);
1003
+ const externalRef = String(input.externalRef ?? "");
1004
+ const feature = String(input.feature ?? "");
1005
+ const idempotencyKey = String(input.idempotencyKey ?? "");
1006
+ const amountMicros = Number(input.amountMicros);
1007
+ if (!externalRef || !feature)
1008
+ return error("externalRef and feature required");
1009
+ if (!idempotencyKey)
1010
+ return error("idempotencyKey required");
1011
+ if (!Number.isSafeInteger(amountMicros) || (route === "grant" ? amountMicros <= 0 : amountMicros < 0)) {
1012
+ return error(route === "grant" ? "amountMicros must be > 0" : "amountMicros must be >= 0");
1013
+ }
1014
+ const { customer } = this.ensureCustomer(state, externalRef);
1015
+ const opKey = `${route}:${idempotencyKey}`;
1016
+ const grantKey = input.grantKey ? `${externalRef}:${String(input.grantKey)}` : "";
1017
+ const alreadyApplied = state.idempotency[opKey] || grantKey && state.grants[grantKey];
1018
+ if (!alreadyApplied) {
1019
+ const current = customer.balances[feature] ?? 0;
1020
+ customer.balances[feature] = route === "grant" ? current + amountMicros : current - amountMicros;
1021
+ state.idempotency[opKey] = true;
1022
+ if (grantKey)
1023
+ state.grants[grantKey] = true;
1024
+ this.cache.write("billing.json", state);
1025
+ }
1026
+ return json({ balanceMicros: customer.balances[feature] ?? 0 });
1027
+ }
1028
+ if (req.method === "GET" && route === "balance") {
1029
+ const externalRef = url.searchParams.get("externalRef") ?? "";
1030
+ const feature = url.searchParams.get("feature") ?? "";
1031
+ if (!externalRef || !feature)
1032
+ return error("externalRef and feature query params required");
1033
+ const customer = state.customers[externalRef];
1034
+ return json({
1035
+ exists: Boolean(customer),
1036
+ plan: customer?.plan ?? "free",
1037
+ balanceMicros: customer?.balances[feature] ?? 0
1038
+ });
1039
+ }
1040
+ if (req.method === "GET" && route === "subscription") {
1041
+ const externalRef = url.searchParams.get("externalRef") ?? "";
1042
+ if (!externalRef)
1043
+ return error("externalRef query param required");
1044
+ return json({ plan: state.customers[externalRef]?.plan ?? "free", subscription: null });
1045
+ }
1046
+ if (req.method === "POST" && route === "checkout") {
1047
+ const input = await body(req);
1048
+ const externalRef = String(input.externalRef ?? "");
1049
+ const plan = String(input.plan ?? "");
1050
+ if (!externalRef || !plan)
1051
+ return error("plan and externalRef required");
1052
+ const { customer } = this.ensureCustomer(state, externalRef, plan);
1053
+ customer.plan = plan;
1054
+ this.cache.write("billing.json", state);
1055
+ const checkoutId = stableId("checkout_local", { externalRef, plan });
1056
+ return json({ checkoutId, checkoutUrl: `${url.origin}/_checkout/${checkoutId}` });
1057
+ }
1058
+ return error(`unknown billing route: ${req.method} ${url.pathname}`, 404);
1059
+ } catch (err) {
1060
+ return error(err instanceof Error ? err.message : String(err));
1061
+ }
1062
+ }
1063
+ async sandbox(req, fork) {
1064
+ try {
1065
+ const input = await body(req);
1066
+ if (fork && !input.snapshotId)
1067
+ return error("snapshotId required");
1068
+ const id = `sbx_local_${randomUUID2()}`;
1069
+ const sandboxes = this.cache.read("sandboxes.json", {});
1070
+ const result = {
1071
+ id,
1072
+ status: "running",
1073
+ local: true,
1074
+ ports: input.ports ?? [],
1075
+ ...fork ? { snapshotId: input.snapshotId } : {}
1076
+ };
1077
+ sandboxes[id] = result;
1078
+ this.cache.write("sandboxes.json", sandboxes);
1079
+ return json(result);
1080
+ } catch (err) {
1081
+ return error(err instanceof Error ? err.message : String(err));
1082
+ }
1083
+ }
1084
+ async submitMedia(req) {
1085
+ try {
1086
+ const input = await body(req);
1087
+ const model = String(input.model ?? "");
1088
+ if (!model)
1089
+ return error("model required");
1090
+ const jobId = stableId("media_local", { model, input: input.input ?? {} });
1091
+ const jobs = this.cache.read("media-jobs.json", {});
1092
+ if (!jobs[jobId]) {
1093
+ if (model.includes("scribe")) {
1094
+ const source = String(input.input?.audio_url ?? "local audio");
1095
+ jobs[jobId] = {
1096
+ jobId,
1097
+ status: "succeeded",
1098
+ model,
1099
+ results: [{ text: `[omg dev transcript placeholder for ${source}]` }]
1100
+ };
1101
+ } else {
1102
+ const artifact = `${jobId}.svg`;
1103
+ const prompt = String(input.input?.prompt ?? model);
1104
+ const svg = this.placeholderSvg(model, prompt);
1105
+ const artifactDir = join3(this.cache.root, "media");
1106
+ mkdirSync2(artifactDir, { recursive: true });
1107
+ writeFileSync2(join3(artifactDir, artifact), svg);
1108
+ jobs[jobId] = {
1109
+ jobId,
1110
+ status: "succeeded",
1111
+ model,
1112
+ results: [{
1113
+ artifact,
1114
+ contentType: "image/svg+xml"
1115
+ }]
1116
+ };
1117
+ }
1118
+ this.cache.write("media-jobs.json", jobs);
1119
+ }
1120
+ return json({ jobId, status: "queued" }, 202);
1121
+ } catch (err) {
1122
+ return error(err instanceof Error ? err.message : String(err));
1123
+ }
1124
+ }
1125
+ getMediaJob(jobId, origin) {
1126
+ const job = this.cache.read("media-jobs.json", {})[jobId];
1127
+ if (!job)
1128
+ return error("job not found", 404);
1129
+ return json({
1130
+ ...job,
1131
+ results: job.results.map((result) => ({
1132
+ ...result.artifact ? { url: `${origin}/media/artifacts/${result.artifact}` } : {},
1133
+ ...result.contentType ? { contentType: result.contentType } : {},
1134
+ ...result.text !== undefined ? { text: result.text } : {}
1135
+ }))
1136
+ });
1137
+ }
1138
+ getMediaArtifact(name) {
1139
+ const safeName = safeSegment(name, "artifact");
1140
+ const path = join3(this.cache.root, "media", safeName);
1141
+ if (!existsSync(path))
1142
+ return error("artifact not found", 404);
1143
+ return new Response(readFileSync3(path), { headers: { "content-type": "image/svg+xml" } });
1144
+ }
1145
+ placeholderSvg(model, prompt) {
1146
+ const escape = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1147
+ const shortPrompt = prompt.replace(/\s+/g, " ").slice(0, 72);
1148
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
1149
+ <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#635bff"/><stop offset="1" stop-color="#12b981"/></linearGradient></defs>
1150
+ <rect width="1024" height="1024" fill="url(#g)"/><circle cx="512" cy="420" r="190" fill="none" stroke="white" stroke-width="28" opacity=".9"/>
1151
+ <text x="512" y="430" fill="white" font-family="system-ui,sans-serif" font-size="72" font-weight="700" text-anchor="middle">omg dev</text>
1152
+ <text x="512" y="690" fill="white" font-family="system-ui,sans-serif" font-size="30" text-anchor="middle">${escape(model.slice(0, 54))}</text>
1153
+ <text x="512" y="750" fill="white" font-family="system-ui,sans-serif" font-size="25" text-anchor="middle">${escape(shortPrompt)}</text>
1154
+ </svg>`;
1155
+ }
1156
+ }
1157
+
1158
+ class CloudDevBackend {
1159
+ name = "cloud";
1160
+ async handle(_req, url) {
1161
+ return error(`Cloud proxy is not implemented for ${url.pathname}. The public omg service routes require a service token; use the default local backend until the control-plane broker lands.`, 501);
1162
+ }
1163
+ }
1164
+
1165
+ // src/dev.ts
1166
+ function startDevEmulator(options) {
1167
+ const root = resolve2(options.root);
1168
+ const mode = options.mode ?? "local";
1169
+ const backend = mode === "cloud" ? new CloudDevBackend : new LocalDevBackend(root);
1170
+ let origin = "";
1171
+ const server = Bun.serve({
1172
+ hostname: "127.0.0.1",
1173
+ port: options.port ?? 0,
1174
+ async fetch(req) {
1175
+ if (req.method === "OPTIONS") {
1176
+ return new Response(null, {
1177
+ status: 204,
1178
+ headers: {
1179
+ "access-control-allow-origin": "*",
1180
+ "access-control-allow-methods": "GET,POST,PUT,DELETE,OPTIONS",
1181
+ "access-control-allow-headers": "*"
1182
+ }
1183
+ });
1184
+ }
1185
+ const response = await backend.handle(req, new URL(req.url), origin);
1186
+ const result = response ?? new Response(JSON.stringify({ error: `Not found: ${req.method} ${new URL(req.url).pathname}` }), { status: 404, headers: { "content-type": "application/json" } });
1187
+ result.headers.set("access-control-allow-origin", "*");
1188
+ return result;
1189
+ }
1190
+ });
1191
+ const port = server.port;
1192
+ if (port === undefined) {
1193
+ server.stop(true);
1194
+ throw new Error("omg dev emulator did not bind a TCP port");
1195
+ }
1196
+ origin = `http://127.0.0.1:${port}`;
1197
+ return {
1198
+ backend: mode,
1199
+ port,
1200
+ url: origin,
1201
+ env: {
1202
+ VIBES_MODE: "dev",
1203
+ OMG_AGENT_URL: origin,
1204
+ VIBES_AGENT_URL: `${origin}/_sandbox`,
1205
+ VIBES_SANDBOX_ROUTER_URL: `${origin}/_sandbox`,
1206
+ OMG_AI_URL: origin,
1207
+ OMG_MEDIA_URL: `${origin}/media`
1208
+ },
1209
+ stop() {
1210
+ server.stop(true);
1211
+ }
1212
+ };
1213
+ }
1214
+ async function runDev(options) {
1215
+ const root = resolve2(options.root);
1216
+ const emulator = startDevEmulator({ root, mode: options.mode, port: options.port });
1217
+ const log = options.onLog ?? console.log;
1218
+ log(`omg dev \xB7 ${emulator.backend} backend`);
1219
+ log(`agent ${emulator.url}`);
1220
+ log(`cache ${root}/.omg/cache`);
1221
+ if (emulator.backend === "cloud") {
1222
+ log("cloud routes are intentionally disabled: the required control-plane service-token broker has not landed");
1223
+ }
1224
+ log("");
1225
+ log("starting `bun run dev`");
1226
+ const child = Bun.spawn(["bun", "run", "dev"], {
1227
+ cwd: root,
1228
+ env: { ...process.env, ...emulator.env },
1229
+ stdin: "inherit",
1230
+ stdout: "inherit",
1231
+ stderr: "inherit"
1232
+ });
1233
+ let stopping = false;
1234
+ const stop = () => {
1235
+ if (stopping)
1236
+ return;
1237
+ stopping = true;
1238
+ child.kill();
1239
+ emulator.stop();
1240
+ };
1241
+ process.once("SIGINT", stop);
1242
+ process.once("SIGTERM", stop);
1243
+ try {
1244
+ return await child.exited;
1245
+ } finally {
1246
+ process.off("SIGINT", stop);
1247
+ process.off("SIGTERM", stop);
1248
+ stop();
1249
+ }
1250
+ }
1251
+
1252
+ // src/index.ts
1253
+ var argv = process.argv.slice(2);
1254
+ var cmd = argv[0] ?? "help";
1255
+ function flag(name) {
1256
+ const i = argv.indexOf(`--${name}`);
1257
+ return i >= 0 ? argv[i + 1] : undefined;
1258
+ }
1259
+ var has = (name) => argv.includes(`--${name}`);
1260
+ var out = (msg = "") => process.stdout.write(msg + `
1261
+ `);
1262
+ var step = (msg) => out(` ${msg}`);
1263
+ function readLink(root) {
1264
+ try {
1265
+ return JSON.parse(readFileSync4(join4(root, LINK_FILE), "utf8"));
1266
+ } catch {
1267
+ return null;
1268
+ }
1269
+ }
1270
+ function writeLink(root, link) {
1271
+ const path = join4(root, LINK_FILE);
1272
+ mkdirSync3(dirname3(path), { recursive: true });
1273
+ writeFileSync3(path, JSON.stringify(link, null, 2) + `
1274
+ `);
1275
+ }
1276
+ var HELP = `omg \u2014 deploy a local project to omg.dev
1277
+
1278
+ omg create <name> [--no-install]
1279
+ omg deploy [--name <name>] [--dir <path>] [--no-wait]
1280
+ omg status [--dir <path>]
1281
+ omg link <slug> [--dir <path>]
1282
+ omg login [--token <omg_sk_...>]
1283
+ omg logout
1284
+ omg whoami
1285
+ omg apps
1286
+ omg dev [--dir <path>] [--agent-port <port>] [--cloud]
1287
+
1288
+ Credentials: OMG_API_KEY, or ~/.omg/credentials.json via \`omg login\`.
1289
+ `;
1290
+ async function main() {
1291
+ const root = resolve3(flag("dir") ?? process.cwd());
1292
+ switch (cmd) {
1293
+ case "create": {
1294
+ const createArgs = argv.slice(1);
1295
+ if (!createArgs.some((arg) => !arg.startsWith("-"))) {
1296
+ out("Usage: omg create <name> [--no-install]");
1297
+ return 1;
1298
+ }
1299
+ return runCreate(createArgs);
1300
+ }
1301
+ case "login": {
1302
+ const token = flag("token");
1303
+ if (!token) {
1304
+ await loginWithBrowser({ output: out });
1305
+ out("Signed in. Credentials saved to ~/.omg/credentials.json");
1306
+ return 0;
1307
+ }
1308
+ if (!token.startsWith("omg_sk_")) {
1309
+ out("That does not look like an omg key (expected omg_sk_...).");
1310
+ return 1;
1311
+ }
1312
+ saveCredentials({ token, kind: "api-key" });
1313
+ out("Saved to ~/.omg/credentials.json");
1314
+ return 0;
1315
+ }
1316
+ case "logout": {
1317
+ clearCredentials();
1318
+ out(process.env.OMG_API_KEY?.trim() ? "Cleared saved credentials. OMG_API_KEY is still set." : "Signed out.");
1319
+ return 0;
1320
+ }
1321
+ case "whoami": {
1322
+ const token = await requireToken();
1323
+ const identity = await whoAmI(token);
1324
+ out(identity.name ? `${identity.name} <${identity.email}>` : identity.email);
1325
+ out(`User ID: ${identity.userId}`);
1326
+ return 0;
1327
+ }
1328
+ case "apps": {
1329
+ const token = await requireToken();
1330
+ const { apps } = await listApps(token);
1331
+ if (!apps?.length) {
1332
+ out("No apps yet.");
1333
+ return 0;
1334
+ }
1335
+ for (const a of apps)
1336
+ out(` ${a.slug.padEnd(32)} ${a.name ?? ""}`);
1337
+ return 0;
1338
+ }
1339
+ case "link": {
1340
+ const slug = argv[1];
1341
+ if (!slug) {
1342
+ out("Usage: omg link <slug>");
1343
+ return 1;
1344
+ }
1345
+ const token = await requireToken();
1346
+ const { apps } = await listApps(token);
1347
+ const app = apps?.find((a) => a.slug === slug);
1348
+ if (!app) {
1349
+ out(`No app "${slug}" on your account. Run \`omg apps\` to see them.`);
1350
+ return 1;
1351
+ }
1352
+ const existing = readLink(root);
1353
+ writeLink(root, { slug, projectId: existing?.projectId ?? "", name: app.name });
1354
+ out(`Linked ${root} \u2192 ${slug}`);
1355
+ if (!existing?.projectId) {
1356
+ out("Run `omg deploy` to complete the binding.");
1357
+ }
1358
+ return 0;
1359
+ }
1360
+ case "status": {
1361
+ const token = await requireToken();
1362
+ const link = readLink(root);
1363
+ if (!link?.slug) {
1364
+ out("Not linked. Run `omg deploy` or `omg link <slug>`.");
1365
+ return 1;
1366
+ }
1367
+ const status = await waitForDeploy(token, link.slug, (s) => step(s), 1).catch((e) => e.message);
1368
+ out(`${link.slug}: ${status}`);
1369
+ return 0;
1370
+ }
1371
+ case "deploy": {
1372
+ const token = await requireToken();
1373
+ const link = readLink(root);
1374
+ const name = flag("name") ?? link?.name ?? basename(root);
1375
+ out(`Deploying ${root}`);
1376
+ if (link?.projectId)
1377
+ step(`linked \u2192 ${link.slug}`);
1378
+ const result = await deploy({
1379
+ root,
1380
+ token,
1381
+ name,
1382
+ projectId: link?.projectId,
1383
+ onProgress: step
1384
+ });
1385
+ writeLink(root, { slug: result.slug, projectId: result.projectId, name });
1386
+ step(`\u2192 ${result.slug} (${result.status})`);
1387
+ if (has("no-wait")) {
1388
+ out("");
1389
+ out(`Building. Check with \`omg status\`.`);
1390
+ out(` https://${result.slug}.omgs.app`);
1391
+ return 0;
1392
+ }
1393
+ await waitForDeploy(token, result.slug, step);
1394
+ out("");
1395
+ out(` https://${result.slug}.omgs.app`);
1396
+ return 0;
1397
+ }
1398
+ case "dev": {
1399
+ const portFlag = flag("agent-port");
1400
+ const port = portFlag === undefined ? 0 : Number(portFlag);
1401
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
1402
+ out("--agent-port must be an integer from 0 to 65535");
1403
+ return 1;
1404
+ }
1405
+ return runDev({
1406
+ root,
1407
+ mode: has("cloud") ? "cloud" : "local",
1408
+ port,
1409
+ onLog: out
1410
+ });
1411
+ }
1412
+ default:
1413
+ out(HELP);
1414
+ return cmd === "help" || has("help") ? 0 : 1;
1415
+ }
1416
+ }
1417
+ main().then((code) => process.exit(code)).catch((err) => {
1418
+ const msg = err instanceof Error ? err.message : String(err);
1419
+ process.stderr.write(`
1420
+ error: ${msg}
1421
+ `);
1422
+ if (err instanceof AuthError) {
1423
+ process.stderr.write("Run `omg login` to sign in again.\n");
1424
+ }
1425
+ process.exit(1);
1426
+ });