@amirhosseinnateghi/vibed-cli 0.1.3 → 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 +242 -54
  2. package/dist/vibed.cjs +242 -54
  3. package/package.json +1 -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
  };
@@ -6073,7 +6073,19 @@ var config = {
6073
6073
  // Light model that auto-tags the experience category. Falls back to a keyword
6074
6074
  // heuristic if the call fails.
6075
6075
  BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
6076
- 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),
6077
6089
  BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
6078
6090
  // Hard cap for a single build turn (across the model + one repair). The build
6079
6091
  // runs detached from the request, so this — not the browser — is what stops a
@@ -9815,23 +9827,23 @@ var ZodEffects = class extends ZodType {
9815
9827
  }
9816
9828
  if (effect.type === "transform") {
9817
9829
  if (ctx.common.async === false) {
9818
- const base = this._def.schema._parseSync({
9830
+ const base2 = this._def.schema._parseSync({
9819
9831
  data: ctx.data,
9820
9832
  path: ctx.path,
9821
9833
  parent: ctx
9822
9834
  });
9823
- if (!isValid(base))
9835
+ if (!isValid(base2))
9824
9836
  return INVALID;
9825
- const result = effect.transform(base.value, checkCtx);
9837
+ const result = effect.transform(base2.value, checkCtx);
9826
9838
  if (result instanceof Promise) {
9827
9839
  throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
9828
9840
  }
9829
9841
  return { status: status.value, value: result };
9830
9842
  } else {
9831
- return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
9832
- if (!isValid(base))
9843
+ return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base2) => {
9844
+ if (!isValid(base2))
9833
9845
  return INVALID;
9834
- return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
9846
+ return Promise.resolve(effect.transform(base2.value, checkCtx)).then((result) => ({
9835
9847
  status: status.value,
9836
9848
  value: result
9837
9849
  }));
@@ -10226,8 +10238,13 @@ var STORAGE = {
10226
10238
  // 256 KB
10227
10239
  /** Max records returnable in one query page. */
10228
10240
  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)
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)
10231
10248
  };
10232
10249
  var COLLECTION_RE = /^[a-z0-9][a-z0-9:_-]{0,63}$/;
10233
10250
  var collectionSchemaStore = external_exports.string().regex(COLLECTION_RE, "invalid collection name");
@@ -10337,13 +10354,31 @@ var commentSchema = external_exports.object({
10337
10354
  parentId: external_exports.string().uuid().optional()
10338
10355
  });
10339
10356
  var eventSchema = external_exports.object({
10340
- 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"]),
10341
10361
  ms: external_exports.number().int().nonnegative().optional()
10342
10362
  });
10343
10363
  var createBuildSchema = external_exports.object({
10344
10364
  mode: external_exports.enum(["ai", "remix"]),
10345
10365
  remixParentId: external_exports.string().uuid().optional()
10346
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
+ });
10347
10382
  var builderAttachmentSchema = external_exports.object({
10348
10383
  kind: external_exports.enum(["image", "file", "audio"]),
10349
10384
  name: external_exports.string().max(200),
@@ -10444,27 +10479,52 @@ var notifPrefsPatchSchema = external_exports.object({
10444
10479
  groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10445
10480
  });
10446
10481
 
10447
- // ../shared/src/models.ts
10448
- var BUILDER_MODELS = [
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 },
10458
- { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", tier: "free", note: "Strong coder \xB7 great value" },
10459
- { id: "qwen3-coder-480b-a35b-instruct", label: "Qwen3 Coder 480B", tier: "free", note: "Fast, capable coder" },
10460
- // ── Subscription (locked until billing is enabled) — premium ──────
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 },
10463
- { id: "gpt-5.2-pro", label: "GPT-5.2 Pro", tier: "subscription", note: "Premium" },
10464
- { 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
10465
10495
  ];
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));
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
+ ]);
10468
10528
 
10469
10529
  // ../shared/src/i18n.ts
10470
10530
  var en = {
@@ -10498,6 +10558,21 @@ var en = {
10498
10558
  "share.title": "Share",
10499
10559
  "share.copyLink": "Copy link",
10500
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.`,
10501
10576
  "create.title": "Create",
10502
10577
  "create.buildWithAI": "Build with AI",
10503
10578
  "create.import": "Import",
@@ -10689,6 +10764,15 @@ function validate(html, opts) {
10689
10764
  errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
10690
10765
  return { ok: false, errors, warnings };
10691
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
+ }
10692
10776
  if (on("frames") && root.querySelector("iframe, frame, object, embed")) {
10693
10777
  errors.push({
10694
10778
  code: "FRAME_TAG",
@@ -11277,19 +11361,35 @@ function normalizeCategory(input) {
11277
11361
  const match = CATEGORIES.find((c) => c.toLowerCase() === input.toLowerCase());
11278
11362
  return match ?? "Other";
11279
11363
  }
11280
- async function publishArtifact(client, html, meta, projectDir) {
11281
- 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) {
11282
11369
  const exp = await apiFetch(client, "POST", "/experiences", {
11283
- title: meta.title?.trim() || inferTitle(html, projectDir),
11370
+ title: meta.title?.trim() || (html ? inferTitle(html, projectDir ?? ".") : "Untitled"),
11284
11371
  caption: meta.caption ?? "",
11285
11372
  category: normalizeCategory(meta.category),
11286
11373
  tags: meta.tags ?? [],
11287
11374
  remixable: meta.remixable ?? true,
11288
11375
  visibility: meta.visibility ?? "public",
11289
- draftArtifactKey: imported.draftArtifactKey
11376
+ draftArtifactKey
11290
11377
  });
11291
11378
  return { id: exp.id, status: exp.status, url: `${client.apiBase}/e/${exp.id}` };
11292
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
+ }
11293
11393
 
11294
11394
  // src/index.ts
11295
11395
  function parseArgs(argv) {
@@ -11324,7 +11424,10 @@ Usage:
11324
11424
  vibed login Authenticate this machine (opens the browser)
11325
11425
  vibed check [path] Check if a project can be published (no network)
11326
11426
  vibed preview [path] Bundle + open the artifact locally (no publish)
11427
+ vibed draft [path] Upload a private, hosted preview link (no publish)
11327
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
11328
11431
  vibed whoami Show the signed-in account
11329
11432
  vibed logout Remove the saved token
11330
11433
 
@@ -11388,7 +11491,30 @@ Preview \u2192 ${file2}`);
11388
11491
  }
11389
11492
  return result.ok ? 0 : 1;
11390
11493
  }
11391
- async function cmdPublish(args) {
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) {
11392
11518
  const json = isTrue(args.flags.json);
11393
11519
  const dir2 = resolve2(args._[1] ?? ".");
11394
11520
  const cfg = await loadConfig({ api: str(args.flags.api) });
@@ -11403,37 +11529,94 @@ async function cmdPublish(args) {
11403
11529
  policy,
11404
11530
  onLog: (m) => !json && console.error(m)
11405
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
+ });
11406
11579
  if (!result.ok || !result.html) {
11407
11580
  out(json, formatReport(result), { ...result, html: void 0 });
11408
11581
  console.error("\nFix the blockers above, then run `vibed publish` again.");
11409
11582
  return 1;
11410
11583
  }
11411
11584
  if (!json) console.log(formatReport(result));
11412
- const meta = {
11413
- title: str(args.flags.title),
11414
- caption: str(args.flags.caption),
11415
- category: str(args.flags.category),
11416
- tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11417
- remixable: !isTrue(args.flags["no-remix"]),
11418
- visibility: isTrue(args.flags.unlisted) ? "unlisted" : "public"
11419
- };
11420
11585
  try {
11421
- const res = await publishArtifact({ apiBase: cfg.apiBase, token: cfg.token }, result.html, meta, dir2);
11586
+ const res = await publishArtifact(client, result.html, meta, dir2);
11422
11587
  out(json, `
11423
11588
  \u2713 Published (${res.status}): ${res.url}`, res);
11424
11589
  return 0;
11425
11590
  } catch (e) {
11426
- if (e instanceof ApiError && e.code === "unauthorized") {
11427
- 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.");
11428
11613
  return 1;
11429
11614
  }
11430
- console.error(`Publish failed: ${e instanceof Error ? e.message : String(e)}`);
11431
- if (e instanceof ApiError && e.details) {
11432
- const d = e.details;
11433
- for (const err of d.errors ?? []) console.error(` \u2717 ${err.code}: ${err.message}`);
11434
- 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;
11435
11618
  }
11436
- return 1;
11619
+ return reportPublishError(e);
11437
11620
  }
11438
11621
  }
11439
11622
  async function cmdLogin(args) {
@@ -11489,8 +11672,13 @@ async function main() {
11489
11672
  return cmdCheck(args);
11490
11673
  case "preview":
11491
11674
  return cmdPreview(args);
11675
+ case "draft":
11676
+ return cmdDraft(args);
11492
11677
  case "publish":
11493
11678
  return cmdPublish(args);
11679
+ case "delete":
11680
+ case "rm":
11681
+ return cmdDelete(args);
11494
11682
  case void 0:
11495
11683
  case "help":
11496
11684
  case "--help":
package/dist/vibed.cjs CHANGED
@@ -1249,10 +1249,10 @@ var require_decode = __commonJS({
1249
1249
  this.state = EntityDecoderState.NumericDecimal;
1250
1250
  return this.stateNumericDecimal(str2, offset);
1251
1251
  };
1252
- EntityDecoder2.prototype.addToNumericResult = function(str2, start, end, base) {
1252
+ EntityDecoder2.prototype.addToNumericResult = function(str2, start, end, base2) {
1253
1253
  if (start !== end) {
1254
1254
  var digitCount = end - start;
1255
- this.result = this.result * Math.pow(base, digitCount) + parseInt(str2.substr(start, digitCount), base);
1255
+ this.result = this.result * Math.pow(base2, digitCount) + parseInt(str2.substr(start, digitCount), base2);
1256
1256
  this.consumed += digitCount;
1257
1257
  }
1258
1258
  };
@@ -6074,7 +6074,19 @@ var config = {
6074
6074
  // Light model that auto-tags the experience category. Falls back to a keyword
6075
6075
  // heuristic if the call fails.
6076
6076
  BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
6077
- BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 8192),
6077
+ // Speech-to-text model for the builder's voice input (docs/builder-architecture.md).
6078
+ // Gemini 2.5 Flash transcribes the recorded clip via GapGPT's OpenAI-compatible
6079
+ // chat API (an `input_audio` content part) — verified to accept the exact
6080
+ // formats browsers produce (Chrome webm/opus, Safari mp4). It's multilingual,
6081
+ // so Persian and English speech both work with no per-language config.
6082
+ BUILDER_TRANSCRIBE_MODEL: strEnv("BUILDER_TRANSCRIBE_MODEL", "gemini-2.5-flash"),
6083
+ // Upload cap for a voice clip. A minute of webm/opus is ~250KB; 8MB is ample
6084
+ // headroom while still bounding what one request can hand the model.
6085
+ BUILDER_TRANSCRIBE_MAX_BYTES: intEnv("BUILDER_TRANSCRIBE_MAX_BYTES", 8388608),
6086
+ // Output budget per model call. 8k proved too small in prod — a rich game is
6087
+ // ~7-8k tokens of file alone, and a truncated file used to slip through as a
6088
+ // text dump. 24576 was probed OK against every catalog model on GapGPT.
6089
+ BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 24576),
6078
6090
  BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
6079
6091
  // Hard cap for a single build turn (across the model + one repair). The build
6080
6092
  // runs detached from the request, so this — not the browser — is what stops a
@@ -9816,23 +9828,23 @@ var ZodEffects = class extends ZodType {
9816
9828
  }
9817
9829
  if (effect.type === "transform") {
9818
9830
  if (ctx.common.async === false) {
9819
- const base = this._def.schema._parseSync({
9831
+ const base2 = this._def.schema._parseSync({
9820
9832
  data: ctx.data,
9821
9833
  path: ctx.path,
9822
9834
  parent: ctx
9823
9835
  });
9824
- if (!isValid(base))
9836
+ if (!isValid(base2))
9825
9837
  return INVALID;
9826
- const result = effect.transform(base.value, checkCtx);
9838
+ const result = effect.transform(base2.value, checkCtx);
9827
9839
  if (result instanceof Promise) {
9828
9840
  throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
9829
9841
  }
9830
9842
  return { status: status.value, value: result };
9831
9843
  } else {
9832
- return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
9833
- if (!isValid(base))
9844
+ return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base2) => {
9845
+ if (!isValid(base2))
9834
9846
  return INVALID;
9835
- return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
9847
+ return Promise.resolve(effect.transform(base2.value, checkCtx)).then((result) => ({
9836
9848
  status: status.value,
9837
9849
  value: result
9838
9850
  }));
@@ -10227,8 +10239,13 @@ var STORAGE = {
10227
10239
  // 256 KB
10228
10240
  /** Max records returnable in one query page. */
10229
10241
  QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10230
- /** Whether anonymous (logged-out) shared writes may be enabled per post. */
10231
- ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", false)
10242
+ /**
10243
+ * Platform kill-switch for anonymous shared writes. ON by default: the real
10244
+ * gate is per-post owner opt-in (`anon_writes` + writePolicy `anyone`), so a
10245
+ * post only accepts guest writes when its owner turns them on. Set the env to
10246
+ * `false` to disable anon writes platform-wide regardless of per-post config.
10247
+ */
10248
+ ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", true)
10232
10249
  };
10233
10250
  var COLLECTION_RE = /^[a-z0-9][a-z0-9:_-]{0,63}$/;
10234
10251
  var collectionSchemaStore = external_exports.string().regex(COLLECTION_RE, "invalid collection name");
@@ -10338,13 +10355,31 @@ var commentSchema = external_exports.object({
10338
10355
  parentId: external_exports.string().uuid().optional()
10339
10356
  });
10340
10357
  var eventSchema = external_exports.object({
10341
- kind: external_exports.enum(["impression", "play", "play_time", "share"]),
10358
+ // "record"/"screenshot" fire when a viewer captures the vibe (see the in-iframe
10359
+ // bridge capture flow, docs/architecture.md §5.4). Plain-text `viewEvents.kind`
10360
+ // column, so new kinds need no migration.
10361
+ kind: external_exports.enum(["impression", "play", "play_time", "share", "record", "screenshot"]),
10342
10362
  ms: external_exports.number().int().nonnegative().optional()
10343
10363
  });
10344
10364
  var createBuildSchema = external_exports.object({
10345
10365
  mode: external_exports.enum(["ai", "remix"]),
10346
10366
  remixParentId: external_exports.string().uuid().optional()
10347
10367
  });
10368
+ var turnAttachmentSchema = external_exports.object({
10369
+ kind: external_exports.enum(["image", "audio", "file", "sketch"]),
10370
+ key: external_exports.string().max(300).default(""),
10371
+ name: external_exports.string().max(200),
10372
+ // NUL bytes break Postgres jsonb (and mean binary-not-text) — strip defensively.
10373
+ text: external_exports.string().max(5e4).transform((t) => t.replace(/\u0000/g, "")).optional()
10374
+ });
10375
+ var createTurnRequestSchema = external_exports.object({
10376
+ clientTurnId: external_exports.string().uuid(),
10377
+ content: external_exports.string().max(4e3).default(""),
10378
+ attachments: external_exports.array(turnAttachmentSchema).max(6).optional(),
10379
+ model: external_exports.string().max(100).optional()
10380
+ }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0, {
10381
+ message: "Send a message or an attachment."
10382
+ });
10348
10383
  var builderAttachmentSchema = external_exports.object({
10349
10384
  kind: external_exports.enum(["image", "file", "audio"]),
10350
10385
  name: external_exports.string().max(200),
@@ -10445,27 +10480,52 @@ var notifPrefsPatchSchema = external_exports.object({
10445
10480
  groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10446
10481
  });
10447
10482
 
10448
- // ../shared/src/models.ts
10449
- var BUILDER_MODELS = [
10450
- // ── Free (selectable now) — ONLY genuinely strong builders ─────────
10451
- // Curated 2026-07: every id verified against GapGPT with a real full-HTML
10452
- // build (not a ping). Weak/cheap models were removed on purpose — a bad
10453
- // first build costs more in user trust than the tokens save.
10454
- { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", tier: "free", note: "Best for polished vibes \xB7 fast", vision: true },
10455
- { id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", tier: "free", note: "Reliable all-rounder", vision: true },
10456
- { id: "gpt-5.2", label: "GPT-5.2", tier: "free", note: "Strong generalist", vision: true },
10457
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex", tier: "free", note: "Code specialist" },
10458
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", tier: "free", note: "Great UI sense", vision: true },
10459
- { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", tier: "free", note: "Strong coder \xB7 great value" },
10460
- { id: "qwen3-coder-480b-a35b-instruct", label: "Qwen3 Coder 480B", tier: "free", note: "Fast, capable coder" },
10461
- // ── Subscription (locked until billing is enabled) — premium ──────
10462
- // Opus 4.8 is owner-only early access: locked for everyone, live for the owner.
10463
- { id: "claude-opus-4-8", label: "Claude Opus 4.8", tier: "subscription", note: "Top quality", vision: true, ownerOnly: true },
10464
- { id: "gpt-5.2-pro", label: "GPT-5.2 Pro", tier: "subscription", note: "Premium" },
10465
- { id: "gemini-3-pro-preview", label: "Gemini 3 Pro", tier: "subscription", note: "Premium", vision: true }
10483
+ // ../shared/src/builderEvents.ts
10484
+ var BUILD_EVENT_VERSION = 1;
10485
+ var BUILD_PHASES = [
10486
+ "queued",
10487
+ // accepted, waiting for a worker slot
10488
+ "thinking",
10489
+ // model connected, no visible output yet (keep-alives flowing)
10490
+ "writing",
10491
+ // streaming the vibe's code/prose
10492
+ "validating",
10493
+ // sandbox pass on the produced artifact
10494
+ "repairing"
10495
+ // one automatic fix round after a validation failure
10466
10496
  ];
10467
- var FREE_IDS = new Set(BUILDER_MODELS.filter((m) => m.tier === "free").map((m) => m.id));
10468
- var OWNER_MODEL_IDS = new Set(BUILDER_MODELS.filter((m) => m.ownerOnly).map((m) => m.id));
10497
+ var base = { v: external_exports.literal(BUILD_EVENT_VERSION), turnId: external_exports.string() };
10498
+ var buildEventSchema = external_exports.discriminatedUnion("type", [
10499
+ /** Assistant prose delta (never contains the HTML code block). */
10500
+ external_exports.object({ ...base, type: external_exports.literal("token"), text: external_exports.string() }),
10501
+ /**
10502
+ * Honest heartbeat, ~every 2s while the turn runs: real elapsed ms, real
10503
+ * output-token count, and the executor's actual phase. The UI renders this
10504
+ * as a live counter — a number that moves because work is happening.
10505
+ */
10506
+ external_exports.object({
10507
+ ...base,
10508
+ type: external_exports.literal("progress"),
10509
+ phase: external_exports.enum(BUILD_PHASES),
10510
+ elapsedMs: external_exports.number(),
10511
+ tokensOut: external_exports.number()
10512
+ }),
10513
+ /** Friendly status chip ("index.html", "Fixing a couple of things…"). */
10514
+ external_exports.object({ ...base, type: external_exports.literal("status"), summary: external_exports.string() }),
10515
+ /** First-message category classification. */
10516
+ external_exports.object({ ...base, type: external_exports.literal("category"), category: external_exports.string() }),
10517
+ /** The validated draft landed. Preview loads it from the draft URL. */
10518
+ external_exports.object({ ...base, type: external_exports.literal("artifact"), artifactKey: external_exports.string() }),
10519
+ /** Terminal: failure with taxonomy code + human message. */
10520
+ external_exports.object({
10521
+ ...base,
10522
+ type: external_exports.literal("error"),
10523
+ code: external_exports.string(),
10524
+ message: external_exports.string()
10525
+ }),
10526
+ /** Terminal: success. */
10527
+ external_exports.object({ ...base, type: external_exports.literal("done") })
10528
+ ]);
10469
10529
 
10470
10530
  // ../shared/src/i18n.ts
10471
10531
  var en = {
@@ -10499,6 +10559,21 @@ var en = {
10499
10559
  "share.title": "Share",
10500
10560
  "share.copyLink": "Copy link",
10501
10561
  "share.copied": "Link copied",
10562
+ "capture.record": "Record",
10563
+ "capture.screenshot": "Screenshot",
10564
+ "capture.recording": "Recording",
10565
+ "capture.stop": "Stop",
10566
+ "capture.preparing": "Preparing\u2026",
10567
+ "capture.processing": "Finishing\u2026",
10568
+ "capture.preview.videoTitle": "Your clip",
10569
+ "capture.preview.imageTitle": "Your screenshot",
10570
+ "capture.save": "Save",
10571
+ "capture.shareVia": "Share via\u2026",
10572
+ "capture.retry": "Try again",
10573
+ "capture.error": "Capture failed \u2014 try again.",
10574
+ "capture.allowShare": "Allow \u201CShare this tab\u201D to record \u2014 it captures just the vibe.",
10575
+ "capture.pickTab": "Choose \u201CThis Tab\u201D so it records only the vibe.",
10576
+ "capture.unsupported": `This ${ITEM.one} can't be captured yet. Try again in a moment.`,
10502
10577
  "create.title": "Create",
10503
10578
  "create.buildWithAI": "Build with AI",
10504
10579
  "create.import": "Import",
@@ -10690,6 +10765,15 @@ function validate(html, opts) {
10690
10765
  errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
10691
10766
  return { ok: false, errors, warnings };
10692
10767
  }
10768
+ const scriptOpens = (html.match(/<script\b/gi) ?? []).length;
10769
+ const scriptCloses = (html.match(/<\/script\s*>/gi) ?? []).length;
10770
+ if (scriptOpens > scriptCloses) {
10771
+ errors.push({
10772
+ code: "TRUNCATED_DOC",
10773
+ message: "The file ends mid-<script> \u2014 it looks cut off. Regenerate or paste the complete file."
10774
+ });
10775
+ return { ok: false, errors, warnings };
10776
+ }
10693
10777
  if (on("frames") && root.querySelector("iframe, frame, object, embed")) {
10694
10778
  errors.push({
10695
10779
  code: "FRAME_TAG",
@@ -11278,19 +11362,35 @@ function normalizeCategory(input) {
11278
11362
  const match = CATEGORIES.find((c) => c.toLowerCase() === input.toLowerCase());
11279
11363
  return match ?? "Other";
11280
11364
  }
11281
- async function publishArtifact(client, html, meta, projectDir) {
11282
- const imported = await apiFetch(client, "POST", "/imports", { html });
11365
+ async function importDraft(client, html) {
11366
+ const r = await apiFetch(client, "POST", "/imports", { html });
11367
+ return { draftArtifactKey: r.draftArtifactKey, previewUrl: r.previewUrl };
11368
+ }
11369
+ async function publishDraft(client, draftArtifactKey, meta, html, projectDir) {
11283
11370
  const exp = await apiFetch(client, "POST", "/experiences", {
11284
- title: meta.title?.trim() || inferTitle(html, projectDir),
11371
+ title: meta.title?.trim() || (html ? inferTitle(html, projectDir ?? ".") : "Untitled"),
11285
11372
  caption: meta.caption ?? "",
11286
11373
  category: normalizeCategory(meta.category),
11287
11374
  tags: meta.tags ?? [],
11288
11375
  remixable: meta.remixable ?? true,
11289
11376
  visibility: meta.visibility ?? "public",
11290
- draftArtifactKey: imported.draftArtifactKey
11377
+ draftArtifactKey
11291
11378
  });
11292
11379
  return { id: exp.id, status: exp.status, url: `${client.apiBase}/e/${exp.id}` };
11293
11380
  }
11381
+ async function publishArtifact(client, html, meta, projectDir) {
11382
+ const { draftArtifactKey } = await importDraft(client, html);
11383
+ return publishDraft(client, draftArtifactKey, meta, html, projectDir);
11384
+ }
11385
+ function experienceIdFrom(input) {
11386
+ const m = input.match(/\/e\/([0-9a-f-]{36})/i);
11387
+ return (m?.[1] ?? input.trim()).toLowerCase();
11388
+ }
11389
+ async function deleteExperience(client, idOrUrl) {
11390
+ const id = experienceIdFrom(idOrUrl);
11391
+ await apiFetch(client, "DELETE", `/experiences/${id}`);
11392
+ return id;
11393
+ }
11294
11394
 
11295
11395
  // src/index.ts
11296
11396
  function parseArgs(argv) {
@@ -11325,7 +11425,10 @@ Usage:
11325
11425
  vibed login Authenticate this machine (opens the browser)
11326
11426
  vibed check [path] Check if a project can be published (no network)
11327
11427
  vibed preview [path] Bundle + open the artifact locally (no publish)
11428
+ vibed draft [path] Upload a private, hosted preview link (no publish)
11328
11429
  vibed publish [path] Bundle the project and publish it to vibed
11430
+ vibed publish --draft-key <k> Promote a draft preview to a public post
11431
+ vibed delete <id|url> Delete one of your posts
11329
11432
  vibed whoami Show the signed-in account
11330
11433
  vibed logout Remove the saved token
11331
11434
 
@@ -11389,7 +11492,30 @@ Preview \u2192 ${file2}`);
11389
11492
  }
11390
11493
  return result.ok ? 0 : 1;
11391
11494
  }
11392
- async function cmdPublish(args) {
11495
+ function metaFromArgs(args) {
11496
+ return {
11497
+ title: str(args.flags.title),
11498
+ caption: str(args.flags.caption),
11499
+ category: str(args.flags.category),
11500
+ tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11501
+ remixable: !isTrue(args.flags["no-remix"]),
11502
+ visibility: isTrue(args.flags.unlisted) ? "unlisted" : "public"
11503
+ };
11504
+ }
11505
+ function reportPublishError(e) {
11506
+ if (e instanceof ApiError && e.code === "unauthorized") {
11507
+ console.error("Your token is no longer valid. Run `vibed login` again.");
11508
+ return 1;
11509
+ }
11510
+ console.error(`Failed: ${e instanceof Error ? e.message : String(e)}`);
11511
+ if (e instanceof ApiError && e.details) {
11512
+ const d = e.details;
11513
+ for (const err of d.errors ?? []) console.error(` \u2717 ${err.code}: ${err.message}`);
11514
+ if (!d.errors) console.error(` ${JSON.stringify(e.details)}`);
11515
+ }
11516
+ return 1;
11517
+ }
11518
+ async function cmdDraft(args) {
11393
11519
  const json = isTrue(args.flags.json);
11394
11520
  const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
11395
11521
  const cfg = await loadConfig({ api: str(args.flags.api) });
@@ -11404,37 +11530,94 @@ async function cmdPublish(args) {
11404
11530
  policy,
11405
11531
  onLog: (m) => !json && console.error(m)
11406
11532
  });
11533
+ if (!result.ok || !result.html) {
11534
+ out(json, formatReport(result), { ...result, html: void 0 });
11535
+ return 1;
11536
+ }
11537
+ if (!json) console.log(formatReport(result));
11538
+ try {
11539
+ const d = await importDraft({ apiBase: cfg.apiBase, token: cfg.token }, result.html);
11540
+ out(
11541
+ json,
11542
+ `
11543
+ \u2713 Preview (not published): ${d.previewUrl ?? "(no preview URL)"}
11544
+ Publish it with: vibed publish --draft-key ${d.draftArtifactKey} --title "\u2026" --category <Cat>`,
11545
+ { ok: true, ...d }
11546
+ );
11547
+ return 0;
11548
+ } catch (e) {
11549
+ return reportPublishError(e);
11550
+ }
11551
+ }
11552
+ async function cmdPublish(args) {
11553
+ const json = isTrue(args.flags.json);
11554
+ const cfg = await loadConfig({ api: str(args.flags.api) });
11555
+ if (!cfg.token) {
11556
+ console.error("Not signed in. Run `vibed login` first.");
11557
+ return 1;
11558
+ }
11559
+ const client = { apiBase: cfg.apiBase, token: cfg.token };
11560
+ const meta = metaFromArgs(args);
11561
+ const draftKey = str(args.flags["draft-key"]);
11562
+ if (draftKey) {
11563
+ try {
11564
+ const res = await publishDraft(client, draftKey, meta);
11565
+ out(json, `
11566
+ \u2713 Published (${res.status}): ${res.url}`, res);
11567
+ return 0;
11568
+ } catch (e) {
11569
+ return reportPublishError(e);
11570
+ }
11571
+ }
11572
+ const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
11573
+ const policy = await fetchPolicy(cfg.apiBase);
11574
+ const result = await bundleProject(dir2, {
11575
+ entry: str(args.flags.entry),
11576
+ build: !isTrue(args.flags["no-build"]),
11577
+ policy,
11578
+ onLog: (m) => !json && console.error(m)
11579
+ });
11407
11580
  if (!result.ok || !result.html) {
11408
11581
  out(json, formatReport(result), { ...result, html: void 0 });
11409
11582
  console.error("\nFix the blockers above, then run `vibed publish` again.");
11410
11583
  return 1;
11411
11584
  }
11412
11585
  if (!json) console.log(formatReport(result));
11413
- const meta = {
11414
- title: str(args.flags.title),
11415
- caption: str(args.flags.caption),
11416
- category: str(args.flags.category),
11417
- tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11418
- remixable: !isTrue(args.flags["no-remix"]),
11419
- visibility: isTrue(args.flags.unlisted) ? "unlisted" : "public"
11420
- };
11421
11586
  try {
11422
- const res = await publishArtifact({ apiBase: cfg.apiBase, token: cfg.token }, result.html, meta, dir2);
11587
+ const res = await publishArtifact(client, result.html, meta, dir2);
11423
11588
  out(json, `
11424
11589
  \u2713 Published (${res.status}): ${res.url}`, res);
11425
11590
  return 0;
11426
11591
  } catch (e) {
11427
- if (e instanceof ApiError && e.code === "unauthorized") {
11428
- console.error("Your token is no longer valid. Run `vibed login` again.");
11592
+ return reportPublishError(e);
11593
+ }
11594
+ }
11595
+ async function cmdDelete(args) {
11596
+ const json = isTrue(args.flags.json);
11597
+ const target = args._[1];
11598
+ if (!target) {
11599
+ console.error("Usage: vibed delete <experience-id | https://\u2026/e/<id>>");
11600
+ return 1;
11601
+ }
11602
+ const cfg = await loadConfig({ api: str(args.flags.api) });
11603
+ if (!cfg.token) {
11604
+ console.error("Not signed in. Run `vibed login` first.");
11605
+ return 1;
11606
+ }
11607
+ try {
11608
+ const id = await deleteExperience({ apiBase: cfg.apiBase, token: cfg.token }, target);
11609
+ out(json, `\u2713 Deleted post ${id}.`, { ok: true, id });
11610
+ return 0;
11611
+ } catch (e) {
11612
+ if (e instanceof ApiError && (e.code === "forbidden" || e.status === 403)) {
11613
+ console.error("That post isn't yours to delete.");
11429
11614
  return 1;
11430
11615
  }
11431
- console.error(`Publish failed: ${e instanceof Error ? e.message : String(e)}`);
11432
- if (e instanceof ApiError && e.details) {
11433
- const d = e.details;
11434
- for (const err of d.errors ?? []) console.error(` \u2717 ${err.code}: ${err.message}`);
11435
- if (!d.errors) console.error(` ${JSON.stringify(e.details)}`);
11616
+ if (e instanceof ApiError && (e.code === "not_found" || e.status === 404)) {
11617
+ console.error("No such post (or already deleted).");
11618
+ return 1;
11436
11619
  }
11437
- return 1;
11620
+ return reportPublishError(e);
11438
11621
  }
11439
11622
  }
11440
11623
  async function cmdLogin(args) {
@@ -11490,8 +11673,13 @@ async function main() {
11490
11673
  return cmdCheck(args);
11491
11674
  case "preview":
11492
11675
  return cmdPreview(args);
11676
+ case "draft":
11677
+ return cmdDraft(args);
11493
11678
  case "publish":
11494
11679
  return cmdPublish(args);
11680
+ case "delete":
11681
+ case "rm":
11682
+ return cmdDelete(args);
11495
11683
  case void 0:
11496
11684
  case "help":
11497
11685
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amirhosseinnateghi/vibed-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "make it vibed — check, bundle, and publish a project to vibed from the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",