@amirhosseinnateghi/vibed-cli 0.1.2 → 0.1.3

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/dist/index.js +377 -19
  2. package/dist/vibed.cjs +11515 -0
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -5973,7 +5973,10 @@ var require_dist = __commonJS({
5973
5973
  });
5974
5974
 
5975
5975
  // src/index.ts
5976
- import { resolve as resolve2 } from "node:path";
5976
+ import { resolve as resolve2, join as join3 } from "node:path";
5977
+ import { writeFile as writeFile2 } from "node:fs/promises";
5978
+ import { tmpdir } from "node:os";
5979
+ import { pathToFileURL } from "node:url";
5977
5980
 
5978
5981
  // src/config.ts
5979
5982
  import { homedir } from "node:os";
@@ -6019,6 +6022,18 @@ import { dirname, extname, isAbsolute, join as join2, resolve } from "node:path"
6019
6022
  // ../sandbox/src/index.ts
6020
6023
  var import_node_html_parser = __toESM(require_dist(), 1);
6021
6024
 
6025
+ // ../shared/src/terms.ts
6026
+ var ITEM = {
6027
+ /** lowercase singular — "vibe" */
6028
+ one: "vibe",
6029
+ /** lowercase plural — "vibes" */
6030
+ many: "vibes",
6031
+ /** Capitalized singular — "Vibe" */
6032
+ One: "Vibe",
6033
+ /** Capitalized plural — "Vibes" */
6034
+ Many: "Vibes"
6035
+ };
6036
+
6022
6037
  // ../shared/src/constants.ts
6023
6038
  var CATEGORIES = [
6024
6039
  "Game",
@@ -6031,10 +6046,13 @@ var CATEGORIES = [
6031
6046
  "Other"
6032
6047
  ];
6033
6048
  var VISIBILITY = ["public", "unlisted"];
6049
+ var COVER_MODES = ["image", "gif"];
6034
6050
  var EXPERIENCE_SOURCE = ["builder", "import", "remix"];
6051
+ var PUSH_PROVIDERS = ["webpush", "fcm", "apns"];
6052
+ var DEVICE_PLATFORMS = ["web", "ios", "android"];
6035
6053
  var config = {
6036
- ARTIFACT_MAX_BYTES: intEnv("ARTIFACT_MAX_BYTES", 3145728),
6037
- // 3 MB
6054
+ ARTIFACT_MAX_BYTES: intEnv("ARTIFACT_MAX_BYTES", 8388608),
6055
+ // 8 MB — headroom for an embedded audio track
6038
6056
  ARTIFACT_ALLOWED_ORIGINS: strEnv(
6039
6057
  "ARTIFACT_ALLOWED_ORIGINS",
6040
6058
  "fonts.googleapis.com,fonts.gstatic.com,cdnjs.cloudflare.com,cdn.jsdelivr.net,unpkg.com"
@@ -6057,6 +6075,19 @@ var config = {
6057
6075
  BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
6058
6076
  BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 8192),
6059
6077
  BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
6078
+ // Hard cap for a single build turn (across the model + one repair). The build
6079
+ // runs detached from the request, so this — not the browser — is what stops a
6080
+ // runaway/abandoned turn. The provider's own 90s per-stall timeout still applies.
6081
+ // Never cut a LIVE generation — these two guards only catch dead work:
6082
+ // stall = total silence (not even a keep-alive byte) long enough to mean the
6083
+ // connection is dead (TCP half-open never errors on its own); the turn cap is
6084
+ // a platform-cost safety ceiling for the whole turn incl. one repair pass.
6085
+ BUILDER_STALL_TIMEOUT_MS: intEnv("BUILDER_STALL_TIMEOUT_MS", 3e5),
6086
+ BUILDER_TURN_TIMEOUT_MS: intEnv("BUILDER_TURN_TIMEOUT_MS", 9e5),
6087
+ // Once a build has run this long, the UI nudges the user to go explore, and on
6088
+ // completion the server drops a notification (§7.1) — so a slow build never
6089
+ // traps them staring at a spinner.
6090
+ BUILDER_SLOW_MS: intEnv("BUILDER_SLOW_MS", 12e3),
6060
6091
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6061
6092
  };
6062
6093
  function strEnv(key, fallback) {
@@ -6079,6 +6110,31 @@ function boolEnv(key, fallback) {
6079
6110
  return v === "true" || v === "1";
6080
6111
  }
6081
6112
 
6113
+ // ../shared/src/notifications.ts
6114
+ var NOTIF_VERB = {
6115
+ like_milestone: `liked your ${ITEM.one}`,
6116
+ comment: `commented on your ${ITEM.one}`,
6117
+ reply: "replied to your comment",
6118
+ follow: "started following you",
6119
+ mention: "mentioned you",
6120
+ remix: `remixed your ${ITEM.one}`
6121
+ };
6122
+ var SYSTEM_NOTIF = {
6123
+ build_ready: { icon: "\u2728", text: "Your build is ready \u2014 tap to see it", tone: "info" },
6124
+ build_failed: { icon: "\u26A0\uFE0F", text: "A build didn't finish \u2014 tap to try again", tone: "error" },
6125
+ // Moderation reject (spec §9): the vibe never went live (or was pulled), so
6126
+ // the inbox row must carry everything — title + reason arrive in the payload.
6127
+ post_rejected: { icon: "\u{1F6AB}", text: `Your ${ITEM.one} didn't pass moderation`, tone: "error" }
6128
+ };
6129
+ var NOTIF_PREF_GROUPS = ["follow", "comments", "likes", "remixes", "builder"];
6130
+ var NOTIF_GROUP_LABEL = {
6131
+ follow: { title: "New followers", hint: "when someone follows you" },
6132
+ comments: { title: "Comments", hint: "comments, replies & mentions" },
6133
+ likes: { title: "Likes", hint: `when your ${ITEM.many} hit like milestones` },
6134
+ remixes: { title: "Remixes", hint: `when someone remixes your ${ITEM.one}` },
6135
+ builder: { title: "Builder", hint: "when a build finishes or fails" }
6136
+ };
6137
+
6082
6138
  // ../shared/src/policy.ts
6083
6139
  function boolEnv2(key, fallback) {
6084
6140
  const v = typeof process !== "undefined" ? process.env[key] : void 0;
@@ -10151,10 +10207,102 @@ var coerce = {
10151
10207
  };
10152
10208
  var NEVER = INVALID;
10153
10209
 
10210
+ // ../shared/src/storage.ts
10211
+ var STORAGE = {
10212
+ /** Total shared bytes per post (values, serialized). */
10213
+ SHARED_MAX_BYTES: intEnv("STORE_SHARED_MAX_BYTES", 1048576),
10214
+ // 1 MB
10215
+ /** Total shared records (rows) per post. */
10216
+ SHARED_MAX_RECORDS: intEnv("STORE_SHARED_MAX_RECORDS", 5e3),
10217
+ /** Serialized size cap for a single value. */
10218
+ VALUE_MAX_BYTES: intEnv("STORE_VALUE_MAX_BYTES", 16384),
10219
+ // 16 KB
10220
+ /** Records a single author may hold in one post (anti-flood / anti-grief). */
10221
+ AUTHOR_MAX_RECORDS: intEnv("STORE_AUTHOR_MAX_RECORDS", 100),
10222
+ /** Distinct collections per post. */
10223
+ MAX_COLLECTIONS: intEnv("STORE_MAX_COLLECTIONS", 32),
10224
+ /** Local-tier budget per (post, identity) — enforced by the host broker. */
10225
+ LOCAL_MAX_BYTES: intEnv("STORE_LOCAL_MAX_BYTES", 262144),
10226
+ // 256 KB
10227
+ /** Max records returnable in one query page. */
10228
+ QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10229
+ /** Whether anonymous (logged-out) shared writes may be enabled per post. */
10230
+ ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", false)
10231
+ };
10232
+ var COLLECTION_RE = /^[a-z0-9][a-z0-9:_-]{0,63}$/;
10233
+ var collectionSchemaStore = external_exports.string().regex(COLLECTION_RE, "invalid collection name");
10234
+ var STORE_KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,127}$/;
10235
+ var RESERVED_STORE_KEYS = ["submit", "rank", "increment"];
10236
+ var storeKeySchema = external_exports.string().regex(STORE_KEY_RE, "invalid key").refine((k) => !RESERVED_STORE_KEYS.includes(k), {
10237
+ message: "reserved key"
10238
+ });
10239
+ var STORE_WRITE_POLICIES = ["anyone", "logged_in", "owner_only"];
10240
+ var STORE_SORTS = ["num", "-num", "created", "-created"];
10241
+ var storeValueSchema = external_exports.unknown().refine(
10242
+ (v) => {
10243
+ try {
10244
+ const s = JSON.stringify(v);
10245
+ return s !== void 0 && byteLength(s) <= STORAGE.VALUE_MAX_BYTES;
10246
+ } catch {
10247
+ return false;
10248
+ }
10249
+ },
10250
+ { message: `value must be JSON-serializable and <= ${STORAGE.VALUE_MAX_BYTES} bytes` }
10251
+ );
10252
+ var storePutSchema = external_exports.object({
10253
+ value: storeValueSchema,
10254
+ /** Optimistic concurrency: fail with `conflict` unless current version matches. */
10255
+ ifVersion: external_exports.number().int().positive().optional()
10256
+ });
10257
+ var storeAppendSchema = external_exports.object({
10258
+ value: storeValueSchema,
10259
+ /** Optional numeric field to index (sorting/leaderboards). */
10260
+ num: external_exports.number().finite().optional()
10261
+ });
10262
+ var storeIncrementSchema = external_exports.object({
10263
+ delta: external_exports.number().int().min(-1e6).max(1e6).default(1)
10264
+ });
10265
+ var storeSubmitSchema = external_exports.object({
10266
+ /** The number that ranks this author (mapped into `num`). */
10267
+ score: external_exports.number().finite(),
10268
+ /** Conflict rule when the author already has a row. */
10269
+ keep: external_exports.enum(["max", "min", "last"]).default("max"),
10270
+ /** Small display payload stored alongside (same value-size gate). */
10271
+ meta: storeValueSchema.optional()
10272
+ });
10273
+ var storeQuerySchema = external_exports.object({
10274
+ sort: external_exports.enum(STORE_SORTS).default("-created"),
10275
+ limit: external_exports.coerce.number().int().min(1).max(STORAGE.QUERY_MAX_LIMIT).default(30),
10276
+ cursor: external_exports.string().max(200).optional(),
10277
+ /** Only the caller's records. */
10278
+ mine: external_exports.coerce.boolean().default(false)
10279
+ });
10280
+ var storeAdminSchema = external_exports.discriminatedUnion("action", [
10281
+ external_exports.object({ action: external_exports.literal("freeze") }),
10282
+ external_exports.object({ action: external_exports.literal("unfreeze") }),
10283
+ external_exports.object({ action: external_exports.literal("set_policy"), writePolicy: external_exports.enum(STORE_WRITE_POLICIES) }),
10284
+ external_exports.object({ action: external_exports.literal("set_anon_writes"), anonWrites: external_exports.boolean() }),
10285
+ external_exports.object({ action: external_exports.literal("reset"), collection: collectionSchemaStore.optional() })
10286
+ ]);
10287
+ var storageOptInSchema = external_exports.object({
10288
+ local: external_exports.boolean().default(false),
10289
+ shared: external_exports.boolean().default(false)
10290
+ });
10291
+ function byteLength(s) {
10292
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
10293
+ let n = 0;
10294
+ for (let i = 0; i < s.length; i++) {
10295
+ const c = s.codePointAt(i);
10296
+ n += c <= 127 ? 1 : c <= 2047 ? 2 : c <= 65535 ? 3 : (i++, 4);
10297
+ }
10298
+ return n;
10299
+ }
10300
+
10154
10301
  // ../shared/src/schemas.ts
10155
10302
  var handleSchema = external_exports.string().regex(/^[a-z0-9_.]{3,24}$/, "3\u201324 chars: a\u2013z, 0\u20139, _ or .");
10156
10303
  var categorySchema = external_exports.enum(CATEGORIES);
10157
10304
  var visibilitySchema = external_exports.enum(VISIBILITY);
10305
+ var coverModeSchema = external_exports.enum(COVER_MODES);
10158
10306
  var sourceSchema = external_exports.enum(EXPERIENCE_SOURCE);
10159
10307
  var tagsSchema = external_exports.array(external_exports.string().min(1).max(40)).max(10).transform(
10160
10308
  (tags) => Array.from(
@@ -10168,8 +10316,12 @@ var publishSchema = external_exports.object({
10168
10316
  tags: tagsSchema,
10169
10317
  remixable: external_exports.boolean().default(true),
10170
10318
  visibility: visibilitySchema.default("public"),
10319
+ coverMode: coverModeSchema.default("gif"),
10171
10320
  draftArtifactKey: external_exports.string().min(1),
10172
- buildSessionId: external_exports.string().uuid().optional()
10321
+ buildSessionId: external_exports.string().uuid().optional(),
10322
+ /** Storage opt-in override; when omitted, publish auto-detects from the source
10323
+ * (docs/post-storage.md §9 — default off, enabled only when actually used). */
10324
+ storage: storageOptInSchema.optional()
10173
10325
  });
10174
10326
  var patchExperienceSchema = external_exports.object({
10175
10327
  title: external_exports.string().min(1).max(120).optional(),
@@ -10177,7 +10329,8 @@ var patchExperienceSchema = external_exports.object({
10177
10329
  tags: tagsSchema.optional(),
10178
10330
  category: categorySchema.optional(),
10179
10331
  remixable: external_exports.boolean().optional(),
10180
- visibility: visibilitySchema.optional()
10332
+ visibility: visibilitySchema.optional(),
10333
+ coverMode: coverModeSchema.optional()
10181
10334
  });
10182
10335
  var commentSchema = external_exports.object({
10183
10336
  body: external_exports.string().min(1).max(500),
@@ -10192,10 +10345,10 @@ var createBuildSchema = external_exports.object({
10192
10345
  remixParentId: external_exports.string().uuid().optional()
10193
10346
  });
10194
10347
  var builderAttachmentSchema = external_exports.object({
10195
- kind: external_exports.enum(["image", "file"]),
10348
+ kind: external_exports.enum(["image", "file", "audio"]),
10196
10349
  name: external_exports.string().max(200),
10197
- dataUrl: external_exports.string().max(8e6).optional(),
10198
- // ~6MB image as base64 data URL
10350
+ // base64 data URL for image/audio (~8.7MB raw fits, covering a short song).
10351
+ dataUrl: external_exports.string().max(12e6).optional(),
10199
10352
  text: external_exports.string().max(5e4).optional()
10200
10353
  });
10201
10354
  var builderMessageSchema = external_exports.object({
@@ -10227,10 +10380,21 @@ var reportSchema = external_exports.object({
10227
10380
  var patchMeSchema = external_exports.object({
10228
10381
  displayName: external_exports.string().min(1).max(60).optional(),
10229
10382
  bio: external_exports.string().max(300).optional(),
10230
- avatar: external_exports.string().url().optional(),
10383
+ // A stored image URL, or "" to remove the photo and fall back to the color avatar.
10384
+ avatar: external_exports.string().url().or(external_exports.literal("")).optional(),
10385
+ // Fallback avatar colour (used when there's no photo). Hex like #6C4CF1.
10386
+ avatarBg: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/, "hex colour, e.g. #6C4CF1").optional(),
10231
10387
  handle: handleSchema.optional()
10232
10388
  // once
10233
10389
  });
10390
+ var waitlistSchema = external_exports.object({
10391
+ email: external_exports.string().trim().email().max(200),
10392
+ note: external_exports.string().trim().max(500).optional()
10393
+ });
10394
+ var waitlistReviewSchema = external_exports.object({
10395
+ requestId: external_exports.string().uuid(),
10396
+ action: external_exports.enum(["approve", "reject"])
10397
+ });
10234
10398
  var aiConnectionSchema = external_exports.object({
10235
10399
  provider: external_exports.enum(AI_PROVIDERS),
10236
10400
  apiKey: external_exports.string().trim().min(12).max(300)
@@ -10244,28 +10408,185 @@ var feedQuerySchema = external_exports.object({
10244
10408
  var searchQuerySchema = external_exports.object({
10245
10409
  q: external_exports.string().default(""),
10246
10410
  tab: external_exports.enum(["experiences", "tags", "creators"]).default("experiences"),
10247
- remixableOnly: external_exports.coerce.boolean().default(false),
10411
+ // Query params arrive as strings; z.coerce.boolean() is just Boolean(x), so
10412
+ // "false"/"0" would (wrongly) become true and the filter could never be turned
10413
+ // off. Parse the string explicitly: only "true"/"1" (or a real boolean) is true.
10414
+ remixableOnly: external_exports.union([external_exports.boolean(), external_exports.string()]).optional().transform((v) => v === true || v === "true" || v === "1"),
10248
10415
  cursor: external_exports.string().optional(),
10249
10416
  limit: external_exports.coerce.number().int().min(1).max(30).default(15)
10250
10417
  });
10418
+ var registerPushDeviceSchema = external_exports.object({
10419
+ provider: external_exports.enum(PUSH_PROVIDERS),
10420
+ token: external_exports.string().trim().min(16).max(4096),
10421
+ keys: external_exports.record(external_exports.string().max(512)).default({}),
10422
+ platform: external_exports.enum(DEVICE_PLATFORMS).default("web"),
10423
+ userAgent: external_exports.string().trim().max(400).default("")
10424
+ }).superRefine((v, ctx) => {
10425
+ if (v.provider === "webpush" && (!v.keys.p256dh || !v.keys.auth)) {
10426
+ ctx.addIssue({
10427
+ code: external_exports.ZodIssueCode.custom,
10428
+ path: ["keys"],
10429
+ message: "webpush requires keys.p256dh and keys.auth"
10430
+ });
10431
+ }
10432
+ });
10433
+ var unregisterPushDeviceSchema = external_exports.object({
10434
+ token: external_exports.string().trim().min(16).max(4096)
10435
+ });
10436
+ var notificationsQuerySchema = external_exports.object({
10437
+ filter: external_exports.enum(["all", "mentions", "remixes"]).default("all"),
10438
+ // Keyset cursor: the `createdAt` ISO timestamp of the last item seen.
10439
+ cursor: external_exports.string().datetime({ offset: true }).optional(),
10440
+ limit: external_exports.coerce.number().int().min(1).max(80).default(30)
10441
+ });
10442
+ var notifPrefsPatchSchema = external_exports.object({
10443
+ enabled: external_exports.boolean().optional(),
10444
+ groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10445
+ });
10251
10446
 
10252
10447
  // ../shared/src/models.ts
10253
10448
  var BUILDER_MODELS = [
10254
- // ── Free (selectable now) — good quality, reasonable price ─────────
10255
- { id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", tier: "free", note: "Best for polished HTML", vision: true },
10256
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", tier: "free", note: "Fast \xB7 great UI \xB7 cheap", vision: true },
10257
- { id: "gpt-4o-mini", label: "GPT-4o mini", tier: "free", note: "Cheap & reliable", vision: true },
10258
- { id: "gpt-5-mini", label: "GPT-5 mini", tier: "free", note: "Newer, capable", vision: true },
10259
- { id: "gpt-5.1-codex", label: "GPT-5.1 Codex", tier: "free", note: "Code specialist" },
10449
+ // ── Free (selectable now) — ONLY genuinely strong builders ─────────
10450
+ // Curated 2026-07: every id verified against GapGPT with a real full-HTML
10451
+ // build (not a ping). Weak/cheap models were removed on purpose a bad
10452
+ // first build costs more in user trust than the tokens save.
10453
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", tier: "free", note: "Best for polished vibes \xB7 fast", vision: true },
10454
+ { id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", tier: "free", note: "Reliable all-rounder", vision: true },
10455
+ { id: "gpt-5.2", label: "GPT-5.2", tier: "free", note: "Strong generalist", vision: true },
10456
+ { id: "gpt-5.3-codex", label: "GPT-5.3 Codex", tier: "free", note: "Code specialist" },
10457
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", tier: "free", note: "Great UI sense", vision: true },
10260
10458
  { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", tier: "free", note: "Strong coder \xB7 great value" },
10261
- { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", tier: "free", note: "Fast \xB7 very cheap" },
10262
- { id: "gapgpt-qwen-3.6", label: "Qwen 3.6", tier: "free", note: "Good value" },
10459
+ { id: "qwen3-coder-480b-a35b-instruct", label: "Qwen3 Coder 480B", tier: "free", note: "Fast, capable coder" },
10263
10460
  // ── Subscription (locked until billing is enabled) — premium ──────
10264
- { id: "claude-opus-4-8", label: "Claude Opus 4.8", tier: "subscription", note: "Top quality", vision: true },
10461
+ // Opus 4.8 is owner-only early access: locked for everyone, live for the owner.
10462
+ { id: "claude-opus-4-8", label: "Claude Opus 4.8", tier: "subscription", note: "Top quality", vision: true, ownerOnly: true },
10265
10463
  { id: "gpt-5.2-pro", label: "GPT-5.2 Pro", tier: "subscription", note: "Premium" },
10266
10464
  { id: "gemini-3-pro-preview", label: "Gemini 3 Pro", tier: "subscription", note: "Premium", vision: true }
10267
10465
  ];
10268
10466
  var FREE_IDS = new Set(BUILDER_MODELS.filter((m) => m.tier === "free").map((m) => m.id));
10467
+ var OWNER_MODEL_IDS = new Set(BUILDER_MODELS.filter((m) => m.ownerOnly).map((m) => m.id));
10468
+
10469
+ // ../shared/src/i18n.ts
10470
+ var en = {
10471
+ "nav.home": "Home",
10472
+ "nav.search": "Search",
10473
+ "nav.create": "Create",
10474
+ "nav.notifications": "Notifications",
10475
+ "nav.profile": "Profile",
10476
+ "feed.explore": "Explore",
10477
+ "feed.following": "Following",
10478
+ "feed.tapToInteract": "Tap to interact",
10479
+ "feed.loading": `Loading ${ITEM.one}`,
10480
+ "feed.exit": "Exit",
10481
+ "feed.remixedFrom": "Remixed from @{handle}",
10482
+ "feed.remixedFromRemoved": `Remixed from a removed ${ITEM.one}`,
10483
+ "feed.newVersionOf": "New version of {title}",
10484
+ "feed.end.title": "You're all caught up",
10485
+ "feed.end.body": `No new ${ITEM.many} right now \u2014 remix one you loved, or check back soon.`,
10486
+ "feed.end.remix": "Find something to remix",
10487
+ "feed.end.top": "Back to top",
10488
+ "rail.like": "Like",
10489
+ "rail.comment": "Comment",
10490
+ "rail.remix": "Remix",
10491
+ "rail.save": "Save",
10492
+ "rail.share": "Share",
10493
+ "comments.title": "Comments",
10494
+ "comments.placeholder": "Add a comment\u2026",
10495
+ "comments.reply": "Reply",
10496
+ "comments.empty": "No comments yet \u2014 be the first.",
10497
+ "comments.post": "Post",
10498
+ "share.title": "Share",
10499
+ "share.copyLink": "Copy link",
10500
+ "share.copied": "Link copied",
10501
+ "create.title": "Create",
10502
+ "create.buildWithAI": "Build with AI",
10503
+ "create.import": "Import",
10504
+ "create.remix": "Remix something",
10505
+ "create.prompt.placeholder": "Describe what you want to build\u2026",
10506
+ "create.prompt.starters": "Start from a template",
10507
+ "builder.publish": "Publish",
10508
+ "builder.thinking": "Building\u2026",
10509
+ "builder.updated": "Updated \u2713",
10510
+ "builder.placeholder": "Ask for a change\u2026",
10511
+ "builder.mockNotice": "Preview builder \u2014 AI generation is mocked for now.",
10512
+ "import.title": "Import",
10513
+ "import.paste": "Paste HTML, or drop an .html file",
10514
+ "import.validate": "Validate",
10515
+ "import.warnings": "Warnings",
10516
+ "publish.title": "Publish",
10517
+ "publish.postTitle": "Title",
10518
+ "publish.caption": "Caption",
10519
+ "publish.category": "Category",
10520
+ "publish.remixable": "Allow remixes",
10521
+ "publish.public": "Public",
10522
+ "publish.unlisted": "Unlisted",
10523
+ "publish.cta": "Publish",
10524
+ "publish.remixCredit": "Remixing @{handle} \u2014 {title}",
10525
+ "publish.toast.published": "Published! \u{1F389}",
10526
+ "publish.toast.remix": "Remix published \u{1F501} @{handle} was credited",
10527
+ "remix.confirmTitle": "Remixing @{handle}'s {title}",
10528
+ "remix.confirmBody": `They'll be credited on your ${ITEM.one}. Attribution can't be removed.`,
10529
+ "remix.start": "Start remixing",
10530
+ "remix.galleryTitle": "Remixes",
10531
+ "remix.sortPopular": "Popular",
10532
+ "remix.sortRecent": "Recent",
10533
+ "remix.count": "{n} remixes",
10534
+ "remix.identicalWarn": "This looks identical to the original \u2014 publish anyway?",
10535
+ "search.placeholder": `Search ${ITEM.many}, tags, creators`,
10536
+ "search.tab.experiences": ITEM.Many,
10537
+ "search.tab.tags": "Hashtags",
10538
+ "search.tab.creators": "Creators",
10539
+ "search.remixableOnly": "Remixable only",
10540
+ "search.trending": "Trending",
10541
+ "profile.tab.experiences": ITEM.Many,
10542
+ "profile.tab.remixes": "Remixes",
10543
+ "profile.tab.saved": "Saved",
10544
+ "profile.followers": "followers",
10545
+ "profile.following": "following",
10546
+ "profile.likes": "likes",
10547
+ "profile.follow": "Follow",
10548
+ "profile.unfollow": "Following",
10549
+ "profile.edit": "Edit profile",
10550
+ "notifs.all": "All",
10551
+ "notifs.mentions": "Mentions",
10552
+ "notifs.remixes": "Remixes",
10553
+ "notifs.remixedYou": `@{handle} remixed your ${ITEM.one}`,
10554
+ "notifs.liked": `@{handle} liked your ${ITEM.one}`,
10555
+ "notifs.commented": "@{handle} commented",
10556
+ "notifs.followed": "@{handle} started following you",
10557
+ "notifs.empty": "Nothing here yet.",
10558
+ "collections.title": "Saved",
10559
+ "collections.new": "New collection",
10560
+ "analytics.title": "Analytics",
10561
+ "analytics.views": "Views",
10562
+ "analytics.plays": "Plays",
10563
+ "analytics.playRate": "Play rate",
10564
+ "analytics.avgTime": "Avg. time",
10565
+ "analytics.likes": "Likes",
10566
+ "analytics.comments": "Comments",
10567
+ "analytics.shares": "Shares",
10568
+ "analytics.saves": "Saves",
10569
+ "analytics.remixes": "Remixes",
10570
+ "post.options": `${ITEM.One} options`,
10571
+ "post.analytics": "View analytics",
10572
+ "post.edit": "Edit caption",
10573
+ "post.delete": `Delete ${ITEM.one}`,
10574
+ "post.deleteConfirm": `Delete this ${ITEM.one}? This can't be undone.`,
10575
+ "post.captionPlaceholder": "Write a caption\u2026",
10576
+ "auth.signIn": "Sign in",
10577
+ "auth.google": "Continue with Google",
10578
+ "auth.github": "Continue with GitHub",
10579
+ "auth.dev": "Dev login",
10580
+ "auth.needAccount": "Sign in to like, comment, and remix.",
10581
+ "common.cancel": "Cancel",
10582
+ "common.save": "Save",
10583
+ "common.back": "Back",
10584
+ "common.retry": "Retry",
10585
+ "common.loading": "Loading\u2026",
10586
+ "common.report": "Report",
10587
+ "common.delete": "Delete",
10588
+ "common.views": "{n} views"
10589
+ };
10269
10590
 
10270
10591
  // ../shared/src/tokens.ts
10271
10592
  var colors = {
@@ -10452,6 +10773,12 @@ function validate(html, opts) {
10452
10773
  }
10453
10774
  }
10454
10775
  }
10776
+ if (/(localStorage|sessionStorage|indexedDB|document\s*\.\s*cookie)/.test(html)) {
10777
+ warnings.push({
10778
+ code: "BROWSER_STORAGE",
10779
+ message: "Uses browser storage (localStorage/sessionStorage/indexedDB/cookies), which throws inside the vibed sandbox. Port persistence to window.vibed.storage (local per-viewer, shared collective)."
10780
+ });
10781
+ }
10455
10782
  const lower = html.toLowerCase();
10456
10783
  const hasCredField = root.querySelector('input[type="email"], input[type="text"], input[type="tel"]') != null && /(password|passcode|otp|verify|sign\s*in|log\s*in|account)/i.test(html);
10457
10784
  if (hasCredField) {
@@ -10996,6 +11323,7 @@ var USAGE = `vibed \u2014 make it vibed
10996
11323
  Usage:
10997
11324
  vibed login Authenticate this machine (opens the browser)
10998
11325
  vibed check [path] Check if a project can be published (no network)
11326
+ vibed preview [path] Bundle + open the artifact locally (no publish)
10999
11327
  vibed publish [path] Bundle the project and publish it to vibed
11000
11328
  vibed whoami Show the signed-in account
11001
11329
  vibed logout Remove the saved token
@@ -11032,6 +11360,34 @@ async function cmdCheck(args) {
11032
11360
  });
11033
11361
  return result.ok ? 0 : 1;
11034
11362
  }
11363
+ async function cmdPreview(args) {
11364
+ const json = isTrue(args.flags.json);
11365
+ const dir2 = resolve2(args._[1] ?? ".");
11366
+ const cfg = await loadConfig({ api: str(args.flags.api) });
11367
+ const policy = isTrue(args.flags["local-policy"]) ? void 0 : await fetchPolicy(cfg.apiBase);
11368
+ const result = await bundleProject(dir2, {
11369
+ entry: str(args.flags.entry),
11370
+ build: !isTrue(args.flags["no-build"]),
11371
+ policy,
11372
+ onLog: (m) => !json && console.error(m)
11373
+ });
11374
+ if (!json) console.log(formatReport(result));
11375
+ if (!result.html) {
11376
+ if (json) out(json, "", { ok: false, fatal: result.fatal });
11377
+ return 1;
11378
+ }
11379
+ const file2 = join3(tmpdir(), `vibed-preview-${result.sizeBytes ?? 0}.html`);
11380
+ await writeFile2(file2, result.html, "utf8");
11381
+ const url = pathToFileURL(file2).href;
11382
+ if (json) {
11383
+ out(json, "", { ok: result.ok, previewFile: file2, previewUrl: url });
11384
+ } else {
11385
+ console.log(`
11386
+ Preview \u2192 ${file2}`);
11387
+ openBrowser(url);
11388
+ }
11389
+ return result.ok ? 0 : 1;
11390
+ }
11035
11391
  async function cmdPublish(args) {
11036
11392
  const json = isTrue(args.flags.json);
11037
11393
  const dir2 = resolve2(args._[1] ?? ".");
@@ -11131,6 +11487,8 @@ async function main() {
11131
11487
  return cmdWhoami(args);
11132
11488
  case "check":
11133
11489
  return cmdCheck(args);
11490
+ case "preview":
11491
+ return cmdPreview(args);
11134
11492
  case "publish":
11135
11493
  return cmdPublish(args);
11136
11494
  case void 0: