@amirhosseinnateghi/vibed-cli 0.1.2 → 0.1.4

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 +604 -58
  2. package/dist/vibed.cjs +11703 -0
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1248,10 +1248,10 @@ var require_decode = __commonJS({
1248
1248
  this.state = EntityDecoderState.NumericDecimal;
1249
1249
  return this.stateNumericDecimal(str2, offset);
1250
1250
  };
1251
- EntityDecoder2.prototype.addToNumericResult = function(str2, start, end, base) {
1251
+ EntityDecoder2.prototype.addToNumericResult = function(str2, start, end, base2) {
1252
1252
  if (start !== end) {
1253
1253
  var digitCount = end - start;
1254
- this.result = this.result * Math.pow(base, digitCount) + parseInt(str2.substr(start, digitCount), base);
1254
+ this.result = this.result * Math.pow(base2, digitCount) + parseInt(str2.substr(start, digitCount), base2);
1255
1255
  this.consumed += digitCount;
1256
1256
  }
1257
1257
  };
@@ -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"
@@ -6055,8 +6073,33 @@ var config = {
6055
6073
  // Light model that auto-tags the experience category. Falls back to a keyword
6056
6074
  // heuristic if the call fails.
6057
6075
  BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
6058
- BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 8192),
6076
+ // Speech-to-text model for the builder's voice input (docs/builder-architecture.md).
6077
+ // Gemini 2.5 Flash transcribes the recorded clip via GapGPT's OpenAI-compatible
6078
+ // chat API (an `input_audio` content part) — verified to accept the exact
6079
+ // formats browsers produce (Chrome webm/opus, Safari mp4). It's multilingual,
6080
+ // so Persian and English speech both work with no per-language config.
6081
+ BUILDER_TRANSCRIBE_MODEL: strEnv("BUILDER_TRANSCRIBE_MODEL", "gemini-2.5-flash"),
6082
+ // Upload cap for a voice clip. A minute of webm/opus is ~250KB; 8MB is ample
6083
+ // headroom while still bounding what one request can hand the model.
6084
+ BUILDER_TRANSCRIBE_MAX_BYTES: intEnv("BUILDER_TRANSCRIBE_MAX_BYTES", 8388608),
6085
+ // Output budget per model call. 8k proved too small in prod — a rich game is
6086
+ // ~7-8k tokens of file alone, and a truncated file used to slip through as a
6087
+ // text dump. 24576 was probed OK against every catalog model on GapGPT.
6088
+ BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 24576),
6059
6089
  BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
6090
+ // Hard cap for a single build turn (across the model + one repair). The build
6091
+ // runs detached from the request, so this — not the browser — is what stops a
6092
+ // runaway/abandoned turn. The provider's own 90s per-stall timeout still applies.
6093
+ // Never cut a LIVE generation — these two guards only catch dead work:
6094
+ // stall = total silence (not even a keep-alive byte) long enough to mean the
6095
+ // connection is dead (TCP half-open never errors on its own); the turn cap is
6096
+ // a platform-cost safety ceiling for the whole turn incl. one repair pass.
6097
+ BUILDER_STALL_TIMEOUT_MS: intEnv("BUILDER_STALL_TIMEOUT_MS", 3e5),
6098
+ BUILDER_TURN_TIMEOUT_MS: intEnv("BUILDER_TURN_TIMEOUT_MS", 9e5),
6099
+ // Once a build has run this long, the UI nudges the user to go explore, and on
6100
+ // completion the server drops a notification (§7.1) — so a slow build never
6101
+ // traps them staring at a spinner.
6102
+ BUILDER_SLOW_MS: intEnv("BUILDER_SLOW_MS", 12e3),
6060
6103
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6061
6104
  };
6062
6105
  function strEnv(key, fallback) {
@@ -6079,6 +6122,31 @@ function boolEnv(key, fallback) {
6079
6122
  return v === "true" || v === "1";
6080
6123
  }
6081
6124
 
6125
+ // ../shared/src/notifications.ts
6126
+ var NOTIF_VERB = {
6127
+ like_milestone: `liked your ${ITEM.one}`,
6128
+ comment: `commented on your ${ITEM.one}`,
6129
+ reply: "replied to your comment",
6130
+ follow: "started following you",
6131
+ mention: "mentioned you",
6132
+ remix: `remixed your ${ITEM.one}`
6133
+ };
6134
+ var SYSTEM_NOTIF = {
6135
+ build_ready: { icon: "\u2728", text: "Your build is ready \u2014 tap to see it", tone: "info" },
6136
+ build_failed: { icon: "\u26A0\uFE0F", text: "A build didn't finish \u2014 tap to try again", tone: "error" },
6137
+ // Moderation reject (spec §9): the vibe never went live (or was pulled), so
6138
+ // the inbox row must carry everything — title + reason arrive in the payload.
6139
+ post_rejected: { icon: "\u{1F6AB}", text: `Your ${ITEM.one} didn't pass moderation`, tone: "error" }
6140
+ };
6141
+ var NOTIF_PREF_GROUPS = ["follow", "comments", "likes", "remixes", "builder"];
6142
+ var NOTIF_GROUP_LABEL = {
6143
+ follow: { title: "New followers", hint: "when someone follows you" },
6144
+ comments: { title: "Comments", hint: "comments, replies & mentions" },
6145
+ likes: { title: "Likes", hint: `when your ${ITEM.many} hit like milestones` },
6146
+ remixes: { title: "Remixes", hint: `when someone remixes your ${ITEM.one}` },
6147
+ builder: { title: "Builder", hint: "when a build finishes or fails" }
6148
+ };
6149
+
6082
6150
  // ../shared/src/policy.ts
6083
6151
  function boolEnv2(key, fallback) {
6084
6152
  const v = typeof process !== "undefined" ? process.env[key] : void 0;
@@ -9759,23 +9827,23 @@ var ZodEffects = class extends ZodType {
9759
9827
  }
9760
9828
  if (effect.type === "transform") {
9761
9829
  if (ctx.common.async === false) {
9762
- const base = this._def.schema._parseSync({
9830
+ const base2 = this._def.schema._parseSync({
9763
9831
  data: ctx.data,
9764
9832
  path: ctx.path,
9765
9833
  parent: ctx
9766
9834
  });
9767
- if (!isValid(base))
9835
+ if (!isValid(base2))
9768
9836
  return INVALID;
9769
- const result = effect.transform(base.value, checkCtx);
9837
+ const result = effect.transform(base2.value, checkCtx);
9770
9838
  if (result instanceof Promise) {
9771
9839
  throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
9772
9840
  }
9773
9841
  return { status: status.value, value: result };
9774
9842
  } else {
9775
- return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
9776
- if (!isValid(base))
9843
+ return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base2) => {
9844
+ if (!isValid(base2))
9777
9845
  return INVALID;
9778
- return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
9846
+ return Promise.resolve(effect.transform(base2.value, checkCtx)).then((result) => ({
9779
9847
  status: status.value,
9780
9848
  value: result
9781
9849
  }));
@@ -10151,10 +10219,107 @@ var coerce = {
10151
10219
  };
10152
10220
  var NEVER = INVALID;
10153
10221
 
10222
+ // ../shared/src/storage.ts
10223
+ var STORAGE = {
10224
+ /** Total shared bytes per post (values, serialized). */
10225
+ SHARED_MAX_BYTES: intEnv("STORE_SHARED_MAX_BYTES", 1048576),
10226
+ // 1 MB
10227
+ /** Total shared records (rows) per post. */
10228
+ SHARED_MAX_RECORDS: intEnv("STORE_SHARED_MAX_RECORDS", 5e3),
10229
+ /** Serialized size cap for a single value. */
10230
+ VALUE_MAX_BYTES: intEnv("STORE_VALUE_MAX_BYTES", 16384),
10231
+ // 16 KB
10232
+ /** Records a single author may hold in one post (anti-flood / anti-grief). */
10233
+ AUTHOR_MAX_RECORDS: intEnv("STORE_AUTHOR_MAX_RECORDS", 100),
10234
+ /** Distinct collections per post. */
10235
+ MAX_COLLECTIONS: intEnv("STORE_MAX_COLLECTIONS", 32),
10236
+ /** Local-tier budget per (post, identity) — enforced by the host broker. */
10237
+ LOCAL_MAX_BYTES: intEnv("STORE_LOCAL_MAX_BYTES", 262144),
10238
+ // 256 KB
10239
+ /** Max records returnable in one query page. */
10240
+ QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10241
+ /**
10242
+ * Platform kill-switch for anonymous shared writes. ON by default: the real
10243
+ * gate is per-post owner opt-in (`anon_writes` + writePolicy `anyone`), so a
10244
+ * post only accepts guest writes when its owner turns them on. Set the env to
10245
+ * `false` to disable anon writes platform-wide regardless of per-post config.
10246
+ */
10247
+ ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", true)
10248
+ };
10249
+ var COLLECTION_RE = /^[a-z0-9][a-z0-9:_-]{0,63}$/;
10250
+ var collectionSchemaStore = external_exports.string().regex(COLLECTION_RE, "invalid collection name");
10251
+ var STORE_KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,127}$/;
10252
+ var RESERVED_STORE_KEYS = ["submit", "rank", "increment"];
10253
+ var storeKeySchema = external_exports.string().regex(STORE_KEY_RE, "invalid key").refine((k) => !RESERVED_STORE_KEYS.includes(k), {
10254
+ message: "reserved key"
10255
+ });
10256
+ var STORE_WRITE_POLICIES = ["anyone", "logged_in", "owner_only"];
10257
+ var STORE_SORTS = ["num", "-num", "created", "-created"];
10258
+ var storeValueSchema = external_exports.unknown().refine(
10259
+ (v) => {
10260
+ try {
10261
+ const s = JSON.stringify(v);
10262
+ return s !== void 0 && byteLength(s) <= STORAGE.VALUE_MAX_BYTES;
10263
+ } catch {
10264
+ return false;
10265
+ }
10266
+ },
10267
+ { message: `value must be JSON-serializable and <= ${STORAGE.VALUE_MAX_BYTES} bytes` }
10268
+ );
10269
+ var storePutSchema = external_exports.object({
10270
+ value: storeValueSchema,
10271
+ /** Optimistic concurrency: fail with `conflict` unless current version matches. */
10272
+ ifVersion: external_exports.number().int().positive().optional()
10273
+ });
10274
+ var storeAppendSchema = external_exports.object({
10275
+ value: storeValueSchema,
10276
+ /** Optional numeric field to index (sorting/leaderboards). */
10277
+ num: external_exports.number().finite().optional()
10278
+ });
10279
+ var storeIncrementSchema = external_exports.object({
10280
+ delta: external_exports.number().int().min(-1e6).max(1e6).default(1)
10281
+ });
10282
+ var storeSubmitSchema = external_exports.object({
10283
+ /** The number that ranks this author (mapped into `num`). */
10284
+ score: external_exports.number().finite(),
10285
+ /** Conflict rule when the author already has a row. */
10286
+ keep: external_exports.enum(["max", "min", "last"]).default("max"),
10287
+ /** Small display payload stored alongside (same value-size gate). */
10288
+ meta: storeValueSchema.optional()
10289
+ });
10290
+ var storeQuerySchema = external_exports.object({
10291
+ sort: external_exports.enum(STORE_SORTS).default("-created"),
10292
+ limit: external_exports.coerce.number().int().min(1).max(STORAGE.QUERY_MAX_LIMIT).default(30),
10293
+ cursor: external_exports.string().max(200).optional(),
10294
+ /** Only the caller's records. */
10295
+ mine: external_exports.coerce.boolean().default(false)
10296
+ });
10297
+ var storeAdminSchema = external_exports.discriminatedUnion("action", [
10298
+ external_exports.object({ action: external_exports.literal("freeze") }),
10299
+ external_exports.object({ action: external_exports.literal("unfreeze") }),
10300
+ external_exports.object({ action: external_exports.literal("set_policy"), writePolicy: external_exports.enum(STORE_WRITE_POLICIES) }),
10301
+ external_exports.object({ action: external_exports.literal("set_anon_writes"), anonWrites: external_exports.boolean() }),
10302
+ external_exports.object({ action: external_exports.literal("reset"), collection: collectionSchemaStore.optional() })
10303
+ ]);
10304
+ var storageOptInSchema = external_exports.object({
10305
+ local: external_exports.boolean().default(false),
10306
+ shared: external_exports.boolean().default(false)
10307
+ });
10308
+ function byteLength(s) {
10309
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
10310
+ let n = 0;
10311
+ for (let i = 0; i < s.length; i++) {
10312
+ const c = s.codePointAt(i);
10313
+ n += c <= 127 ? 1 : c <= 2047 ? 2 : c <= 65535 ? 3 : (i++, 4);
10314
+ }
10315
+ return n;
10316
+ }
10317
+
10154
10318
  // ../shared/src/schemas.ts
10155
10319
  var handleSchema = external_exports.string().regex(/^[a-z0-9_.]{3,24}$/, "3\u201324 chars: a\u2013z, 0\u20139, _ or .");
10156
10320
  var categorySchema = external_exports.enum(CATEGORIES);
10157
10321
  var visibilitySchema = external_exports.enum(VISIBILITY);
10322
+ var coverModeSchema = external_exports.enum(COVER_MODES);
10158
10323
  var sourceSchema = external_exports.enum(EXPERIENCE_SOURCE);
10159
10324
  var tagsSchema = external_exports.array(external_exports.string().min(1).max(40)).max(10).transform(
10160
10325
  (tags) => Array.from(
@@ -10168,8 +10333,12 @@ var publishSchema = external_exports.object({
10168
10333
  tags: tagsSchema,
10169
10334
  remixable: external_exports.boolean().default(true),
10170
10335
  visibility: visibilitySchema.default("public"),
10336
+ coverMode: coverModeSchema.default("gif"),
10171
10337
  draftArtifactKey: external_exports.string().min(1),
10172
- buildSessionId: external_exports.string().uuid().optional()
10338
+ buildSessionId: external_exports.string().uuid().optional(),
10339
+ /** Storage opt-in override; when omitted, publish auto-detects from the source
10340
+ * (docs/post-storage.md §9 — default off, enabled only when actually used). */
10341
+ storage: storageOptInSchema.optional()
10173
10342
  });
10174
10343
  var patchExperienceSchema = external_exports.object({
10175
10344
  title: external_exports.string().min(1).max(120).optional(),
@@ -10177,25 +10346,44 @@ var patchExperienceSchema = external_exports.object({
10177
10346
  tags: tagsSchema.optional(),
10178
10347
  category: categorySchema.optional(),
10179
10348
  remixable: external_exports.boolean().optional(),
10180
- visibility: visibilitySchema.optional()
10349
+ visibility: visibilitySchema.optional(),
10350
+ coverMode: coverModeSchema.optional()
10181
10351
  });
10182
10352
  var commentSchema = external_exports.object({
10183
10353
  body: external_exports.string().min(1).max(500),
10184
10354
  parentId: external_exports.string().uuid().optional()
10185
10355
  });
10186
10356
  var eventSchema = external_exports.object({
10187
- kind: external_exports.enum(["impression", "play", "play_time", "share"]),
10357
+ // "record"/"screenshot" fire when a viewer captures the vibe (see the in-iframe
10358
+ // bridge capture flow, docs/architecture.md §5.4). Plain-text `viewEvents.kind`
10359
+ // column, so new kinds need no migration.
10360
+ kind: external_exports.enum(["impression", "play", "play_time", "share", "record", "screenshot"]),
10188
10361
  ms: external_exports.number().int().nonnegative().optional()
10189
10362
  });
10190
10363
  var createBuildSchema = external_exports.object({
10191
10364
  mode: external_exports.enum(["ai", "remix"]),
10192
10365
  remixParentId: external_exports.string().uuid().optional()
10193
10366
  });
10367
+ var turnAttachmentSchema = external_exports.object({
10368
+ kind: external_exports.enum(["image", "audio", "file", "sketch"]),
10369
+ key: external_exports.string().max(300).default(""),
10370
+ name: external_exports.string().max(200),
10371
+ // NUL bytes break Postgres jsonb (and mean binary-not-text) — strip defensively.
10372
+ text: external_exports.string().max(5e4).transform((t) => t.replace(/\u0000/g, "")).optional()
10373
+ });
10374
+ var createTurnRequestSchema = external_exports.object({
10375
+ clientTurnId: external_exports.string().uuid(),
10376
+ content: external_exports.string().max(4e3).default(""),
10377
+ attachments: external_exports.array(turnAttachmentSchema).max(6).optional(),
10378
+ model: external_exports.string().max(100).optional()
10379
+ }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0, {
10380
+ message: "Send a message or an attachment."
10381
+ });
10194
10382
  var builderAttachmentSchema = external_exports.object({
10195
- kind: external_exports.enum(["image", "file"]),
10383
+ kind: external_exports.enum(["image", "file", "audio"]),
10196
10384
  name: external_exports.string().max(200),
10197
- dataUrl: external_exports.string().max(8e6).optional(),
10198
- // ~6MB image as base64 data URL
10385
+ // base64 data URL for image/audio (~8.7MB raw fits, covering a short song).
10386
+ dataUrl: external_exports.string().max(12e6).optional(),
10199
10387
  text: external_exports.string().max(5e4).optional()
10200
10388
  });
10201
10389
  var builderMessageSchema = external_exports.object({
@@ -10227,10 +10415,21 @@ var reportSchema = external_exports.object({
10227
10415
  var patchMeSchema = external_exports.object({
10228
10416
  displayName: external_exports.string().min(1).max(60).optional(),
10229
10417
  bio: external_exports.string().max(300).optional(),
10230
- avatar: external_exports.string().url().optional(),
10418
+ // A stored image URL, or "" to remove the photo and fall back to the color avatar.
10419
+ avatar: external_exports.string().url().or(external_exports.literal("")).optional(),
10420
+ // Fallback avatar colour (used when there's no photo). Hex like #6C4CF1.
10421
+ avatarBg: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/, "hex colour, e.g. #6C4CF1").optional(),
10231
10422
  handle: handleSchema.optional()
10232
10423
  // once
10233
10424
  });
10425
+ var waitlistSchema = external_exports.object({
10426
+ email: external_exports.string().trim().email().max(200),
10427
+ note: external_exports.string().trim().max(500).optional()
10428
+ });
10429
+ var waitlistReviewSchema = external_exports.object({
10430
+ requestId: external_exports.string().uuid(),
10431
+ action: external_exports.enum(["approve", "reject"])
10432
+ });
10234
10433
  var aiConnectionSchema = external_exports.object({
10235
10434
  provider: external_exports.enum(AI_PROVIDERS),
10236
10435
  apiKey: external_exports.string().trim().min(12).max(300)
@@ -10244,28 +10443,225 @@ var feedQuerySchema = external_exports.object({
10244
10443
  var searchQuerySchema = external_exports.object({
10245
10444
  q: external_exports.string().default(""),
10246
10445
  tab: external_exports.enum(["experiences", "tags", "creators"]).default("experiences"),
10247
- remixableOnly: external_exports.coerce.boolean().default(false),
10446
+ // Query params arrive as strings; z.coerce.boolean() is just Boolean(x), so
10447
+ // "false"/"0" would (wrongly) become true and the filter could never be turned
10448
+ // off. Parse the string explicitly: only "true"/"1" (or a real boolean) is true.
10449
+ remixableOnly: external_exports.union([external_exports.boolean(), external_exports.string()]).optional().transform((v) => v === true || v === "true" || v === "1"),
10248
10450
  cursor: external_exports.string().optional(),
10249
10451
  limit: external_exports.coerce.number().int().min(1).max(30).default(15)
10250
10452
  });
10453
+ var registerPushDeviceSchema = external_exports.object({
10454
+ provider: external_exports.enum(PUSH_PROVIDERS),
10455
+ token: external_exports.string().trim().min(16).max(4096),
10456
+ keys: external_exports.record(external_exports.string().max(512)).default({}),
10457
+ platform: external_exports.enum(DEVICE_PLATFORMS).default("web"),
10458
+ userAgent: external_exports.string().trim().max(400).default("")
10459
+ }).superRefine((v, ctx) => {
10460
+ if (v.provider === "webpush" && (!v.keys.p256dh || !v.keys.auth)) {
10461
+ ctx.addIssue({
10462
+ code: external_exports.ZodIssueCode.custom,
10463
+ path: ["keys"],
10464
+ message: "webpush requires keys.p256dh and keys.auth"
10465
+ });
10466
+ }
10467
+ });
10468
+ var unregisterPushDeviceSchema = external_exports.object({
10469
+ token: external_exports.string().trim().min(16).max(4096)
10470
+ });
10471
+ var notificationsQuerySchema = external_exports.object({
10472
+ filter: external_exports.enum(["all", "mentions", "remixes"]).default("all"),
10473
+ // Keyset cursor: the `createdAt` ISO timestamp of the last item seen.
10474
+ cursor: external_exports.string().datetime({ offset: true }).optional(),
10475
+ limit: external_exports.coerce.number().int().min(1).max(80).default(30)
10476
+ });
10477
+ var notifPrefsPatchSchema = external_exports.object({
10478
+ enabled: external_exports.boolean().optional(),
10479
+ groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10480
+ });
10251
10481
 
10252
- // ../shared/src/models.ts
10253
- 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" },
10260
- { 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" },
10263
- // ── 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 },
10265
- { id: "gpt-5.2-pro", label: "GPT-5.2 Pro", tier: "subscription", note: "Premium" },
10266
- { id: "gemini-3-pro-preview", label: "Gemini 3 Pro", tier: "subscription", note: "Premium", vision: true }
10482
+ // ../shared/src/builderEvents.ts
10483
+ var BUILD_EVENT_VERSION = 1;
10484
+ var BUILD_PHASES = [
10485
+ "queued",
10486
+ // accepted, waiting for a worker slot
10487
+ "thinking",
10488
+ // model connected, no visible output yet (keep-alives flowing)
10489
+ "writing",
10490
+ // streaming the vibe's code/prose
10491
+ "validating",
10492
+ // sandbox pass on the produced artifact
10493
+ "repairing"
10494
+ // one automatic fix round after a validation failure
10267
10495
  ];
10268
- var FREE_IDS = new Set(BUILDER_MODELS.filter((m) => m.tier === "free").map((m) => m.id));
10496
+ var base = { v: external_exports.literal(BUILD_EVENT_VERSION), turnId: external_exports.string() };
10497
+ var buildEventSchema = external_exports.discriminatedUnion("type", [
10498
+ /** Assistant prose delta (never contains the HTML code block). */
10499
+ external_exports.object({ ...base, type: external_exports.literal("token"), text: external_exports.string() }),
10500
+ /**
10501
+ * Honest heartbeat, ~every 2s while the turn runs: real elapsed ms, real
10502
+ * output-token count, and the executor's actual phase. The UI renders this
10503
+ * as a live counter — a number that moves because work is happening.
10504
+ */
10505
+ external_exports.object({
10506
+ ...base,
10507
+ type: external_exports.literal("progress"),
10508
+ phase: external_exports.enum(BUILD_PHASES),
10509
+ elapsedMs: external_exports.number(),
10510
+ tokensOut: external_exports.number()
10511
+ }),
10512
+ /** Friendly status chip ("index.html", "Fixing a couple of things…"). */
10513
+ external_exports.object({ ...base, type: external_exports.literal("status"), summary: external_exports.string() }),
10514
+ /** First-message category classification. */
10515
+ external_exports.object({ ...base, type: external_exports.literal("category"), category: external_exports.string() }),
10516
+ /** The validated draft landed. Preview loads it from the draft URL. */
10517
+ external_exports.object({ ...base, type: external_exports.literal("artifact"), artifactKey: external_exports.string() }),
10518
+ /** Terminal: failure with taxonomy code + human message. */
10519
+ external_exports.object({
10520
+ ...base,
10521
+ type: external_exports.literal("error"),
10522
+ code: external_exports.string(),
10523
+ message: external_exports.string()
10524
+ }),
10525
+ /** Terminal: success. */
10526
+ external_exports.object({ ...base, type: external_exports.literal("done") })
10527
+ ]);
10528
+
10529
+ // ../shared/src/i18n.ts
10530
+ var en = {
10531
+ "nav.home": "Home",
10532
+ "nav.search": "Search",
10533
+ "nav.create": "Create",
10534
+ "nav.notifications": "Notifications",
10535
+ "nav.profile": "Profile",
10536
+ "feed.explore": "Explore",
10537
+ "feed.following": "Following",
10538
+ "feed.tapToInteract": "Tap to interact",
10539
+ "feed.loading": `Loading ${ITEM.one}`,
10540
+ "feed.exit": "Exit",
10541
+ "feed.remixedFrom": "Remixed from @{handle}",
10542
+ "feed.remixedFromRemoved": `Remixed from a removed ${ITEM.one}`,
10543
+ "feed.newVersionOf": "New version of {title}",
10544
+ "feed.end.title": "You're all caught up",
10545
+ "feed.end.body": `No new ${ITEM.many} right now \u2014 remix one you loved, or check back soon.`,
10546
+ "feed.end.remix": "Find something to remix",
10547
+ "feed.end.top": "Back to top",
10548
+ "rail.like": "Like",
10549
+ "rail.comment": "Comment",
10550
+ "rail.remix": "Remix",
10551
+ "rail.save": "Save",
10552
+ "rail.share": "Share",
10553
+ "comments.title": "Comments",
10554
+ "comments.placeholder": "Add a comment\u2026",
10555
+ "comments.reply": "Reply",
10556
+ "comments.empty": "No comments yet \u2014 be the first.",
10557
+ "comments.post": "Post",
10558
+ "share.title": "Share",
10559
+ "share.copyLink": "Copy link",
10560
+ "share.copied": "Link copied",
10561
+ "capture.record": "Record",
10562
+ "capture.screenshot": "Screenshot",
10563
+ "capture.recording": "Recording",
10564
+ "capture.stop": "Stop",
10565
+ "capture.preparing": "Preparing\u2026",
10566
+ "capture.processing": "Finishing\u2026",
10567
+ "capture.preview.videoTitle": "Your clip",
10568
+ "capture.preview.imageTitle": "Your screenshot",
10569
+ "capture.save": "Save",
10570
+ "capture.shareVia": "Share via\u2026",
10571
+ "capture.retry": "Try again",
10572
+ "capture.error": "Capture failed \u2014 try again.",
10573
+ "capture.allowShare": "Allow \u201CShare this tab\u201D to record \u2014 it captures just the vibe.",
10574
+ "capture.pickTab": "Choose \u201CThis Tab\u201D so it records only the vibe.",
10575
+ "capture.unsupported": `This ${ITEM.one} can't be captured yet. Try again in a moment.`,
10576
+ "create.title": "Create",
10577
+ "create.buildWithAI": "Build with AI",
10578
+ "create.import": "Import",
10579
+ "create.remix": "Remix something",
10580
+ "create.prompt.placeholder": "Describe what you want to build\u2026",
10581
+ "create.prompt.starters": "Start from a template",
10582
+ "builder.publish": "Publish",
10583
+ "builder.thinking": "Building\u2026",
10584
+ "builder.updated": "Updated \u2713",
10585
+ "builder.placeholder": "Ask for a change\u2026",
10586
+ "builder.mockNotice": "Preview builder \u2014 AI generation is mocked for now.",
10587
+ "import.title": "Import",
10588
+ "import.paste": "Paste HTML, or drop an .html file",
10589
+ "import.validate": "Validate",
10590
+ "import.warnings": "Warnings",
10591
+ "publish.title": "Publish",
10592
+ "publish.postTitle": "Title",
10593
+ "publish.caption": "Caption",
10594
+ "publish.category": "Category",
10595
+ "publish.remixable": "Allow remixes",
10596
+ "publish.public": "Public",
10597
+ "publish.unlisted": "Unlisted",
10598
+ "publish.cta": "Publish",
10599
+ "publish.remixCredit": "Remixing @{handle} \u2014 {title}",
10600
+ "publish.toast.published": "Published! \u{1F389}",
10601
+ "publish.toast.remix": "Remix published \u{1F501} @{handle} was credited",
10602
+ "remix.confirmTitle": "Remixing @{handle}'s {title}",
10603
+ "remix.confirmBody": `They'll be credited on your ${ITEM.one}. Attribution can't be removed.`,
10604
+ "remix.start": "Start remixing",
10605
+ "remix.galleryTitle": "Remixes",
10606
+ "remix.sortPopular": "Popular",
10607
+ "remix.sortRecent": "Recent",
10608
+ "remix.count": "{n} remixes",
10609
+ "remix.identicalWarn": "This looks identical to the original \u2014 publish anyway?",
10610
+ "search.placeholder": `Search ${ITEM.many}, tags, creators`,
10611
+ "search.tab.experiences": ITEM.Many,
10612
+ "search.tab.tags": "Hashtags",
10613
+ "search.tab.creators": "Creators",
10614
+ "search.remixableOnly": "Remixable only",
10615
+ "search.trending": "Trending",
10616
+ "profile.tab.experiences": ITEM.Many,
10617
+ "profile.tab.remixes": "Remixes",
10618
+ "profile.tab.saved": "Saved",
10619
+ "profile.followers": "followers",
10620
+ "profile.following": "following",
10621
+ "profile.likes": "likes",
10622
+ "profile.follow": "Follow",
10623
+ "profile.unfollow": "Following",
10624
+ "profile.edit": "Edit profile",
10625
+ "notifs.all": "All",
10626
+ "notifs.mentions": "Mentions",
10627
+ "notifs.remixes": "Remixes",
10628
+ "notifs.remixedYou": `@{handle} remixed your ${ITEM.one}`,
10629
+ "notifs.liked": `@{handle} liked your ${ITEM.one}`,
10630
+ "notifs.commented": "@{handle} commented",
10631
+ "notifs.followed": "@{handle} started following you",
10632
+ "notifs.empty": "Nothing here yet.",
10633
+ "collections.title": "Saved",
10634
+ "collections.new": "New collection",
10635
+ "analytics.title": "Analytics",
10636
+ "analytics.views": "Views",
10637
+ "analytics.plays": "Plays",
10638
+ "analytics.playRate": "Play rate",
10639
+ "analytics.avgTime": "Avg. time",
10640
+ "analytics.likes": "Likes",
10641
+ "analytics.comments": "Comments",
10642
+ "analytics.shares": "Shares",
10643
+ "analytics.saves": "Saves",
10644
+ "analytics.remixes": "Remixes",
10645
+ "post.options": `${ITEM.One} options`,
10646
+ "post.analytics": "View analytics",
10647
+ "post.edit": "Edit caption",
10648
+ "post.delete": `Delete ${ITEM.one}`,
10649
+ "post.deleteConfirm": `Delete this ${ITEM.one}? This can't be undone.`,
10650
+ "post.captionPlaceholder": "Write a caption\u2026",
10651
+ "auth.signIn": "Sign in",
10652
+ "auth.google": "Continue with Google",
10653
+ "auth.github": "Continue with GitHub",
10654
+ "auth.dev": "Dev login",
10655
+ "auth.needAccount": "Sign in to like, comment, and remix.",
10656
+ "common.cancel": "Cancel",
10657
+ "common.save": "Save",
10658
+ "common.back": "Back",
10659
+ "common.retry": "Retry",
10660
+ "common.loading": "Loading\u2026",
10661
+ "common.report": "Report",
10662
+ "common.delete": "Delete",
10663
+ "common.views": "{n} views"
10664
+ };
10269
10665
 
10270
10666
  // ../shared/src/tokens.ts
10271
10667
  var colors = {
@@ -10368,6 +10764,15 @@ function validate(html, opts) {
10368
10764
  errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
10369
10765
  return { ok: false, errors, warnings };
10370
10766
  }
10767
+ const scriptOpens = (html.match(/<script\b/gi) ?? []).length;
10768
+ const scriptCloses = (html.match(/<\/script\s*>/gi) ?? []).length;
10769
+ if (scriptOpens > scriptCloses) {
10770
+ errors.push({
10771
+ code: "TRUNCATED_DOC",
10772
+ message: "The file ends mid-<script> \u2014 it looks cut off. Regenerate or paste the complete file."
10773
+ });
10774
+ return { ok: false, errors, warnings };
10775
+ }
10371
10776
  if (on("frames") && root.querySelector("iframe, frame, object, embed")) {
10372
10777
  errors.push({
10373
10778
  code: "FRAME_TAG",
@@ -10452,6 +10857,12 @@ function validate(html, opts) {
10452
10857
  }
10453
10858
  }
10454
10859
  }
10860
+ if (/(localStorage|sessionStorage|indexedDB|document\s*\.\s*cookie)/.test(html)) {
10861
+ warnings.push({
10862
+ code: "BROWSER_STORAGE",
10863
+ 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)."
10864
+ });
10865
+ }
10455
10866
  const lower = html.toLowerCase();
10456
10867
  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
10868
  if (hasCredField) {
@@ -10950,19 +11361,35 @@ function normalizeCategory(input) {
10950
11361
  const match = CATEGORIES.find((c) => c.toLowerCase() === input.toLowerCase());
10951
11362
  return match ?? "Other";
10952
11363
  }
10953
- async function publishArtifact(client, html, meta, projectDir) {
10954
- const imported = await apiFetch(client, "POST", "/imports", { html });
11364
+ async function importDraft(client, html) {
11365
+ const r = await apiFetch(client, "POST", "/imports", { html });
11366
+ return { draftArtifactKey: r.draftArtifactKey, previewUrl: r.previewUrl };
11367
+ }
11368
+ async function publishDraft(client, draftArtifactKey, meta, html, projectDir) {
10955
11369
  const exp = await apiFetch(client, "POST", "/experiences", {
10956
- title: meta.title?.trim() || inferTitle(html, projectDir),
11370
+ title: meta.title?.trim() || (html ? inferTitle(html, projectDir ?? ".") : "Untitled"),
10957
11371
  caption: meta.caption ?? "",
10958
11372
  category: normalizeCategory(meta.category),
10959
11373
  tags: meta.tags ?? [],
10960
11374
  remixable: meta.remixable ?? true,
10961
11375
  visibility: meta.visibility ?? "public",
10962
- draftArtifactKey: imported.draftArtifactKey
11376
+ draftArtifactKey
10963
11377
  });
10964
11378
  return { id: exp.id, status: exp.status, url: `${client.apiBase}/e/${exp.id}` };
10965
11379
  }
11380
+ async function publishArtifact(client, html, meta, projectDir) {
11381
+ const { draftArtifactKey } = await importDraft(client, html);
11382
+ return publishDraft(client, draftArtifactKey, meta, html, projectDir);
11383
+ }
11384
+ function experienceIdFrom(input) {
11385
+ const m = input.match(/\/e\/([0-9a-f-]{36})/i);
11386
+ return (m?.[1] ?? input.trim()).toLowerCase();
11387
+ }
11388
+ async function deleteExperience(client, idOrUrl) {
11389
+ const id = experienceIdFrom(idOrUrl);
11390
+ await apiFetch(client, "DELETE", `/experiences/${id}`);
11391
+ return id;
11392
+ }
10966
11393
 
10967
11394
  // src/index.ts
10968
11395
  function parseArgs(argv) {
@@ -10996,7 +11423,11 @@ var USAGE = `vibed \u2014 make it vibed
10996
11423
  Usage:
10997
11424
  vibed login Authenticate this machine (opens the browser)
10998
11425
  vibed check [path] Check if a project can be published (no network)
11426
+ vibed preview [path] Bundle + open the artifact locally (no publish)
11427
+ vibed draft [path] Upload a private, hosted preview link (no publish)
10999
11428
  vibed publish [path] Bundle the project and publish it to vibed
11429
+ vibed publish --draft-key <k> Promote a draft preview to a public post
11430
+ vibed delete <id|url> Delete one of your posts
11000
11431
  vibed whoami Show the signed-in account
11001
11432
  vibed logout Remove the saved token
11002
11433
 
@@ -11032,7 +11463,58 @@ async function cmdCheck(args) {
11032
11463
  });
11033
11464
  return result.ok ? 0 : 1;
11034
11465
  }
11035
- async function cmdPublish(args) {
11466
+ async function cmdPreview(args) {
11467
+ const json = isTrue(args.flags.json);
11468
+ const dir2 = resolve2(args._[1] ?? ".");
11469
+ const cfg = await loadConfig({ api: str(args.flags.api) });
11470
+ const policy = isTrue(args.flags["local-policy"]) ? void 0 : await fetchPolicy(cfg.apiBase);
11471
+ const result = await bundleProject(dir2, {
11472
+ entry: str(args.flags.entry),
11473
+ build: !isTrue(args.flags["no-build"]),
11474
+ policy,
11475
+ onLog: (m) => !json && console.error(m)
11476
+ });
11477
+ if (!json) console.log(formatReport(result));
11478
+ if (!result.html) {
11479
+ if (json) out(json, "", { ok: false, fatal: result.fatal });
11480
+ return 1;
11481
+ }
11482
+ const file2 = join3(tmpdir(), `vibed-preview-${result.sizeBytes ?? 0}.html`);
11483
+ await writeFile2(file2, result.html, "utf8");
11484
+ const url = pathToFileURL(file2).href;
11485
+ if (json) {
11486
+ out(json, "", { ok: result.ok, previewFile: file2, previewUrl: url });
11487
+ } else {
11488
+ console.log(`
11489
+ Preview \u2192 ${file2}`);
11490
+ openBrowser(url);
11491
+ }
11492
+ return result.ok ? 0 : 1;
11493
+ }
11494
+ function metaFromArgs(args) {
11495
+ return {
11496
+ title: str(args.flags.title),
11497
+ caption: str(args.flags.caption),
11498
+ category: str(args.flags.category),
11499
+ tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11500
+ remixable: !isTrue(args.flags["no-remix"]),
11501
+ visibility: isTrue(args.flags.unlisted) ? "unlisted" : "public"
11502
+ };
11503
+ }
11504
+ function reportPublishError(e) {
11505
+ if (e instanceof ApiError && e.code === "unauthorized") {
11506
+ console.error("Your token is no longer valid. Run `vibed login` again.");
11507
+ return 1;
11508
+ }
11509
+ console.error(`Failed: ${e instanceof Error ? e.message : String(e)}`);
11510
+ if (e instanceof ApiError && e.details) {
11511
+ const d = e.details;
11512
+ for (const err of d.errors ?? []) console.error(` \u2717 ${err.code}: ${err.message}`);
11513
+ if (!d.errors) console.error(` ${JSON.stringify(e.details)}`);
11514
+ }
11515
+ return 1;
11516
+ }
11517
+ async function cmdDraft(args) {
11036
11518
  const json = isTrue(args.flags.json);
11037
11519
  const dir2 = resolve2(args._[1] ?? ".");
11038
11520
  const cfg = await loadConfig({ api: str(args.flags.api) });
@@ -11047,37 +11529,94 @@ async function cmdPublish(args) {
11047
11529
  policy,
11048
11530
  onLog: (m) => !json && console.error(m)
11049
11531
  });
11532
+ if (!result.ok || !result.html) {
11533
+ out(json, formatReport(result), { ...result, html: void 0 });
11534
+ return 1;
11535
+ }
11536
+ if (!json) console.log(formatReport(result));
11537
+ try {
11538
+ const d = await importDraft({ apiBase: cfg.apiBase, token: cfg.token }, result.html);
11539
+ out(
11540
+ json,
11541
+ `
11542
+ \u2713 Preview (not published): ${d.previewUrl ?? "(no preview URL)"}
11543
+ Publish it with: vibed publish --draft-key ${d.draftArtifactKey} --title "\u2026" --category <Cat>`,
11544
+ { ok: true, ...d }
11545
+ );
11546
+ return 0;
11547
+ } catch (e) {
11548
+ return reportPublishError(e);
11549
+ }
11550
+ }
11551
+ async function cmdPublish(args) {
11552
+ const json = isTrue(args.flags.json);
11553
+ const cfg = await loadConfig({ api: str(args.flags.api) });
11554
+ if (!cfg.token) {
11555
+ console.error("Not signed in. Run `vibed login` first.");
11556
+ return 1;
11557
+ }
11558
+ const client = { apiBase: cfg.apiBase, token: cfg.token };
11559
+ const meta = metaFromArgs(args);
11560
+ const draftKey = str(args.flags["draft-key"]);
11561
+ if (draftKey) {
11562
+ try {
11563
+ const res = await publishDraft(client, draftKey, meta);
11564
+ out(json, `
11565
+ \u2713 Published (${res.status}): ${res.url}`, res);
11566
+ return 0;
11567
+ } catch (e) {
11568
+ return reportPublishError(e);
11569
+ }
11570
+ }
11571
+ const dir2 = resolve2(args._[1] ?? ".");
11572
+ const policy = await fetchPolicy(cfg.apiBase);
11573
+ const result = await bundleProject(dir2, {
11574
+ entry: str(args.flags.entry),
11575
+ build: !isTrue(args.flags["no-build"]),
11576
+ policy,
11577
+ onLog: (m) => !json && console.error(m)
11578
+ });
11050
11579
  if (!result.ok || !result.html) {
11051
11580
  out(json, formatReport(result), { ...result, html: void 0 });
11052
11581
  console.error("\nFix the blockers above, then run `vibed publish` again.");
11053
11582
  return 1;
11054
11583
  }
11055
11584
  if (!json) console.log(formatReport(result));
11056
- const meta = {
11057
- title: str(args.flags.title),
11058
- caption: str(args.flags.caption),
11059
- category: str(args.flags.category),
11060
- tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11061
- remixable: !isTrue(args.flags["no-remix"]),
11062
- visibility: isTrue(args.flags.unlisted) ? "unlisted" : "public"
11063
- };
11064
11585
  try {
11065
- const res = await publishArtifact({ apiBase: cfg.apiBase, token: cfg.token }, result.html, meta, dir2);
11586
+ const res = await publishArtifact(client, result.html, meta, dir2);
11066
11587
  out(json, `
11067
11588
  \u2713 Published (${res.status}): ${res.url}`, res);
11068
11589
  return 0;
11069
11590
  } catch (e) {
11070
- if (e instanceof ApiError && e.code === "unauthorized") {
11071
- console.error("Your token is no longer valid. Run `vibed login` again.");
11591
+ return reportPublishError(e);
11592
+ }
11593
+ }
11594
+ async function cmdDelete(args) {
11595
+ const json = isTrue(args.flags.json);
11596
+ const target = args._[1];
11597
+ if (!target) {
11598
+ console.error("Usage: vibed delete <experience-id | https://\u2026/e/<id>>");
11599
+ return 1;
11600
+ }
11601
+ const cfg = await loadConfig({ api: str(args.flags.api) });
11602
+ if (!cfg.token) {
11603
+ console.error("Not signed in. Run `vibed login` first.");
11604
+ return 1;
11605
+ }
11606
+ try {
11607
+ const id = await deleteExperience({ apiBase: cfg.apiBase, token: cfg.token }, target);
11608
+ out(json, `\u2713 Deleted post ${id}.`, { ok: true, id });
11609
+ return 0;
11610
+ } catch (e) {
11611
+ if (e instanceof ApiError && (e.code === "forbidden" || e.status === 403)) {
11612
+ console.error("That post isn't yours to delete.");
11072
11613
  return 1;
11073
11614
  }
11074
- console.error(`Publish failed: ${e instanceof Error ? e.message : String(e)}`);
11075
- if (e instanceof ApiError && e.details) {
11076
- const d = e.details;
11077
- for (const err of d.errors ?? []) console.error(` \u2717 ${err.code}: ${err.message}`);
11078
- if (!d.errors) console.error(` ${JSON.stringify(e.details)}`);
11615
+ if (e instanceof ApiError && (e.code === "not_found" || e.status === 404)) {
11616
+ console.error("No such post (or already deleted).");
11617
+ return 1;
11079
11618
  }
11080
- return 1;
11619
+ return reportPublishError(e);
11081
11620
  }
11082
11621
  }
11083
11622
  async function cmdLogin(args) {
@@ -11131,8 +11670,15 @@ async function main() {
11131
11670
  return cmdWhoami(args);
11132
11671
  case "check":
11133
11672
  return cmdCheck(args);
11673
+ case "preview":
11674
+ return cmdPreview(args);
11675
+ case "draft":
11676
+ return cmdDraft(args);
11134
11677
  case "publish":
11135
11678
  return cmdPublish(args);
11679
+ case "delete":
11680
+ case "rm":
11681
+ return cmdDelete(args);
11136
11682
  case void 0:
11137
11683
  case "help":
11138
11684
  case "--help":