@omg-dev/vite-plugin 0.4.24

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/dist/index.mjs ADDED
@@ -0,0 +1,1087 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { schemaToCollections, schemaToDrizzle, schemaToZod } from "@omg-dev/schema";
4
+ import { scanTriggers } from "@omg-dev/server/trigger-scan";
5
+ //#region src/auth-bridge.ts
6
+ const AUTH_RELAY_SKIP_HEADERS = new Set([
7
+ "set-cookie",
8
+ "content-encoding",
9
+ "content-length",
10
+ "transfer-encoding",
11
+ "connection",
12
+ "keep-alive"
13
+ ]);
14
+ /** Relay an upstream auth response with sane framing for the buffered body. */
15
+ async function relayAuthUpstream(res, upstream) {
16
+ const body = Buffer.from(await upstream.arrayBuffer());
17
+ res.statusCode = upstream.status;
18
+ upstream.headers.forEach((value, key) => {
19
+ if (AUTH_RELAY_SKIP_HEADERS.has(key.toLowerCase())) return;
20
+ res.setHeader(key, value);
21
+ });
22
+ res.setHeader("Content-Length", String(body.byteLength));
23
+ res.end(body);
24
+ }
25
+ /**
26
+ * Fetch the auth upstream for the preview bridges.
27
+ *
28
+ * Prefers the host-side per-sandbox proxy (OMG_AI_URL, the same listener the
29
+ * LLM proxy uses at 169.254.0.1:9090) when it advertises the auth route via
30
+ * the `x-vibes-auth-proxy` marker: in-VM Bun fetch to the Cloudflare-proxied
31
+ * auth host is the AAAA-first/no-IPv6-route hang class, while the host-side
32
+ * Go client dials dual-stack. Falls back to a direct fetch when the proxy is
33
+ * absent (local dev outside a sandbox) or predates the auth route.
34
+ */
35
+ async function fetchAuthUpstream(kind, init, qs = "") {
36
+ const proxyBase = (process.env.OMG_AI_URL ?? "").trim().replace(/\/+$/, "");
37
+ if (proxyBase) {
38
+ const proxyUrl = kind === "token" ? `${proxyBase}/auth/token` : `${proxyBase}/auth/get-session${qs}`;
39
+ try {
40
+ const viaProxy = await fetch(proxyUrl, init);
41
+ if (viaProxy.headers.get("x-vibes-auth-proxy")) return viaProxy;
42
+ await viaProxy.arrayBuffer().catch(() => {});
43
+ } catch {}
44
+ }
45
+ const authUrl = (process.env.VIBES_AUTH_URL || process.env.VITE_AUTH_URL || "https://auth.omg.dev").replace(/\/+$/, "");
46
+ const directUrl = kind === "token" ? `${authUrl}/token` : `${authUrl}/api/auth/get-session${qs}`;
47
+ return fetch(directUrl, init);
48
+ }
49
+ //#endregion
50
+ //#region src/scanner.ts
51
+ const HTTP_METHODS = new Set([
52
+ "GET",
53
+ "POST",
54
+ "PUT",
55
+ "PATCH",
56
+ "DELETE"
57
+ ]);
58
+ function handlerToRoute(routePath, handlerName, modulePath) {
59
+ if (HTTP_METHODS.has(handlerName)) return {
60
+ method: handlerName,
61
+ path: routePath,
62
+ module: modulePath,
63
+ handler: handlerName,
64
+ style: "method"
65
+ };
66
+ switch (handlerName) {
67
+ case "list": return {
68
+ method: "GET",
69
+ path: routePath,
70
+ module: modulePath,
71
+ handler: handlerName,
72
+ style: "crud"
73
+ };
74
+ case "get": return {
75
+ method: "GET",
76
+ path: `${routePath}/:id`,
77
+ module: modulePath,
78
+ handler: handlerName,
79
+ style: "crud"
80
+ };
81
+ case "create": return {
82
+ method: "POST",
83
+ path: routePath,
84
+ module: modulePath,
85
+ handler: handlerName,
86
+ style: "crud"
87
+ };
88
+ case "update": return {
89
+ method: "PATCH",
90
+ path: `${routePath}/:id`,
91
+ module: modulePath,
92
+ handler: handlerName,
93
+ style: "crud"
94
+ };
95
+ case "remove": return {
96
+ method: "DELETE",
97
+ path: `${routePath}/:id`,
98
+ module: modulePath,
99
+ handler: handlerName,
100
+ style: "crud"
101
+ };
102
+ default: return {
103
+ method: "POST",
104
+ path: `${routePath}/${handlerName}`,
105
+ module: modulePath,
106
+ handler: handlerName,
107
+ style: "crud"
108
+ };
109
+ }
110
+ }
111
+ /**
112
+ * Scans the `functions/` directory in `dir` and returns a list of routes.
113
+ *
114
+ * Layout:
115
+ * functions/<name>.ts → routes mounted at /api/<name>
116
+ * functions/api/<name>.ts → routes mounted at /api/<name> (the `api/`
117
+ * subdir is treated as a no-op prefix so the
118
+ * path doesn't double up to /api/api/<name>;
119
+ * lets people group "raw HTTP" handlers
120
+ * alongside CRUD without an ugly URL).
121
+ *
122
+ * Uses regex to extract exported function names — no AST parser needed.
123
+ */
124
+ async function scanFunctions(dir) {
125
+ const functionsDir = path.join(dir, "functions");
126
+ if (!fs.existsSync(functionsDir)) return [];
127
+ const routes = [];
128
+ walkDir(functionsDir, "/api", routes, "api");
129
+ const apiDir = path.join(functionsDir, "api");
130
+ if (fs.existsSync(apiDir) && fs.statSync(apiDir).isDirectory()) walkDir(apiDir, "/api", routes, null);
131
+ return routes;
132
+ }
133
+ function walkDir(fullDir, routePrefix, routes, skipDirName) {
134
+ const entries = fs.readdirSync(fullDir, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ if (entry.isDirectory()) continue;
137
+ if (skipDirName && entry.name === skipDirName) continue;
138
+ if (!entry.name.endsWith(".ts") || entry.name.endsWith(".d.ts")) continue;
139
+ const filePath = path.join(fullDir, entry.name);
140
+ const resource = path.basename(entry.name, ".ts");
141
+ const handlerNames = extractExports(fs.readFileSync(filePath, "utf-8"));
142
+ for (const handlerName of handlerNames) routes.push(handlerToRoute(`${routePrefix}/${resource}`, handlerName, filePath));
143
+ }
144
+ }
145
+ /**
146
+ * Extracts exported function/const names from TypeScript source using regex.
147
+ */
148
+ function extractExports(source) {
149
+ const names = [];
150
+ const seen = /* @__PURE__ */ new Set();
151
+ const fnRegex = /^export\s+(?:async\s+)?function\s+(\w+)/gm;
152
+ let match;
153
+ while ((match = fnRegex.exec(source)) !== null) {
154
+ const name = match[1];
155
+ if (!seen.has(name)) {
156
+ seen.add(name);
157
+ names.push(name);
158
+ }
159
+ }
160
+ const constRegex = /^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=/gm;
161
+ while ((match = constRegex.exec(source)) !== null) {
162
+ const name = match[1];
163
+ if (!seen.has(name)) {
164
+ seen.add(name);
165
+ names.push(name);
166
+ }
167
+ }
168
+ const namedExportRegex = /^export\s*\{([^}]+)\}/gm;
169
+ while ((match = namedExportRegex.exec(source)) !== null) {
170
+ const exports = match[1].split(",").map((s) => s.trim().split(/\s+as\s+/).pop().trim());
171
+ for (const name of exports) if (name && !seen.has(name)) {
172
+ seen.add(name);
173
+ names.push(name);
174
+ }
175
+ }
176
+ return names;
177
+ }
178
+ //#endregion
179
+ //#region src/scanner-workflows.ts
180
+ const EXPORT_WORKFLOW_RE = /^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*workflow\s*\(\s*(.+?)\s*,/gm;
181
+ const NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
182
+ var WorkflowScanError = class extends Error {
183
+ constructor(file, detail) {
184
+ super(`[vibes:workflows] ${file}: ${detail}`);
185
+ this.file = file;
186
+ this.detail = detail;
187
+ this.name = "WorkflowScanError";
188
+ }
189
+ };
190
+ /** scanWorkflows walks `<root>/functions/` and returns every declared workflow. */
191
+ async function scanWorkflows(root) {
192
+ const fnDir = path.join(root, "functions");
193
+ if (!fs.existsSync(fnDir)) return [];
194
+ const out = [];
195
+ walk(fnDir, out);
196
+ const apiDir = path.join(fnDir, "api");
197
+ if (fs.existsSync(apiDir) && fs.statSync(apiDir).isDirectory()) walk(apiDir, out);
198
+ const seen = /* @__PURE__ */ new Map();
199
+ for (const w of out) {
200
+ const prev = seen.get(w.name);
201
+ if (prev) throw new WorkflowScanError(w.module, `duplicate workflow name "${w.name}" (also declared by ${prev})`);
202
+ seen.set(w.name, w.handler);
203
+ }
204
+ return out;
205
+ }
206
+ function walk(dir, out) {
207
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
208
+ if (entry.isDirectory()) continue;
209
+ if (!entry.name.endsWith(".ts") || entry.name.endsWith(".d.ts")) continue;
210
+ const filePath = path.join(dir, entry.name);
211
+ const source = fs.readFileSync(filePath, "utf-8");
212
+ const basename = path.basename(entry.name, ".ts");
213
+ out.push(...extractWorkflows(source, filePath, basename));
214
+ }
215
+ }
216
+ function extractWorkflows(source, filePath, basename) {
217
+ const out = [];
218
+ const stripped = source.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
219
+ let m;
220
+ EXPORT_WORKFLOW_RE.lastIndex = 0;
221
+ while ((m = EXPORT_WORKFLOW_RE.exec(stripped)) !== null) {
222
+ const [, exportName, firstArg] = m;
223
+ const lit = parseStringLiteral(firstArg);
224
+ if (lit === null) throw new WorkflowScanError(filePath, `workflow() first arg must be a string literal — got: ${firstArg.slice(0, 60)}`);
225
+ if (!NAME_RE.test(lit)) throw new WorkflowScanError(filePath, `workflow name "${lit}" is invalid — use [A-Za-z][A-Za-z0-9_]* (no dashes; it becomes the engine handler name)`);
226
+ out.push({
227
+ name: lit,
228
+ handler: `${basename}.${exportName}`,
229
+ module: filePath,
230
+ exportName
231
+ });
232
+ }
233
+ return out;
234
+ }
235
+ function parseStringLiteral(raw) {
236
+ const trimmed = raw.trim();
237
+ if (trimmed.length < 2) return null;
238
+ const first = trimmed[0];
239
+ if ((first === "\"" || first === "'") && trimmed.endsWith(first)) {
240
+ const inner = trimmed.slice(1, -1);
241
+ if (inner.includes("${")) return null;
242
+ return inner.replace(/\\(.)/g, "$1");
243
+ }
244
+ return null;
245
+ }
246
+ //#endregion
247
+ //#region src/codegen.ts
248
+ async function generateAll(root) {
249
+ await regenerateSchema(root);
250
+ await regenerateRoutes(root);
251
+ await regenerateTriggers(root);
252
+ await regenerateWorkflows(root);
253
+ }
254
+ /**
255
+ * Reads schema.ts from root, imports it dynamically, and writes:
256
+ * src/db.generated.ts — TypeScript types + Zod schemas + collection configs
257
+ * src/db.drizzle.ts — Drizzle table definitions for server-side helpers
258
+ */
259
+ async function regenerateSchema(root) {
260
+ const schemaPath = path.join(root, "schema.ts");
261
+ if (!fs.existsSync(schemaPath)) {
262
+ console.warn(`[vibes:codegen] No schema.ts found at ${schemaPath}`);
263
+ return;
264
+ }
265
+ let schema;
266
+ try {
267
+ schema = (await import(schemaPath)).default;
268
+ } catch (err) {
269
+ console.error(`[vibes:codegen] Failed to import schema.ts:`, err);
270
+ return;
271
+ }
272
+ const srcDir = path.join(root, "src");
273
+ if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
274
+ const outputPath = path.join(srcDir, "db.generated.ts");
275
+ const lines = [
276
+ "// This file is auto-generated by @omg-dev/vite-plugin. Do not edit manually.",
277
+ "// To regenerate: restart the dev server or run `vp build`.",
278
+ "",
279
+ "// ── Zod schemas & types ─────────────────────────────────────────────────────────",
280
+ "",
281
+ schemaToZod(schema),
282
+ "",
283
+ "// ── Collection configs ─────────────────────────────────────────────────────────",
284
+ "",
285
+ schemaToCollections(schema)
286
+ ];
287
+ fs.writeFileSync(outputPath, lines.join("\n"), "utf-8");
288
+ console.log(`[vibes:codegen] Generated ${outputPath}`);
289
+ const drizzlePath = path.join(srcDir, "db.drizzle.ts");
290
+ const drizzleLines = [
291
+ "// This file is auto-generated by @omg-dev/vite-plugin. Do not edit manually.",
292
+ "// To regenerate: restart the dev server or run `vp build`.",
293
+ "",
294
+ schemaToDrizzle(schema)
295
+ ];
296
+ fs.writeFileSync(drizzlePath, drizzleLines.join("\n"), "utf-8");
297
+ console.log(`[vibes:codegen] Generated ${drizzlePath}`);
298
+ }
299
+ /**
300
+ * Scans functions/ directory and writes .vibes/routes.json
301
+ */
302
+ async function regenerateRoutes(root) {
303
+ const routes = await scanFunctions(root);
304
+ const vibesDir = path.join(root, ".vibes");
305
+ if (!fs.existsSync(vibesDir)) fs.mkdirSync(vibesDir, { recursive: true });
306
+ const routesPath = path.join(vibesDir, "routes.json");
307
+ fs.writeFileSync(routesPath, JSON.stringify(routes, null, 2), "utf-8");
308
+ console.log(`[vibes:codegen] Generated ${routesPath} with ${routes.length} route(s)`);
309
+ }
310
+ /**
311
+ * Scans functions/ for `cron()` and `on()` calls and writes .vibes/triggers.json
312
+ * consumed by @omg-dev/server's scheduler (dev) and by the orchestrator's queue
313
+ * runtime (prod). Throws TriggerScanError on malformed declarations so the
314
+ * user sees the failure at build / HMR time rather than silently losing a
315
+ * trigger.
316
+ */
317
+ async function regenerateTriggers(root) {
318
+ const triggers = await scanTriggers(root);
319
+ const vibesDir = path.join(root, ".vibes");
320
+ if (!fs.existsSync(vibesDir)) fs.mkdirSync(vibesDir, { recursive: true });
321
+ const triggersPath = path.join(vibesDir, "triggers.json");
322
+ fs.writeFileSync(triggersPath, JSON.stringify(triggers, null, 2), "utf-8");
323
+ console.log(`[vibes:codegen] Generated ${triggersPath} with ${triggers.length} trigger(s)`);
324
+ }
325
+ /**
326
+ * Scans functions/ for `workflow()` declarations and writes
327
+ * .vibes/workflows.json — consumed by @omg-dev/server's dev engine and the
328
+ * prod Restate endpoint, and read by the orchestrator at publish to decide
329
+ * whether to register a workflow deployment. Throws WorkflowScanError on
330
+ * malformed declarations (non-literal name, duplicate names).
331
+ */
332
+ async function regenerateWorkflows(root) {
333
+ const workflows = await scanWorkflows(root);
334
+ const vibesDir = path.join(root, ".vibes");
335
+ if (!fs.existsSync(vibesDir)) fs.mkdirSync(vibesDir, { recursive: true });
336
+ const workflowsPath = path.join(vibesDir, "workflows.json");
337
+ fs.writeFileSync(workflowsPath, JSON.stringify(workflows, null, 2), "utf-8");
338
+ if (workflows.length > 0) console.log(`[vibes:codegen] Generated ${workflowsPath} with ${workflows.length} workflow(s)`);
339
+ }
340
+ //#endregion
341
+ //#region src/error-sink.ts
342
+ /**
343
+ * Error sink writer used by the vibes vite plugin in dev mode.
344
+ *
345
+ * Both server (Vite logger) and runtime (in-iframe reporter) errors get
346
+ * appended as JSONL to <root>/.vibes/errors.jsonl. The agent-server
347
+ * inside the sandbox tails this file between turns and splices new
348
+ * entries into pi's user message.
349
+ *
350
+ * Pure helpers — no plugin/Vite types — so they can be unit-tested.
351
+ */
352
+ const MAX_BYTES_BEFORE_ROTATE = 1048576;
353
+ function errorsPathFor(root) {
354
+ return path.join(root, ".vibes", "errors.jsonl");
355
+ }
356
+ /**
357
+ * Append one entry to the sink. The `at` timestamp is stamped here so
358
+ * the file is the authoritative ordering — callers must not pre-stamp.
359
+ *
360
+ * Errors are logged but not rethrown — the dev server must keep running
361
+ * even if the sink is broken. The previous version swallowed silently,
362
+ * which made a failing writer indistinguishable from "no errors yet".
363
+ */
364
+ function appendErrorEntry(errorsPath, entry) {
365
+ try {
366
+ fs.mkdirSync(path.dirname(errorsPath), { recursive: true });
367
+ const line = JSON.stringify({
368
+ at: Date.now(),
369
+ ...entry
370
+ }) + "\n";
371
+ fs.appendFileSync(errorsPath, line);
372
+ if (fs.statSync(errorsPath).size > MAX_BYTES_BEFORE_ROTATE) fs.renameSync(errorsPath, errorsPath + ".1");
373
+ } catch (err) {
374
+ console.error(`[vibes:error-sink] failed to append to ${errorsPath}:`, err instanceof Error ? err.message : err);
375
+ }
376
+ }
377
+ /** Drop ANSI color codes from Vite's rendered logger output. */
378
+ function stripAnsi(s) {
379
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
380
+ }
381
+ //#endregion
382
+ //#region src/pwa.ts
383
+ /** Icon set contract shared with templates/react-ts/public/icons/. */
384
+ const PWA_ICONS = [
385
+ {
386
+ file: "icons/pwa-192x192.png",
387
+ sizes: "192x192",
388
+ purpose: "any"
389
+ },
390
+ {
391
+ file: "icons/pwa-512x512.png",
392
+ sizes: "512x512",
393
+ purpose: "any"
394
+ },
395
+ {
396
+ file: "icons/pwa-512x512-maskable.png",
397
+ sizes: "512x512",
398
+ purpose: "maskable"
399
+ }
400
+ ];
401
+ const APPLE_TOUCH_ICON = "icons/apple-touch-icon.png";
402
+ const MANIFEST_FILE = "manifest.webmanifest";
403
+ function resolvePwaConfig(opts, indexHtml) {
404
+ const title = /<title[^>]*>([^<]*)<\/title>/i.exec(indexHtml)?.[1]?.trim();
405
+ const metaTheme = /<meta[^>]+name=["']theme-color["'][^>]*content=["']([^"']+)["']/i.exec(indexHtml)?.[1] ?? /<meta[^>]+content=["']([^"']+)["'][^>]*name=["']theme-color["']/i.exec(indexHtml)?.[1];
406
+ const name = opts?.name ?? (title || "App");
407
+ const themeColor = opts?.themeColor ?? metaTheme ?? "#ffffff";
408
+ return {
409
+ name,
410
+ shortName: opts?.shortName ?? (name.length > 12 ? name.slice(0, 12).trimEnd() : name),
411
+ themeColor,
412
+ backgroundColor: opts?.backgroundColor ?? themeColor,
413
+ autoPrompt: opts?.autoPrompt !== false
414
+ };
415
+ }
416
+ function buildWebManifest(cfg, availableIconFiles) {
417
+ const available = new Set(availableIconFiles);
418
+ const icons = PWA_ICONS.filter((i) => available.has(i.file)).map((i) => ({
419
+ src: `/${i.file}`,
420
+ sizes: i.sizes,
421
+ type: "image/png",
422
+ ...i.purpose === "maskable" ? { purpose: "maskable" } : {}
423
+ }));
424
+ return JSON.stringify({
425
+ name: cfg.name,
426
+ short_name: cfg.shortName,
427
+ start_url: "/",
428
+ scope: "/",
429
+ display: "standalone",
430
+ background_color: cfg.backgroundColor,
431
+ theme_color: cfg.themeColor,
432
+ icons
433
+ }, null, 2);
434
+ }
435
+ function escapeAttr(value) {
436
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
437
+ }
438
+ /** Inject PWA tags into built index.html. Idempotent: an app that already
439
+ * declares a manifest link (hand-rolled PWA) is left entirely alone. */
440
+ function injectPwaTags(html, cfg, opts) {
441
+ if (/rel=["']manifest["']/i.test(html)) return html;
442
+ const tags = [`<link rel="manifest" href="${opts.manifestHref ?? `/manifest.webmanifest`}">`, `<meta name="mobile-web-app-capable" content="yes">`];
443
+ if (!/name=["']apple-mobile-web-app-capable["']/i.test(html)) tags.push(`<meta name="apple-mobile-web-app-capable" content="yes">`);
444
+ if (!/name=["']apple-mobile-web-app-title["']/i.test(html)) tags.push(`<meta name="apple-mobile-web-app-title" content="${escapeAttr(cfg.shortName)}">`);
445
+ if (opts.hasAppleIcon && !/rel=["']apple-touch-icon["']/i.test(html)) tags.push(`<link rel="apple-touch-icon" href="/${APPLE_TOUCH_ICON}">`);
446
+ if (!/name=["']theme-color["']/i.test(html)) tags.push(`<meta name="theme-color" content="${escapeAttr(cfg.themeColor)}">`);
447
+ const block = `\n ${tags.join("\n ")}`;
448
+ return /<\/head>/i.test(html) ? html.replace(/<\/head>/i, `${block}\n </head>`) : block + html;
449
+ }
450
+ /** True when `id` is the app's client entry (src/main.*) — the injection
451
+ * point for the auto install prompt import. */
452
+ function isAppEntry(id, root) {
453
+ const clean = id.split("?")[0];
454
+ if (!clean.startsWith(root)) return false;
455
+ if (clean.includes("node_modules")) return false;
456
+ return /[\\/]src[\\/]main\.(tsx|ts|jsx|js)$/.test(clean);
457
+ }
458
+ //#endregion
459
+ //#region src/index.ts
460
+ async function pwaDeclaresSubpathExport(resolve, importer, subpath) {
461
+ try {
462
+ const rootEntry = await resolve("@omg-dev/pwa", importer);
463
+ if (!rootEntry?.id) return false;
464
+ let dir = path.dirname(rootEntry.id.split("?")[0]);
465
+ for (let i = 0; i < 12; i++) {
466
+ const pkgPath = path.join(dir, "package.json");
467
+ if (fs.existsSync(pkgPath)) try {
468
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
469
+ if (pkg.name === "@omg-dev/pwa") {
470
+ const exp = pkg.exports;
471
+ return !!exp && typeof exp === "object" && subpath in exp;
472
+ }
473
+ } catch {}
474
+ const parent = path.dirname(dir);
475
+ if (parent === dir) break;
476
+ dir = parent;
477
+ }
478
+ return false;
479
+ } catch {
480
+ return false;
481
+ }
482
+ }
483
+ function vibes(opts = {}) {
484
+ let resolvedRoot;
485
+ let config;
486
+ let serverInstance = null;
487
+ const pwaEnabled = opts.pwa !== false;
488
+ const pwaOpts = typeof opts.pwa === "object" ? opts.pwa : {};
489
+ let pwaAutoInjected = false;
490
+ let remixCtaInjected = false;
491
+ const feedbackEnabled = opts.feedback !== false;
492
+ let feedbackInjected = false;
493
+ const brandBadgeEnabled = opts.brandBadge !== false;
494
+ let brandBadgeInjected = false;
495
+ function existingIconFiles() {
496
+ const publicDir = config?.publicDir;
497
+ if (!publicDir) return [];
498
+ return [...PWA_ICONS.map((i) => i.file), APPLE_TOUCH_ICON].filter((f) => fs.existsSync(path.join(publicDir, f)));
499
+ }
500
+ function userManifestHref() {
501
+ const publicDir = config?.publicDir;
502
+ if (!publicDir) return null;
503
+ if (fs.existsSync(path.join(publicDir, "manifest.webmanifest"))) return `/${MANIFEST_FILE}`;
504
+ if (fs.existsSync(path.join(publicDir, "manifest.json"))) return "/manifest.json";
505
+ return null;
506
+ }
507
+ return {
508
+ name: "vibes",
509
+ enforce: "pre",
510
+ config() {
511
+ return {
512
+ define: { __VIBES_BRAND_FEEDBACK__: JSON.stringify(brandBadgeEnabled && feedbackEnabled) },
513
+ server: { watch: { ignored: ["**/.vibes/**"] } }
514
+ };
515
+ },
516
+ configResolved(resolvedConfig) {
517
+ config = resolvedConfig;
518
+ resolvedRoot = opts.root ?? resolvedConfig.root;
519
+ },
520
+ transformIndexHtml: {
521
+ order: "pre",
522
+ handler(html) {
523
+ if (config?.command === "build") {
524
+ const websiteId = process.env.VIBES_ANALYTICS_WEBSITE_ID;
525
+ if (websiteId) {
526
+ const src = process.env.VIBES_ANALYTICS_SRC || "https://analytics.omg.dev/script.js";
527
+ if (!html.includes(src)) {
528
+ const tag = `<script defer src="${src}" data-website-id="${websiteId}"><\/script>`;
529
+ html = /<\/head>/i.test(html) ? html.replace(/<\/head>/i, `${tag}</head>`) : tag + html;
530
+ }
531
+ }
532
+ if (pwaEnabled) html = injectPwaTags(html, resolvePwaConfig(pwaOpts, html), {
533
+ hasAppleIcon: existingIconFiles().includes(APPLE_TOUCH_ICON),
534
+ manifestHref: userManifestHref() ?? void 0
535
+ });
536
+ return html;
537
+ }
538
+ if (config?.command !== "serve") return html;
539
+ const appId = readPreviewAppId(resolvedRoot);
540
+ const authBootstrap = appId ? `
541
+ <script>
542
+ window.__VIBES_APP_ID = ${JSON.stringify(appId)};
543
+ window.__VIBES_AUTH_TOKEN_URL = "/__vibes/auth/token";
544
+ window.__VIBES_AUTH_SESSION_URL = "/__vibes/auth/session";
545
+ <\/script>` : "";
546
+ const reporter = `
547
+ <script>
548
+ (function(){
549
+ if (window.__vibesReporterInstalled) return;
550
+ window.__vibesReporterInstalled = true;
551
+ function send(payload){
552
+ var msg = Object.assign({ kind: "runtime", at: Date.now() }, payload);
553
+ // POST into the dev server so it lands in .vibes/errors.jsonl. Use
554
+ // keepalive so errors thrown right before navigation still flush.
555
+ // Surface the response status to the parent so a 404/CORS/etc on
556
+ // the sink endpoint is diagnosable without looking at sandbox logs.
557
+ try {
558
+ fetch("/__vibes/runtime-error", {
559
+ method: "POST",
560
+ headers: { "Content-Type": "application/json" },
561
+ body: JSON.stringify(msg),
562
+ keepalive: true,
563
+ }).then(function(r){
564
+ if (!r.ok) {
565
+ try { window.parent && window.parent.postMessage(
566
+ { type: "vibes:reporter-fetch", status: r.status, ok: false }, "*"); } catch (_) {}
567
+ }
568
+ }).catch(function(err){
569
+ try { window.parent && window.parent.postMessage(
570
+ { type: "vibes:reporter-fetch", error: String(err && err.message || err), ok: false }, "*"); } catch (_) {}
571
+ });
572
+ } catch (e) {}
573
+ // Also notify the parent shell for UI surfacing.
574
+ try {
575
+ window.parent && window.parent.postMessage(
576
+ Object.assign({ type: "vibes:runtime-error" }, msg), "*");
577
+ } catch (e) {}
578
+ }
579
+ window.addEventListener("error", function(ev){
580
+ var err = ev.error;
581
+ send({
582
+ message: (err && err.message) || ev.message || "Error",
583
+ stack: (err && err.stack) || null,
584
+ source: ev.filename || null,
585
+ line: ev.lineno || null,
586
+ col: ev.colno || null,
587
+ });
588
+ });
589
+ window.addEventListener("unhandledrejection", function(ev){
590
+ var r = ev.reason;
591
+ send({
592
+ message: (r && (r.message || String(r))) || "Unhandled rejection",
593
+ stack: (r && r.stack) || null,
594
+ rejection: true,
595
+ });
596
+ });
597
+ var origErr = console.error;
598
+ console.error = function(){
599
+ try {
600
+ var args = Array.prototype.slice.call(arguments);
601
+ var msg = args.map(function(a){
602
+ if (a instanceof Error) return a.message;
603
+ if (typeof a === "string") return a;
604
+ try { return JSON.stringify(a); } catch (e) { return String(a); }
605
+ }).join(" ");
606
+ send({ message: msg, viaConsole: true });
607
+ } catch (e) {}
608
+ origErr.apply(console, arguments);
609
+ };
610
+ })();
611
+ <\/script>`;
612
+ if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, function(m) {
613
+ return m + authBootstrap + reporter;
614
+ });
615
+ return authBootstrap + reporter + html;
616
+ }
617
+ },
618
+ async configureServer(server) {
619
+ const errorsPath = errorsPathFor(resolvedRoot);
620
+ const logger = server.config.logger;
621
+ const origErr = logger.error.bind(logger);
622
+ logger.error = (msg, opts) => {
623
+ try {
624
+ const text = typeof msg === "string" ? msg : String(msg);
625
+ if (!text.includes("__vibes-self")) appendErrorEntry(errorsPath, {
626
+ kind: "server",
627
+ source: "vite",
628
+ message: stripAnsi(text).slice(0, 4e3)
629
+ });
630
+ } catch {}
631
+ return origErr(msg, opts);
632
+ };
633
+ server.middlewares.use((req, res, next) => {
634
+ if (req.url !== "/__vibes/runtime-error" || req.method !== "POST") return next();
635
+ const chunks = [];
636
+ req.on("data", (c) => chunks.push(c));
637
+ req.on("end", () => {
638
+ try {
639
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
640
+ appendErrorEntry(errorsPath, {
641
+ kind: "runtime",
642
+ source: "browser",
643
+ message: String(body.message ?? "").slice(0, 4e3),
644
+ stack: body.stack ? String(body.stack).slice(0, 4e3) : void 0,
645
+ file: body.source,
646
+ line: body.line,
647
+ col: body.col,
648
+ viaConsole: body.viaConsole,
649
+ rejection: body.rejection
650
+ });
651
+ } catch {}
652
+ res.statusCode = 204;
653
+ res.end();
654
+ });
655
+ req.on("error", () => {
656
+ res.statusCode = 400;
657
+ res.end();
658
+ });
659
+ });
660
+ server.middlewares.use((req, res, next) => {
661
+ if (req.url !== "/__vibes/auth/token") return next();
662
+ if (req.method === "OPTIONS") {
663
+ res.statusCode = 204;
664
+ res.end();
665
+ return;
666
+ }
667
+ if (req.method !== "POST") {
668
+ res.statusCode = 405;
669
+ res.setHeader("Content-Type", "application/json");
670
+ res.end(JSON.stringify({ error: "method not allowed" }));
671
+ return;
672
+ }
673
+ const appId = readPreviewAppId(resolvedRoot);
674
+ if (!appId) {
675
+ res.statusCode = 404;
676
+ res.setHeader("Content-Type", "application/json");
677
+ res.end(JSON.stringify({ error: "preview app id not configured" }));
678
+ return;
679
+ }
680
+ req.on("data", () => {});
681
+ req.on("end", async () => {
682
+ try {
683
+ await relayAuthUpstream(res, await fetchAuthUpstream("token", {
684
+ method: "POST",
685
+ headers: {
686
+ "Content-Type": "application/json",
687
+ Origin: "https://omg.dev",
688
+ ...req.headers.cookie ? { Cookie: req.headers.cookie } : {}
689
+ },
690
+ body: JSON.stringify({ appId })
691
+ }));
692
+ } catch (err) {
693
+ res.statusCode = 502;
694
+ res.setHeader("Content-Type", "application/json");
695
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : "auth token proxy failed" }));
696
+ }
697
+ });
698
+ req.on("error", () => {
699
+ res.statusCode = 400;
700
+ res.end();
701
+ });
702
+ });
703
+ server.middlewares.use((req, res, next) => {
704
+ if ((req.url ?? "").split("?")[0] !== "/__vibes/auth/session") return next();
705
+ if (req.method === "OPTIONS") {
706
+ res.statusCode = 204;
707
+ res.end();
708
+ return;
709
+ }
710
+ if (req.method !== "GET") {
711
+ res.statusCode = 405;
712
+ res.setHeader("Content-Type", "application/json");
713
+ res.end(JSON.stringify({ error: "method not allowed" }));
714
+ return;
715
+ }
716
+ (async () => {
717
+ try {
718
+ const qs = (req.url ?? "").includes("?") ? (req.url ?? "").slice((req.url ?? "").indexOf("?")) : "";
719
+ await relayAuthUpstream(res, await fetchAuthUpstream("session", {
720
+ method: "GET",
721
+ headers: {
722
+ Origin: "https://omg.dev",
723
+ ...req.headers.cookie ? { Cookie: req.headers.cookie } : {}
724
+ }
725
+ }, qs));
726
+ } catch (err) {
727
+ res.statusCode = 502;
728
+ res.setHeader("Content-Type", "application/json");
729
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : "auth session proxy failed" }));
730
+ }
731
+ })();
732
+ });
733
+ await generateAll(resolvedRoot);
734
+ const schema = await loadSchema(resolvedRoot);
735
+ process.env.VIBES_MODE = "dev";
736
+ const { createVibesServer } = await import("@omg-dev/server");
737
+ const dbPath = opts.db ?? ".vibes/data.db";
738
+ serverInstance = await createVibesServer({
739
+ root: resolvedRoot,
740
+ db: dbPath,
741
+ auth: opts.auth ?? "vibes",
742
+ schema: schema ?? void 0
743
+ });
744
+ const { addClient, removeClient } = await import("@omg-dev/server");
745
+ const { addSubClient, removeSubClient, handleSubMessage } = await import("@omg-dev/server");
746
+ const HEARTBEAT_MS = 25e3;
747
+ server.middlewares.use((req, res, next) => {
748
+ if (req.url !== "/__vibes_events") return next();
749
+ res.writeHead(200, {
750
+ "Content-Type": "text/event-stream",
751
+ "Cache-Control": "no-cache",
752
+ Connection: "keep-alive",
753
+ "Access-Control-Allow-Origin": "*",
754
+ "X-Accel-Buffering": "no"
755
+ });
756
+ res.write(":\n\n");
757
+ const client = {
758
+ readyState: 1,
759
+ send(data) {
760
+ res.write(`data: ${data}\n\n`);
761
+ }
762
+ };
763
+ addClient(client);
764
+ const cleanup = () => {
765
+ if (client.readyState === 3) return;
766
+ client.readyState = 3;
767
+ clearInterval(hb);
768
+ removeClient(client);
769
+ };
770
+ const hb = setInterval(() => {
771
+ try {
772
+ if (!res.write(":hb\n\n")) cleanup();
773
+ } catch {
774
+ cleanup();
775
+ }
776
+ }, HEARTBEAT_MS);
777
+ req.on("close", cleanup);
778
+ });
779
+ const { WebSocketServer } = await import("ws");
780
+ const wss = new WebSocketServer({
781
+ noServer: true,
782
+ handleProtocols: (protocols) => {
783
+ for (const p of protocols) if (p.startsWith("vibes-bearer.")) return p;
784
+ return protocols.values().next().value ?? false;
785
+ }
786
+ });
787
+ const { createAuthMiddleware } = await import("@omg-dev/auth");
788
+ const subAuthMW = createAuthMiddleware(opts.auth ?? "vibes");
789
+ const httpServer = server.httpServer;
790
+ if (httpServer) httpServer.on("upgrade", async (req, socket, head) => {
791
+ try {
792
+ if (new URL(req.url ?? "/", "http://localhost").pathname !== "/__vibes_sub") return;
793
+ const bearerProto = (req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim()).filter(Boolean).find((p) => p.startsWith("vibes-bearer."));
794
+ let userId = null;
795
+ if (bearerProto) {
796
+ const token = bearerProto.slice(13);
797
+ try {
798
+ userId = (await subAuthMW(new Request("http://localhost/__vibes_sub", { headers: { authorization: `Bearer ${token}` } })))?.userId ?? null;
799
+ } catch {}
800
+ }
801
+ wss.handleUpgrade(req, socket, head, (ws) => {
802
+ const subClient = {
803
+ readyState: 1,
804
+ send(data) {
805
+ try {
806
+ ws.send(data);
807
+ } catch {}
808
+ },
809
+ ctx: { userId }
810
+ };
811
+ addSubClient(subClient);
812
+ ws.on("message", (data) => {
813
+ handleSubMessage(subClient, typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : Array.isArray(data) ? Buffer.concat(data).toString("utf8") : Buffer.from(data).toString("utf8"));
814
+ });
815
+ const closeOut = () => {
816
+ if (subClient.readyState === 3) return;
817
+ subClient.readyState = 3;
818
+ removeSubClient(subClient);
819
+ };
820
+ ws.on("close", closeOut);
821
+ ws.on("error", closeOut);
822
+ });
823
+ } catch {
824
+ try {
825
+ socket.destroy();
826
+ } catch {}
827
+ }
828
+ });
829
+ const storageMod = await import("@omg-dev/server");
830
+ server.middlewares.use(async (req, res, next) => {
831
+ if (!req.url) return next();
832
+ if (!req.url.startsWith("/_vibes_storage/")) return next();
833
+ try {
834
+ const qIdx = req.url.indexOf("?");
835
+ const rawPath = qIdx === -1 ? req.url : req.url.slice(0, qIdx);
836
+ const queryStr = qIdx === -1 ? "" : req.url.slice(qIdx + 1);
837
+ const rel = decodeURIComponent(rawPath.slice(16));
838
+ const token = new URLSearchParams(queryStr).get("t") ?? "";
839
+ if (!rel || rel.includes("..") || rel.includes("//")) {
840
+ res.statusCode = 400;
841
+ return res.end(JSON.stringify({ error: "invalid path" }));
842
+ }
843
+ let scope;
844
+ let userId = "";
845
+ let key = "";
846
+ if (rel.startsWith("users/")) {
847
+ const rest = rel.slice(6);
848
+ const slash = rest.indexOf("/");
849
+ if (slash <= 0) {
850
+ res.statusCode = 400;
851
+ return res.end(JSON.stringify({ error: "invalid path" }));
852
+ }
853
+ scope = "user";
854
+ userId = rest.slice(0, slash);
855
+ key = rest.slice(slash + 1);
856
+ } else if (rel.startsWith("app/")) {
857
+ scope = "app";
858
+ key = rel.slice(4);
859
+ } else {
860
+ res.statusCode = 400;
861
+ return res.end(JSON.stringify({ error: "invalid scope prefix" }));
862
+ }
863
+ const method = (req.method ?? "GET").toUpperCase();
864
+ if (method === "PUT") {
865
+ if (!storageMod._verifyDevStorageToken(rel, "put", token)) {
866
+ res.statusCode = 403;
867
+ return res.end(JSON.stringify({ error: "invalid or expired token" }));
868
+ }
869
+ const chunks = [];
870
+ let size = 0;
871
+ const MAX = 25 * 1024 * 1024;
872
+ req.on("data", (c) => {
873
+ size += c.length;
874
+ if (size > MAX) {
875
+ req.destroy();
876
+ res.statusCode = 413;
877
+ res.end(JSON.stringify({ error: "exceeds 25MB" }));
878
+ return;
879
+ }
880
+ chunks.push(c);
881
+ });
882
+ req.on("end", () => {
883
+ try {
884
+ storageMod._devStorageWrite(scope, userId, key, Buffer.concat(chunks));
885
+ res.statusCode = 200;
886
+ res.setHeader("Content-Type", "application/json");
887
+ res.end(JSON.stringify({
888
+ ok: true,
889
+ size
890
+ }));
891
+ } catch (err) {
892
+ res.statusCode = 500;
893
+ res.end(JSON.stringify({ error: String(err.message) }));
894
+ }
895
+ });
896
+ req.on("error", () => {
897
+ res.statusCode = 400;
898
+ res.end();
899
+ });
900
+ return;
901
+ }
902
+ if (method === "GET" || method === "HEAD") {
903
+ if (!storageMod._verifyDevStorageToken(rel, "get", token)) {
904
+ res.statusCode = 403;
905
+ return res.end(JSON.stringify({ error: "invalid or expired token" }));
906
+ }
907
+ const abs = storageMod._devStorageRead(scope, userId, key);
908
+ if (!abs) {
909
+ res.statusCode = 404;
910
+ return res.end(JSON.stringify({ error: "not found" }));
911
+ }
912
+ const stat = fs.statSync(abs);
913
+ res.setHeader("Content-Length", String(stat.size));
914
+ const ext = path.extname(key).toLowerCase();
915
+ res.setHeader("Content-Type", {
916
+ ".png": "image/png",
917
+ ".jpg": "image/jpeg",
918
+ ".jpeg": "image/jpeg",
919
+ ".gif": "image/gif",
920
+ ".webp": "image/webp",
921
+ ".svg": "image/svg+xml",
922
+ ".json": "application/json",
923
+ ".pdf": "application/pdf",
924
+ ".mp4": "video/mp4",
925
+ ".webm": "video/webm",
926
+ ".txt": "text/plain"
927
+ }[ext] ?? "application/octet-stream");
928
+ res.statusCode = 200;
929
+ if (method === "HEAD") return res.end();
930
+ fs.createReadStream(abs).pipe(res);
931
+ return;
932
+ }
933
+ if (method === "OPTIONS") {
934
+ res.statusCode = 204;
935
+ res.setHeader("Access-Control-Allow-Origin", "*");
936
+ res.setHeader("Access-Control-Allow-Methods", "PUT,GET,HEAD,OPTIONS");
937
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
938
+ return res.end();
939
+ }
940
+ res.statusCode = 405;
941
+ res.end(JSON.stringify({ error: "method not allowed" }));
942
+ } catch (err) {
943
+ res.statusCode = 500;
944
+ res.end(JSON.stringify({ error: String(err.message) }));
945
+ }
946
+ });
947
+ server.middlewares.use(async (req, res, next) => {
948
+ const isApi = req.url?.startsWith("/api/");
949
+ const isVibesInternal = req.url?.startsWith("/_vibes/");
950
+ if (!isApi && !isVibesInternal) return next();
951
+ try {
952
+ const url = `http://localhost${req.url}`;
953
+ const headers = {};
954
+ for (const [key, value] of Object.entries(req.headers)) if (typeof value === "string") headers[key] = value;
955
+ else if (Array.isArray(value)) headers[key] = value.join(", ");
956
+ const body = await new Promise((resolve, reject) => {
957
+ const chunks = [];
958
+ req.on("data", (chunk) => chunks.push(chunk));
959
+ req.on("end", () => resolve(Buffer.concat(chunks)));
960
+ req.on("error", reject);
961
+ });
962
+ const fetchReq = new Request(url, {
963
+ method: req.method,
964
+ headers,
965
+ body: body.length > 0 ? body : void 0
966
+ });
967
+ const response = isVibesInternal ? await serverInstance.fetch(fetchReq) : await serverInstance.apiHandler(fetchReq);
968
+ res.statusCode = response.status;
969
+ response.headers.forEach((value, key) => {
970
+ res.setHeader(key, value);
971
+ });
972
+ const responseBody = await response.arrayBuffer();
973
+ res.end(Buffer.from(responseBody));
974
+ } catch (err) {
975
+ console.error("[vibes] API handler error:", err);
976
+ res.statusCode = 500;
977
+ res.end(JSON.stringify({ error: "Internal server error" }));
978
+ }
979
+ });
980
+ console.log("[vibes] Dev server ready (with realtime).");
981
+ },
982
+ async handleHotUpdate({ file }) {
983
+ const rel = path.relative(resolvedRoot, file);
984
+ if (rel === "schema.ts") {
985
+ console.log("[vibes] schema.ts changed — regenerating...");
986
+ await regenerateSchema(resolvedRoot);
987
+ if (serverInstance) {
988
+ const newSchema = await loadSchema(resolvedRoot);
989
+ if (newSchema) serverInstance.migrate(newSchema);
990
+ }
991
+ return [];
992
+ }
993
+ if (rel.startsWith("functions/") && rel.endsWith(".ts")) {
994
+ console.log(`[vibes] functions/${path.basename(file)} changed — rescanning routes + triggers...`);
995
+ await regenerateRoutes(resolvedRoot);
996
+ try {
997
+ await regenerateTriggers(resolvedRoot);
998
+ await serverInstance?.reloadTriggers();
999
+ } catch (err) {
1000
+ console.error(`[vibes] trigger scan failed:`, err);
1001
+ }
1002
+ try {
1003
+ await regenerateWorkflows(resolvedRoot);
1004
+ await serverInstance?.reloadWorkflows();
1005
+ } catch (err) {
1006
+ console.error(`[vibes] workflow scan failed:`, err);
1007
+ }
1008
+ serverInstance?.reloadFunctions();
1009
+ return [];
1010
+ }
1011
+ },
1012
+ async buildStart() {
1013
+ await generateAll(resolvedRoot);
1014
+ },
1015
+ async transform(code, id) {
1016
+ if (config?.command !== "build") return null;
1017
+ if (!pwaEnabled && !feedbackEnabled && !brandBadgeEnabled) return null;
1018
+ if (!isAppEntry(id, resolvedRoot)) return null;
1019
+ const imports = [];
1020
+ if (brandBadgeEnabled && !brandBadgeInjected) if (!await this.resolve("@omg-dev/sdk/brand/auto", id)) this.warn("[vibes] brand badge: @omg-dev/sdk/brand/auto is not available — badge skipped. Update @omg-dev/sdk, or silence this with vibes({ brandBadge: false }).");
1021
+ else {
1022
+ brandBadgeInjected = true;
1023
+ imports.push(`import "@omg-dev/sdk/brand/auto";`);
1024
+ }
1025
+ if (feedbackEnabled && !brandBadgeEnabled && !feedbackInjected) if (!await this.resolve("@omg-dev/sdk/feedback/auto", id)) this.warn("[vibes] feedback: @omg-dev/sdk is not installed — shake-to-report skipped. Add @omg-dev/sdk to dependencies, or silence this with vibes({ feedback: false }).");
1026
+ else {
1027
+ feedbackInjected = true;
1028
+ imports.push(`import "@omg-dev/sdk/feedback/auto";`);
1029
+ }
1030
+ if (pwaEnabled && pwaOpts.autoPrompt !== false && !pwaAutoInjected) if (!await this.resolve("@omg-dev/pwa/auto", id)) this.warn("[vibes] pwa: @omg-dev/pwa is not installed — soft install prompt skipped. Add @omg-dev/pwa to dependencies, or silence this with vibes({ pwa: { autoPrompt: false } }).");
1031
+ else {
1032
+ pwaAutoInjected = true;
1033
+ imports.push(`import "@omg-dev/pwa/auto";`);
1034
+ }
1035
+ if (pwaEnabled && !brandBadgeEnabled && !remixCtaInjected) {
1036
+ if (await pwaDeclaresSubpathExport((source, importer) => this.resolve(source, importer), id, "./remix-cta")) {
1037
+ remixCtaInjected = true;
1038
+ imports.push(`import "@omg-dev/pwa/remix-cta";`);
1039
+ }
1040
+ }
1041
+ if (imports.length === 0) return null;
1042
+ return {
1043
+ code: `${code}\n${imports.join("\n")}\n`,
1044
+ map: null
1045
+ };
1046
+ },
1047
+ generateBundle() {
1048
+ if (!pwaEnabled) return;
1049
+ if (userManifestHref()) return;
1050
+ const indexPath = path.join(resolvedRoot, "index.html");
1051
+ const cfg = resolvePwaConfig(pwaOpts, fs.existsSync(indexPath) ? fs.readFileSync(indexPath, "utf-8") : "");
1052
+ this.emitFile({
1053
+ type: "asset",
1054
+ fileName: MANIFEST_FILE,
1055
+ source: buildWebManifest(cfg, existingIconFiles())
1056
+ });
1057
+ },
1058
+ closeBundle() {
1059
+ serverInstance?.close();
1060
+ serverInstance = null;
1061
+ }
1062
+ };
1063
+ }
1064
+ async function loadSchema(root) {
1065
+ const schemaPath = path.join(root, "schema.ts");
1066
+ if (!fs.existsSync(schemaPath)) return null;
1067
+ try {
1068
+ return (await import(`${schemaPath}?t=${Date.now()}`)).default;
1069
+ } catch (err) {
1070
+ console.error("[vibes] Failed to load schema.ts:", err);
1071
+ return null;
1072
+ }
1073
+ }
1074
+ function readPreviewAppId(root) {
1075
+ const fromEnv = process.env.VIBES_APP_ID?.trim();
1076
+ if (fromEnv && /^[a-z0-9-]+$/.test(fromEnv)) return fromEnv;
1077
+ const metaPath = path.join(root, ".vibes", "app.json");
1078
+ if (!fs.existsSync(metaPath)) return null;
1079
+ try {
1080
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
1081
+ return typeof meta.appId === "string" && /^[a-z0-9-]+$/.test(meta.appId) ? meta.appId : null;
1082
+ } catch {
1083
+ return null;
1084
+ }
1085
+ }
1086
+ //#endregion
1087
+ export { vibes as default };