@amirhosseinnateghi/vibed-cli 0.1.8 → 0.2.0

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 +346 -15
  2. package/dist/vibed.cjs +351 -20
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -5973,8 +5973,8 @@ var require_dist = __commonJS({
5973
5973
  });
5974
5974
 
5975
5975
  // src/index.ts
5976
- import { resolve as resolve2, join as join3 } from "node:path";
5977
- import { writeFile as writeFile2 } from "node:fs/promises";
5976
+ import { resolve as resolve2, join as join4 } from "node:path";
5977
+ import { writeFile as writeFile3 } from "node:fs/promises";
5978
5978
  import { tmpdir } from "node:os";
5979
5979
  import { pathToFileURL } from "node:url";
5980
5980
 
@@ -6121,6 +6121,9 @@ var config = {
6121
6121
  // completion the server drops a notification (§7.1) — so a slow build never
6122
6122
  // traps them staring at a spinner.
6123
6123
  BUILDER_SLOW_MS: intEnv("BUILDER_SLOW_MS", 12e3),
6124
+ /** Lite edits (ف۷) kill switch: set to "off" to force every edit turn back to
6125
+ * whole-file regeneration without deploying code. */
6126
+ BUILDER_LITE_EDITS: strEnv("BUILDER_LITE_EDITS", "on"),
6124
6127
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6125
6128
  };
6126
6129
  function strEnv(key, fallback) {
@@ -10260,10 +10263,11 @@ var STORAGE = {
10260
10263
  /** Max records returnable in one query page. */
10261
10264
  QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10262
10265
  /**
10263
- * Platform kill-switch for anonymous shared writes. ON by default: the real
10264
- * gate is per-post owner opt-in (`anon_writes` + writePolicy `anyone`), so a
10265
- * post only accepts guest writes when its owner turns them on. Set the env to
10266
- * `false` to disable anon writes platform-wide regardless of per-post config.
10266
+ * Platform kill-switch for anonymous shared writes. ON by default. The real
10267
+ * gate is the per-post config (`anon_writes` + writePolicy `anyone`), which is
10268
+ * guest-writable BY DEFAULT (a shared vibe should work for whoever opens the
10269
+ * link) an owner can lock it down per post. Set the env to `false` to disable
10270
+ * anon writes platform-wide regardless of per-post config.
10267
10271
  */
10268
10272
  ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", true)
10269
10273
  };
@@ -10377,9 +10381,39 @@ var patchExperienceSchema = external_exports.object({
10377
10381
  visibility: visibilitySchema.optional(),
10378
10382
  coverMode: coverModeSchema.optional()
10379
10383
  });
10384
+ var COMMENT_MEDIA = {
10385
+ quality: 0.82,
10386
+ maxEdge: 1600,
10387
+ maxBytes: 8 * 1024 * 1024,
10388
+ // video/* is the compressed comment clip (client-side MediaRecorder → mp4 on
10389
+ // iOS, webm on Chromium); the client never sends a raw camera video as-is.
10390
+ types: ["image/webp", "image/jpeg", "image/png", "image/gif", "video/mp4", "video/webm"]
10391
+ };
10392
+ var COMMENT_VIDEO = {
10393
+ maxDurationSec: 60,
10394
+ maxEdge: 720,
10395
+ // 1 Mbps → a 60s clip is ~7.5MB, comfortably under the 10MB request-body cap.
10396
+ bitsPerSecond: 1e6,
10397
+ crf: 30,
10398
+ maxUploadBytes: 120 * 1024 * 1024
10399
+ };
10400
+ var commentMediaSchema = external_exports.object({
10401
+ key: external_exports.string().min(1).max(300),
10402
+ type: external_exports.enum(COMMENT_MEDIA.types),
10403
+ w: external_exports.number().int().positive().max(2e4),
10404
+ h: external_exports.number().int().positive().max(2e4)
10405
+ });
10406
+ var commentVideoSchema = external_exports.object({ key: external_exports.string().min(1).max(300) });
10380
10407
  var commentSchema = external_exports.object({
10381
- body: external_exports.string().min(1).max(500),
10382
- parentId: external_exports.string().uuid().optional()
10408
+ body: external_exports.string().max(500),
10409
+ parentId: external_exports.string().uuid().optional(),
10410
+ media: commentMediaSchema.optional(),
10411
+ // ready image/GIF (compressed client-side)
10412
+ video: commentVideoSchema.optional()
10413
+ // raw video → transcoded async in the worker
10414
+ }).refine((d) => d.body.trim().length > 0 || !!d.media || !!d.video, {
10415
+ message: "Write something or attach a photo or video.",
10416
+ path: ["body"]
10383
10417
  });
10384
10418
  var eventSchema = external_exports.object({
10385
10419
  // "record"/"screenshot" fire when a viewer captures the vibe (see the in-iframe
@@ -10410,6 +10444,9 @@ var createTurnRequestSchema = external_exports.object({
10410
10444
  // A voice turn carries no text yet — the worker transcribes `voice` first (م۷).
10411
10445
  voice: voiceInputSchema.optional(),
10412
10446
  model: external_exports.string().max(100).optional()
10447
+ // No editMode: lite edits (ف۷) proved themselves, so every edit takes that
10448
+ // path and the server decides — a client can't ask for the slow rewrite.
10449
+ // BUILDER_LITE_EDITS=off remains the fleet-wide kill switch.
10413
10450
  }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0 || !!v.voice, {
10414
10451
  message: "Send a message, an attachment, or a voice recording."
10415
10452
  });
@@ -10514,6 +10551,183 @@ var notifPrefsPatchSchema = external_exports.object({
10514
10551
  groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10515
10552
  });
10516
10553
 
10554
+ // ../shared/src/remix.ts
10555
+ var REMIX_GUIDE = `# Make it remix-friendly
10556
+
10557
+ On vibed every published vibe is **remixable**: anyone can fork it and make it
10558
+ theirs, and the AI builder can be asked to change it. A vibe is only *really*
10559
+ remixable if a stranger \u2014 or a model editing it \u2014 can find the knobs without
10560
+ reading the whole file. Treat this as part of shipping, like mobile-first.
10561
+
10562
+ It pays off twice: edits to a well-structured file are applied as small precise
10563
+ patches instead of regenerating the whole thing, which is faster, cheaper, and
10564
+ far less likely to break what already worked.
10565
+
10566
+ ## 1. One CONFIG block, at the top
10567
+
10568
+ Every tunable lives in a single object at the top of the script \u2014 the first
10569
+ thing anyone sees:
10570
+
10571
+ \`\`\`js
10572
+ const CONFIG = {
10573
+ // --- Feel ---
10574
+ gravity: 0.6,
10575
+ jumpForce: -11,
10576
+ pipeGap: 160,
10577
+ pipeSpeed: 2.4,
10578
+ // --- Look ---
10579
+ skyColor: "#7ec8ff",
10580
+ birdColor: "#ffd166",
10581
+ // --- Words (all user-facing text) ---
10582
+ title: "Tap to Fly",
10583
+ gameOverText: "Ouch! Tap to try again",
10584
+ scoreLabel: "Score",
10585
+ };
10586
+ \`\`\`
10587
+
10588
+ Rules that make it useful:
10589
+ - **Literal values only** \u2014 numbers, strings, colors. No computed expressions.
10590
+ - **Everything reads from CONFIG.** If a number appears twice in the logic, it
10591
+ belongs in CONFIG once.
10592
+ - **All user-facing text goes here too**, so rewording or translating a vibe is
10593
+ one edit, not a hunt.
10594
+
10595
+ ## 2. Content separate from logic
10596
+
10597
+ Questions, levels, items, cards, prompts \u2014 a labelled array right after CONFIG:
10598
+
10599
+ \`\`\`js
10600
+ const QUESTIONS = [
10601
+ { q: "Pick a color", options: ["Red", "Blue"], answer: 0 },
10602
+ // add more here \u2014 same shape
10603
+ ];
10604
+ \`\`\`
10605
+
10606
+ Now "add five more questions" is a data edit anyone can make.
10607
+
10608
+ ## 3. Predictable section order
10609
+
10610
+ Lay the script out in this order, each with a one-line banner comment:
10611
+
10612
+ \`\`\`js
10613
+ // ============ CONFIG ============
10614
+ // ============ CONTENT ===========
10615
+ // ============ STATE =============
10616
+ // ============ HELPERS ===========
10617
+ // ============ RENDER ============
10618
+ // ============ INPUT =============
10619
+ // ============ INIT ==============
10620
+ \`\`\`
10621
+
10622
+ A remixer (and a patching model) then knows where a change belongs before
10623
+ reading a single line.
10624
+
10625
+ ## 4. Write it plainly
10626
+
10627
+ - Descriptive names: \`spawnEnemy()\`, not \`se()\`; \`playerSpeed\`, not \`ps\`.
10628
+ - Small functions that do one thing. One \`init()\` entry point, one game loop.
10629
+ - Two-space indent, no minification, no bundler output, no clever one-liners or
10630
+ chained ternaries.
10631
+ - A short comment at each seam someone would want to change:
10632
+ \`// Win condition \u2014 swap this for a timer, a score target, ...\`
10633
+ - Stable \`id\`/\`class\` names on the elements a remixer would target.
10634
+ - No dead code, and no copy-pasted near-identical blocks \u2014 factor them into one
10635
+ function. (Duplicated blocks also make an automated edit ambiguous, so it
10636
+ fails or lands in the wrong place.)
10637
+
10638
+ ## 5. Quick self-check before publishing
10639
+
10640
+ - Could someone change the colors, speed, and all the words by editing only
10641
+ CONFIG?
10642
+ - Could they add one more level/question/item by editing only the content array?
10643
+ - Is there any magic number or user-facing string outside those two blocks?
10644
+ - Would a stranger know where to add a feature within ten seconds of scrolling?
10645
+
10646
+ If the answer to any of those is no, fix it before you publish \u2014 that is the
10647
+ difference between a vibe people fork and one they scroll past.`;
10648
+ var SMALL_VIBE_CHARS = 1200;
10649
+ var SMALL_VIBE_LINES = 40;
10650
+ var COMMENT_EXPECTED_LINES = 60;
10651
+ var SECTIONS_EXPECTED_LINES = 120;
10652
+ var MINIFIED_LINE_CHARS = 400;
10653
+ var MINIFIED_AVG_LINE_CHARS = 160;
10654
+ function authorScript(html) {
10655
+ const parts = [];
10656
+ const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
10657
+ let m;
10658
+ while (m = re.exec(html)) {
10659
+ const attrs = m[1] ?? "";
10660
+ if (/data-vibed-bridge/i.test(attrs)) continue;
10661
+ if (/\bsrc=/i.test(attrs)) continue;
10662
+ parts.push(m[2] ?? "");
10663
+ }
10664
+ return parts.join("\n");
10665
+ }
10666
+ function remixAudit(html) {
10667
+ const script = authorScript(html ?? "");
10668
+ const lines = script.split(/\r?\n/);
10669
+ const codeLines = lines.filter((l) => l.trim() !== "");
10670
+ const scriptLines = codeLines.length;
10671
+ const scriptChars = codeLines.reduce((n, l) => n + l.trim().length, 0);
10672
+ const avgLine = scriptLines > 0 ? scriptChars / scriptLines : 0;
10673
+ const findings = [];
10674
+ const longest = codeLines.reduce((n, l) => Math.max(n, l.trim().length), 0);
10675
+ const denselyPacked = scriptChars > 600 && avgLine > MINIFIED_AVG_LINE_CHARS;
10676
+ if (longest > MINIFIED_LINE_CHARS || denselyPacked) {
10677
+ findings.push({
10678
+ code: "MINIFIED",
10679
+ blocking: true,
10680
+ message: `The script averages ${Math.round(avgLine)} characters per line (longest ${longest}) \u2014 this looks minified or bundled.`,
10681
+ fix: "Publish readable source: turn off minification for the vibe build, or inline the unminified code."
10682
+ });
10683
+ }
10684
+ if (scriptLines >= SMALL_VIBE_LINES || scriptChars >= SMALL_VIBE_CHARS) {
10685
+ if (!/\b(?:const|let|var)\s+CONFIG\s*=/.test(script)) {
10686
+ findings.push({
10687
+ code: "NO_CONFIG",
10688
+ blocking: true,
10689
+ message: "No `const CONFIG = { ... }` block \u2014 a remixer has to hunt through the logic to change anything.",
10690
+ fix: "Move every tunable (speeds, sizes, colors, counts, durations) AND all user-facing text into one CONFIG object at the top, and read from it everywhere."
10691
+ });
10692
+ }
10693
+ const cryptic = /* @__PURE__ */ new Set();
10694
+ const decl = /\b(?:const|let|var)\s+([A-Za-z_$])\b(?!\s*[,)])/g;
10695
+ let d;
10696
+ while (d = decl.exec(script)) {
10697
+ const name = d[1];
10698
+ if (!"ijknxyt_".includes(name)) cryptic.add(name);
10699
+ }
10700
+ if (cryptic.size >= 5) {
10701
+ findings.push({
10702
+ code: "CRYPTIC_NAMES",
10703
+ blocking: true,
10704
+ message: `${cryptic.size} single-letter variables (${[...cryptic].slice(0, 6).join(", ")}) \u2014 the code reads as machine output.`,
10705
+ fix: "Give variables and functions descriptive names (`playerSpeed`, not `p`)."
10706
+ });
10707
+ }
10708
+ if (scriptLines >= COMMENT_EXPECTED_LINES) {
10709
+ const comments = codeLines.filter((l) => /^\s*(\/\/|\/\*|\*)/.test(l)).length;
10710
+ if (comments === 0) {
10711
+ findings.push({
10712
+ code: "NO_COMMENTS",
10713
+ blocking: false,
10714
+ message: "The script has no comments at all.",
10715
+ fix: "Add a short note at each seam a remixer would change (the win condition, the spawn rule, the scoring)."
10716
+ });
10717
+ }
10718
+ }
10719
+ if (scriptLines >= SECTIONS_EXPECTED_LINES && !/CONFIG\s*=+\s*$|=== *CONFIG|-- *CONFIG/im.test(script)) {
10720
+ findings.push({
10721
+ code: "NO_SECTIONS",
10722
+ blocking: false,
10723
+ message: "No section banners in a long script.",
10724
+ fix: "Split it with banner comments: CONFIG \u2192 CONTENT \u2192 STATE \u2192 helpers \u2192 render \u2192 input \u2192 init."
10725
+ });
10726
+ }
10727
+ }
10728
+ return { ok: !findings.some((f) => f.blocking), findings, scriptLines };
10729
+ }
10730
+
10517
10731
  // ../shared/src/builderEvents.ts
10518
10732
  var BUILD_EVENT_VERSION = 1;
10519
10733
  var BUILD_PHASES = [
@@ -11171,8 +11385,10 @@ async function bundleProject(projectDir, opts = {}) {
11171
11385
  allowedOrigins
11172
11386
  });
11173
11387
  const result = validate(html, { maxBytes, allowedOrigins, policy: opts.policy });
11388
+ const remix = remixAudit(html);
11174
11389
  return {
11175
- ok: result.ok,
11390
+ ok: result.ok && remix.ok,
11391
+ remix,
11176
11392
  html,
11177
11393
  policy: opts.policy,
11178
11394
  entry: resolved.entry,
@@ -11233,12 +11449,28 @@ function formatReport(result) {
11233
11449
  result.sizeBytes != null ? `size: ${fmtSize(result.sizeBytes)}` : null
11234
11450
  ].filter(Boolean).join(" \xB7 ");
11235
11451
  if (where) out2.push(where);
11452
+ const remixBlockers = (result.remix?.findings ?? []).filter((f) => f.blocking);
11236
11453
  if (result.ok) {
11237
11454
  out2.push("\u2713 Vibeable \u2014 this project can be published to vibed.");
11238
- } else {
11455
+ } else if (result.blockers.length > 0) {
11239
11456
  out2.push(`\u2717 Not vibeable yet \u2014 ${result.blockers.length} blocker(s):`);
11240
11457
  out2.push(...summarize(result).lines);
11241
11458
  }
11459
+ if (remixBlockers.length > 0) {
11460
+ out2.push("");
11461
+ out2.push(`\u2717 Not remixable yet \u2014 ${remixBlockers.length} thing(s) to fix before publishing:`);
11462
+ for (const f of remixBlockers) {
11463
+ out2.push(` \u2022 ${f.message}`);
11464
+ out2.push(` \u2192 ${f.fix}`);
11465
+ }
11466
+ out2.push(" Run `vibed remixable` for the full guide.");
11467
+ }
11468
+ const remixAdvice = (result.remix?.findings ?? []).filter((f) => !f.blocking);
11469
+ if (remixAdvice.length > 0) {
11470
+ out2.push("");
11471
+ out2.push("Remix polish (not blocking):");
11472
+ for (const f of remixAdvice) out2.push(` \u2022 ${f.message} \u2192 ${f.fix}`);
11473
+ }
11242
11474
  if (result.policy && ruleActive("no_vertical_scroll", result.policy)) {
11243
11475
  out2.push(
11244
11476
  "",
@@ -11520,6 +11752,82 @@ const m = await vibed.storage.meta(); // { postId, ... } \u2014 this post'
11520
11752
  5. Re-run \`vibed check\` \u2014 the \`BROWSER_STORAGE\` warning should be gone.
11521
11753
  `;
11522
11754
 
11755
+ // src/version.ts
11756
+ import { homedir as homedir2 } from "node:os";
11757
+ import { join as join3 } from "node:path";
11758
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
11759
+ var VERSION = true ? "0.2.0" : "0.0.0-dev";
11760
+ var PKG = "@amirhosseinnateghi/vibed-cli";
11761
+ var REGISTRY = `https://registry.npmjs.org/${PKG}/latest`;
11762
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
11763
+ var FETCH_TIMEOUT_MS = 2e3;
11764
+ var stateFile = () => join3(homedir2(), ".vibed", "update-check.json");
11765
+ function isOutdated(current, latest) {
11766
+ const parse4 = (v) => {
11767
+ const core = v.trim().replace(/^v/, "").split("-")[0];
11768
+ if (!core || !/^\d+(\.\d+)*$/.test(core)) return null;
11769
+ return core.split(".").map(Number);
11770
+ };
11771
+ const a = parse4(current);
11772
+ const b = parse4(latest);
11773
+ if (!a || !b) return false;
11774
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
11775
+ const x = a[i] ?? 0;
11776
+ const y = b[i] ?? 0;
11777
+ if (y > x) return true;
11778
+ if (y < x) return false;
11779
+ }
11780
+ return false;
11781
+ }
11782
+ function updateCheckEnabled(argv, env) {
11783
+ if (argv.includes("--json")) return false;
11784
+ if (env.VIBED_NO_UPDATE_CHECK === "1" || env.NO_UPDATE_NOTIFIER === "1") return false;
11785
+ if (env.CI) return false;
11786
+ if (VERSION === "0.0.0-dev") return false;
11787
+ return true;
11788
+ }
11789
+ function updateHint(latest, env) {
11790
+ const viaPlugin = !!env.CLAUDE_PLUGIN_ROOT;
11791
+ const how = viaPlugin ? "update it with: /plugin update make-it-vibed" : `update it with: npm i -g ${PKG}`;
11792
+ return `vibed ${VERSION} is out of date (latest ${latest}) \u2014 ${how}`;
11793
+ }
11794
+ async function readState() {
11795
+ try {
11796
+ return JSON.parse(await readFile3(stateFile(), "utf8"));
11797
+ } catch {
11798
+ return {};
11799
+ }
11800
+ }
11801
+ async function writeState(latest) {
11802
+ try {
11803
+ await mkdir2(join3(homedir2(), ".vibed"), { recursive: true });
11804
+ await writeFile2(stateFile(), JSON.stringify({ checkedAt: Date.now(), latest }), "utf8");
11805
+ } catch {
11806
+ }
11807
+ }
11808
+ async function checkForUpdate(argv = process.argv, env = process.env) {
11809
+ if (!updateCheckEnabled(argv, env)) return null;
11810
+ const state = await readState();
11811
+ if (state.checkedAt && Date.now() - state.checkedAt < CHECK_INTERVAL_MS) {
11812
+ return state.latest && isOutdated(VERSION, state.latest) ? updateHint(state.latest, env) : null;
11813
+ }
11814
+ let latest = null;
11815
+ try {
11816
+ const res = await fetch(REGISTRY, {
11817
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
11818
+ signal: AbortSignal.timeout?.(FETCH_TIMEOUT_MS)
11819
+ });
11820
+ if (res.ok) {
11821
+ const body = await res.json();
11822
+ if (typeof body.version === "string") latest = body.version;
11823
+ }
11824
+ } catch {
11825
+ }
11826
+ if (!latest) return null;
11827
+ await writeState(latest);
11828
+ return isOutdated(VERSION, latest) ? updateHint(latest, env) : null;
11829
+ }
11830
+
11523
11831
  // src/index.ts
11524
11832
  function parseArgs(argv) {
11525
11833
  const _ = [];
@@ -11553,6 +11861,7 @@ Usage:
11553
11861
  vibed login Authenticate this machine (opens the browser)
11554
11862
  vibed check [path] Check if a project can be published (no network)
11555
11863
  vibed storage Print the persistence guide (leaderboards, saves, shared state)
11864
+ vibed remixable Print the remix-friendly authoring guide (CONFIG block, readable code)
11556
11865
  vibed preview [path] Bundle + open the artifact locally (no publish)
11557
11866
  vibed draft [path] Upload a private, hosted preview link (no publish)
11558
11867
  vibed publish [path] Bundle the project and publish it to vibed
@@ -11560,6 +11869,7 @@ Usage:
11560
11869
  vibed delete <id|url> Delete one of your posts
11561
11870
  vibed whoami Show the signed-in account
11562
11871
  vibed logout Remove the saved token
11872
+ vibed version Print the connector version (--version works too)
11563
11873
 
11564
11874
  Flags:
11565
11875
  --api <url> API base (or VIBED_API env). Default: production
@@ -11609,8 +11919,8 @@ async function cmdPreview(args) {
11609
11919
  if (json) out(json, "", { ok: false, fatal: result.fatal });
11610
11920
  return 1;
11611
11921
  }
11612
- const file2 = join3(tmpdir(), `vibed-preview-${result.sizeBytes ?? 0}.html`);
11613
- await writeFile2(file2, result.html, "utf8");
11922
+ const file2 = join4(tmpdir(), `vibed-preview-${result.sizeBytes ?? 0}.html`);
11923
+ await writeFile3(file2, result.html, "utf8");
11614
11924
  const url = pathToFileURL(file2).href;
11615
11925
  if (json) {
11616
11926
  out(json, "", { ok: result.ok, previewFile: file2, previewUrl: url });
@@ -11709,7 +12019,7 @@ async function cmdPublish(args) {
11709
12019
  });
11710
12020
  if (!result.ok || !result.html) {
11711
12021
  out(json, formatReport(result), { ...result, html: void 0 });
11712
- console.error("\nFix the blockers above, then run `vibed publish` again.");
12022
+ console.error("\nFix everything above, then run `vibed publish` again.");
11713
12023
  return 1;
11714
12024
  }
11715
12025
  if (!json) console.log(formatReport(result));
@@ -11790,7 +12100,12 @@ async function cmdLogout() {
11790
12100
  return 0;
11791
12101
  }
11792
12102
  async function main() {
11793
- const args = parseArgs(process.argv.slice(2));
12103
+ const raw = process.argv.slice(2);
12104
+ if (raw.includes("--version") || raw.includes("-v")) {
12105
+ console.log(VERSION);
12106
+ return 0;
12107
+ }
12108
+ const args = parseArgs(raw);
11794
12109
  const cmd = args._[0];
11795
12110
  switch (cmd) {
11796
12111
  case "login":
@@ -11804,6 +12119,9 @@ async function main() {
11804
12119
  case "storage":
11805
12120
  console.log(STORAGE_GUIDE);
11806
12121
  return 0;
12122
+ case "remixable":
12123
+ console.log(REMIX_GUIDE);
12124
+ return 0;
11807
12125
  case "preview":
11808
12126
  return cmdPreview(args);
11809
12127
  case "draft":
@@ -11813,6 +12131,11 @@ async function main() {
11813
12131
  case "delete":
11814
12132
  case "rm":
11815
12133
  return cmdDelete(args);
12134
+ case "version":
12135
+ case "--version":
12136
+ case "-v":
12137
+ console.log(VERSION);
12138
+ return 0;
11816
12139
  case void 0:
11817
12140
  case "help":
11818
12141
  case "--help":
@@ -11825,7 +12148,15 @@ ${USAGE}`);
11825
12148
  return 1;
11826
12149
  }
11827
12150
  }
11828
- main().then((code) => process.exit(code)).catch((e) => {
12151
+ main().then(async (code) => {
12152
+ const nudge = await Promise.race([
12153
+ checkForUpdate().catch(() => null),
12154
+ new Promise((r) => setTimeout(() => r(null), 2500))
12155
+ ]);
12156
+ if (nudge) console.error(`
12157
+ ${nudge}`);
12158
+ process.exit(code);
12159
+ }).catch((e) => {
11829
12160
  console.error(e instanceof Error ? e.message : String(e));
11830
12161
  process.exit(1);
11831
12162
  });
package/dist/vibed.cjs CHANGED
@@ -5974,9 +5974,9 @@ var require_dist = __commonJS({
5974
5974
  });
5975
5975
 
5976
5976
  // src/index.ts
5977
- var import_node_path4 = require("node:path");
5978
- var import_promises3 = require("node:fs/promises");
5979
- var import_node_os3 = require("node:os");
5977
+ var import_node_path5 = require("node:path");
5978
+ var import_promises4 = require("node:fs/promises");
5979
+ var import_node_os4 = require("node:os");
5980
5980
  var import_node_url = require("node:url");
5981
5981
 
5982
5982
  // src/config.ts
@@ -6122,6 +6122,9 @@ var config = {
6122
6122
  // completion the server drops a notification (§7.1) — so a slow build never
6123
6123
  // traps them staring at a spinner.
6124
6124
  BUILDER_SLOW_MS: intEnv("BUILDER_SLOW_MS", 12e3),
6125
+ /** Lite edits (ف۷) kill switch: set to "off" to force every edit turn back to
6126
+ * whole-file regeneration without deploying code. */
6127
+ BUILDER_LITE_EDITS: strEnv("BUILDER_LITE_EDITS", "on"),
6125
6128
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6126
6129
  };
6127
6130
  function strEnv(key, fallback) {
@@ -10261,10 +10264,11 @@ var STORAGE = {
10261
10264
  /** Max records returnable in one query page. */
10262
10265
  QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10263
10266
  /**
10264
- * Platform kill-switch for anonymous shared writes. ON by default: the real
10265
- * gate is per-post owner opt-in (`anon_writes` + writePolicy `anyone`), so a
10266
- * post only accepts guest writes when its owner turns them on. Set the env to
10267
- * `false` to disable anon writes platform-wide regardless of per-post config.
10267
+ * Platform kill-switch for anonymous shared writes. ON by default. The real
10268
+ * gate is the per-post config (`anon_writes` + writePolicy `anyone`), which is
10269
+ * guest-writable BY DEFAULT (a shared vibe should work for whoever opens the
10270
+ * link) an owner can lock it down per post. Set the env to `false` to disable
10271
+ * anon writes platform-wide regardless of per-post config.
10268
10272
  */
10269
10273
  ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", true)
10270
10274
  };
@@ -10378,9 +10382,39 @@ var patchExperienceSchema = external_exports.object({
10378
10382
  visibility: visibilitySchema.optional(),
10379
10383
  coverMode: coverModeSchema.optional()
10380
10384
  });
10385
+ var COMMENT_MEDIA = {
10386
+ quality: 0.82,
10387
+ maxEdge: 1600,
10388
+ maxBytes: 8 * 1024 * 1024,
10389
+ // video/* is the compressed comment clip (client-side MediaRecorder → mp4 on
10390
+ // iOS, webm on Chromium); the client never sends a raw camera video as-is.
10391
+ types: ["image/webp", "image/jpeg", "image/png", "image/gif", "video/mp4", "video/webm"]
10392
+ };
10393
+ var COMMENT_VIDEO = {
10394
+ maxDurationSec: 60,
10395
+ maxEdge: 720,
10396
+ // 1 Mbps → a 60s clip is ~7.5MB, comfortably under the 10MB request-body cap.
10397
+ bitsPerSecond: 1e6,
10398
+ crf: 30,
10399
+ maxUploadBytes: 120 * 1024 * 1024
10400
+ };
10401
+ var commentMediaSchema = external_exports.object({
10402
+ key: external_exports.string().min(1).max(300),
10403
+ type: external_exports.enum(COMMENT_MEDIA.types),
10404
+ w: external_exports.number().int().positive().max(2e4),
10405
+ h: external_exports.number().int().positive().max(2e4)
10406
+ });
10407
+ var commentVideoSchema = external_exports.object({ key: external_exports.string().min(1).max(300) });
10381
10408
  var commentSchema = external_exports.object({
10382
- body: external_exports.string().min(1).max(500),
10383
- parentId: external_exports.string().uuid().optional()
10409
+ body: external_exports.string().max(500),
10410
+ parentId: external_exports.string().uuid().optional(),
10411
+ media: commentMediaSchema.optional(),
10412
+ // ready image/GIF (compressed client-side)
10413
+ video: commentVideoSchema.optional()
10414
+ // raw video → transcoded async in the worker
10415
+ }).refine((d) => d.body.trim().length > 0 || !!d.media || !!d.video, {
10416
+ message: "Write something or attach a photo or video.",
10417
+ path: ["body"]
10384
10418
  });
10385
10419
  var eventSchema = external_exports.object({
10386
10420
  // "record"/"screenshot" fire when a viewer captures the vibe (see the in-iframe
@@ -10411,6 +10445,9 @@ var createTurnRequestSchema = external_exports.object({
10411
10445
  // A voice turn carries no text yet — the worker transcribes `voice` first (م۷).
10412
10446
  voice: voiceInputSchema.optional(),
10413
10447
  model: external_exports.string().max(100).optional()
10448
+ // No editMode: lite edits (ف۷) proved themselves, so every edit takes that
10449
+ // path and the server decides — a client can't ask for the slow rewrite.
10450
+ // BUILDER_LITE_EDITS=off remains the fleet-wide kill switch.
10414
10451
  }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0 || !!v.voice, {
10415
10452
  message: "Send a message, an attachment, or a voice recording."
10416
10453
  });
@@ -10515,6 +10552,183 @@ var notifPrefsPatchSchema = external_exports.object({
10515
10552
  groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10516
10553
  });
10517
10554
 
10555
+ // ../shared/src/remix.ts
10556
+ var REMIX_GUIDE = `# Make it remix-friendly
10557
+
10558
+ On vibed every published vibe is **remixable**: anyone can fork it and make it
10559
+ theirs, and the AI builder can be asked to change it. A vibe is only *really*
10560
+ remixable if a stranger \u2014 or a model editing it \u2014 can find the knobs without
10561
+ reading the whole file. Treat this as part of shipping, like mobile-first.
10562
+
10563
+ It pays off twice: edits to a well-structured file are applied as small precise
10564
+ patches instead of regenerating the whole thing, which is faster, cheaper, and
10565
+ far less likely to break what already worked.
10566
+
10567
+ ## 1. One CONFIG block, at the top
10568
+
10569
+ Every tunable lives in a single object at the top of the script \u2014 the first
10570
+ thing anyone sees:
10571
+
10572
+ \`\`\`js
10573
+ const CONFIG = {
10574
+ // --- Feel ---
10575
+ gravity: 0.6,
10576
+ jumpForce: -11,
10577
+ pipeGap: 160,
10578
+ pipeSpeed: 2.4,
10579
+ // --- Look ---
10580
+ skyColor: "#7ec8ff",
10581
+ birdColor: "#ffd166",
10582
+ // --- Words (all user-facing text) ---
10583
+ title: "Tap to Fly",
10584
+ gameOverText: "Ouch! Tap to try again",
10585
+ scoreLabel: "Score",
10586
+ };
10587
+ \`\`\`
10588
+
10589
+ Rules that make it useful:
10590
+ - **Literal values only** \u2014 numbers, strings, colors. No computed expressions.
10591
+ - **Everything reads from CONFIG.** If a number appears twice in the logic, it
10592
+ belongs in CONFIG once.
10593
+ - **All user-facing text goes here too**, so rewording or translating a vibe is
10594
+ one edit, not a hunt.
10595
+
10596
+ ## 2. Content separate from logic
10597
+
10598
+ Questions, levels, items, cards, prompts \u2014 a labelled array right after CONFIG:
10599
+
10600
+ \`\`\`js
10601
+ const QUESTIONS = [
10602
+ { q: "Pick a color", options: ["Red", "Blue"], answer: 0 },
10603
+ // add more here \u2014 same shape
10604
+ ];
10605
+ \`\`\`
10606
+
10607
+ Now "add five more questions" is a data edit anyone can make.
10608
+
10609
+ ## 3. Predictable section order
10610
+
10611
+ Lay the script out in this order, each with a one-line banner comment:
10612
+
10613
+ \`\`\`js
10614
+ // ============ CONFIG ============
10615
+ // ============ CONTENT ===========
10616
+ // ============ STATE =============
10617
+ // ============ HELPERS ===========
10618
+ // ============ RENDER ============
10619
+ // ============ INPUT =============
10620
+ // ============ INIT ==============
10621
+ \`\`\`
10622
+
10623
+ A remixer (and a patching model) then knows where a change belongs before
10624
+ reading a single line.
10625
+
10626
+ ## 4. Write it plainly
10627
+
10628
+ - Descriptive names: \`spawnEnemy()\`, not \`se()\`; \`playerSpeed\`, not \`ps\`.
10629
+ - Small functions that do one thing. One \`init()\` entry point, one game loop.
10630
+ - Two-space indent, no minification, no bundler output, no clever one-liners or
10631
+ chained ternaries.
10632
+ - A short comment at each seam someone would want to change:
10633
+ \`// Win condition \u2014 swap this for a timer, a score target, ...\`
10634
+ - Stable \`id\`/\`class\` names on the elements a remixer would target.
10635
+ - No dead code, and no copy-pasted near-identical blocks \u2014 factor them into one
10636
+ function. (Duplicated blocks also make an automated edit ambiguous, so it
10637
+ fails or lands in the wrong place.)
10638
+
10639
+ ## 5. Quick self-check before publishing
10640
+
10641
+ - Could someone change the colors, speed, and all the words by editing only
10642
+ CONFIG?
10643
+ - Could they add one more level/question/item by editing only the content array?
10644
+ - Is there any magic number or user-facing string outside those two blocks?
10645
+ - Would a stranger know where to add a feature within ten seconds of scrolling?
10646
+
10647
+ If the answer to any of those is no, fix it before you publish \u2014 that is the
10648
+ difference between a vibe people fork and one they scroll past.`;
10649
+ var SMALL_VIBE_CHARS = 1200;
10650
+ var SMALL_VIBE_LINES = 40;
10651
+ var COMMENT_EXPECTED_LINES = 60;
10652
+ var SECTIONS_EXPECTED_LINES = 120;
10653
+ var MINIFIED_LINE_CHARS = 400;
10654
+ var MINIFIED_AVG_LINE_CHARS = 160;
10655
+ function authorScript(html) {
10656
+ const parts = [];
10657
+ const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
10658
+ let m;
10659
+ while (m = re.exec(html)) {
10660
+ const attrs = m[1] ?? "";
10661
+ if (/data-vibed-bridge/i.test(attrs)) continue;
10662
+ if (/\bsrc=/i.test(attrs)) continue;
10663
+ parts.push(m[2] ?? "");
10664
+ }
10665
+ return parts.join("\n");
10666
+ }
10667
+ function remixAudit(html) {
10668
+ const script = authorScript(html ?? "");
10669
+ const lines = script.split(/\r?\n/);
10670
+ const codeLines = lines.filter((l) => l.trim() !== "");
10671
+ const scriptLines = codeLines.length;
10672
+ const scriptChars = codeLines.reduce((n, l) => n + l.trim().length, 0);
10673
+ const avgLine = scriptLines > 0 ? scriptChars / scriptLines : 0;
10674
+ const findings = [];
10675
+ const longest = codeLines.reduce((n, l) => Math.max(n, l.trim().length), 0);
10676
+ const denselyPacked = scriptChars > 600 && avgLine > MINIFIED_AVG_LINE_CHARS;
10677
+ if (longest > MINIFIED_LINE_CHARS || denselyPacked) {
10678
+ findings.push({
10679
+ code: "MINIFIED",
10680
+ blocking: true,
10681
+ message: `The script averages ${Math.round(avgLine)} characters per line (longest ${longest}) \u2014 this looks minified or bundled.`,
10682
+ fix: "Publish readable source: turn off minification for the vibe build, or inline the unminified code."
10683
+ });
10684
+ }
10685
+ if (scriptLines >= SMALL_VIBE_LINES || scriptChars >= SMALL_VIBE_CHARS) {
10686
+ if (!/\b(?:const|let|var)\s+CONFIG\s*=/.test(script)) {
10687
+ findings.push({
10688
+ code: "NO_CONFIG",
10689
+ blocking: true,
10690
+ message: "No `const CONFIG = { ... }` block \u2014 a remixer has to hunt through the logic to change anything.",
10691
+ fix: "Move every tunable (speeds, sizes, colors, counts, durations) AND all user-facing text into one CONFIG object at the top, and read from it everywhere."
10692
+ });
10693
+ }
10694
+ const cryptic = /* @__PURE__ */ new Set();
10695
+ const decl = /\b(?:const|let|var)\s+([A-Za-z_$])\b(?!\s*[,)])/g;
10696
+ let d;
10697
+ while (d = decl.exec(script)) {
10698
+ const name = d[1];
10699
+ if (!"ijknxyt_".includes(name)) cryptic.add(name);
10700
+ }
10701
+ if (cryptic.size >= 5) {
10702
+ findings.push({
10703
+ code: "CRYPTIC_NAMES",
10704
+ blocking: true,
10705
+ message: `${cryptic.size} single-letter variables (${[...cryptic].slice(0, 6).join(", ")}) \u2014 the code reads as machine output.`,
10706
+ fix: "Give variables and functions descriptive names (`playerSpeed`, not `p`)."
10707
+ });
10708
+ }
10709
+ if (scriptLines >= COMMENT_EXPECTED_LINES) {
10710
+ const comments = codeLines.filter((l) => /^\s*(\/\/|\/\*|\*)/.test(l)).length;
10711
+ if (comments === 0) {
10712
+ findings.push({
10713
+ code: "NO_COMMENTS",
10714
+ blocking: false,
10715
+ message: "The script has no comments at all.",
10716
+ fix: "Add a short note at each seam a remixer would change (the win condition, the spawn rule, the scoring)."
10717
+ });
10718
+ }
10719
+ }
10720
+ if (scriptLines >= SECTIONS_EXPECTED_LINES && !/CONFIG\s*=+\s*$|=== *CONFIG|-- *CONFIG/im.test(script)) {
10721
+ findings.push({
10722
+ code: "NO_SECTIONS",
10723
+ blocking: false,
10724
+ message: "No section banners in a long script.",
10725
+ fix: "Split it with banner comments: CONFIG \u2192 CONTENT \u2192 STATE \u2192 helpers \u2192 render \u2192 input \u2192 init."
10726
+ });
10727
+ }
10728
+ }
10729
+ return { ok: !findings.some((f) => f.blocking), findings, scriptLines };
10730
+ }
10731
+
10518
10732
  // ../shared/src/builderEvents.ts
10519
10733
  var BUILD_EVENT_VERSION = 1;
10520
10734
  var BUILD_PHASES = [
@@ -11172,8 +11386,10 @@ async function bundleProject(projectDir, opts = {}) {
11172
11386
  allowedOrigins
11173
11387
  });
11174
11388
  const result = validate(html, { maxBytes, allowedOrigins, policy: opts.policy });
11389
+ const remix = remixAudit(html);
11175
11390
  return {
11176
- ok: result.ok,
11391
+ ok: result.ok && remix.ok,
11392
+ remix,
11177
11393
  html,
11178
11394
  policy: opts.policy,
11179
11395
  entry: resolved.entry,
@@ -11234,12 +11450,28 @@ function formatReport(result) {
11234
11450
  result.sizeBytes != null ? `size: ${fmtSize(result.sizeBytes)}` : null
11235
11451
  ].filter(Boolean).join(" \xB7 ");
11236
11452
  if (where) out2.push(where);
11453
+ const remixBlockers = (result.remix?.findings ?? []).filter((f) => f.blocking);
11237
11454
  if (result.ok) {
11238
11455
  out2.push("\u2713 Vibeable \u2014 this project can be published to vibed.");
11239
- } else {
11456
+ } else if (result.blockers.length > 0) {
11240
11457
  out2.push(`\u2717 Not vibeable yet \u2014 ${result.blockers.length} blocker(s):`);
11241
11458
  out2.push(...summarize(result).lines);
11242
11459
  }
11460
+ if (remixBlockers.length > 0) {
11461
+ out2.push("");
11462
+ out2.push(`\u2717 Not remixable yet \u2014 ${remixBlockers.length} thing(s) to fix before publishing:`);
11463
+ for (const f of remixBlockers) {
11464
+ out2.push(` \u2022 ${f.message}`);
11465
+ out2.push(` \u2192 ${f.fix}`);
11466
+ }
11467
+ out2.push(" Run `vibed remixable` for the full guide.");
11468
+ }
11469
+ const remixAdvice = (result.remix?.findings ?? []).filter((f) => !f.blocking);
11470
+ if (remixAdvice.length > 0) {
11471
+ out2.push("");
11472
+ out2.push("Remix polish (not blocking):");
11473
+ for (const f of remixAdvice) out2.push(` \u2022 ${f.message} \u2192 ${f.fix}`);
11474
+ }
11243
11475
  if (result.policy && ruleActive("no_vertical_scroll", result.policy)) {
11244
11476
  out2.push(
11245
11477
  "",
@@ -11521,6 +11753,82 @@ const m = await vibed.storage.meta(); // { postId, ... } \u2014 this post'
11521
11753
  5. Re-run \`vibed check\` \u2014 the \`BROWSER_STORAGE\` warning should be gone.
11522
11754
  `;
11523
11755
 
11756
+ // src/version.ts
11757
+ var import_node_os3 = require("node:os");
11758
+ var import_node_path4 = require("node:path");
11759
+ var import_promises3 = require("node:fs/promises");
11760
+ var VERSION = true ? "0.2.0" : "0.0.0-dev";
11761
+ var PKG = "@amirhosseinnateghi/vibed-cli";
11762
+ var REGISTRY = `https://registry.npmjs.org/${PKG}/latest`;
11763
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
11764
+ var FETCH_TIMEOUT_MS = 2e3;
11765
+ var stateFile = () => (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".vibed", "update-check.json");
11766
+ function isOutdated(current, latest) {
11767
+ const parse4 = (v) => {
11768
+ const core = v.trim().replace(/^v/, "").split("-")[0];
11769
+ if (!core || !/^\d+(\.\d+)*$/.test(core)) return null;
11770
+ return core.split(".").map(Number);
11771
+ };
11772
+ const a = parse4(current);
11773
+ const b = parse4(latest);
11774
+ if (!a || !b) return false;
11775
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
11776
+ const x = a[i] ?? 0;
11777
+ const y = b[i] ?? 0;
11778
+ if (y > x) return true;
11779
+ if (y < x) return false;
11780
+ }
11781
+ return false;
11782
+ }
11783
+ function updateCheckEnabled(argv, env) {
11784
+ if (argv.includes("--json")) return false;
11785
+ if (env.VIBED_NO_UPDATE_CHECK === "1" || env.NO_UPDATE_NOTIFIER === "1") return false;
11786
+ if (env.CI) return false;
11787
+ if (VERSION === "0.0.0-dev") return false;
11788
+ return true;
11789
+ }
11790
+ function updateHint(latest, env) {
11791
+ const viaPlugin = !!env.CLAUDE_PLUGIN_ROOT;
11792
+ const how = viaPlugin ? "update it with: /plugin update make-it-vibed" : `update it with: npm i -g ${PKG}`;
11793
+ return `vibed ${VERSION} is out of date (latest ${latest}) \u2014 ${how}`;
11794
+ }
11795
+ async function readState() {
11796
+ try {
11797
+ return JSON.parse(await (0, import_promises3.readFile)(stateFile(), "utf8"));
11798
+ } catch {
11799
+ return {};
11800
+ }
11801
+ }
11802
+ async function writeState(latest) {
11803
+ try {
11804
+ await (0, import_promises3.mkdir)((0, import_node_path4.join)((0, import_node_os3.homedir)(), ".vibed"), { recursive: true });
11805
+ await (0, import_promises3.writeFile)(stateFile(), JSON.stringify({ checkedAt: Date.now(), latest }), "utf8");
11806
+ } catch {
11807
+ }
11808
+ }
11809
+ async function checkForUpdate(argv = process.argv, env = process.env) {
11810
+ if (!updateCheckEnabled(argv, env)) return null;
11811
+ const state = await readState();
11812
+ if (state.checkedAt && Date.now() - state.checkedAt < CHECK_INTERVAL_MS) {
11813
+ return state.latest && isOutdated(VERSION, state.latest) ? updateHint(state.latest, env) : null;
11814
+ }
11815
+ let latest = null;
11816
+ try {
11817
+ const res = await fetch(REGISTRY, {
11818
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
11819
+ signal: AbortSignal.timeout?.(FETCH_TIMEOUT_MS)
11820
+ });
11821
+ if (res.ok) {
11822
+ const body = await res.json();
11823
+ if (typeof body.version === "string") latest = body.version;
11824
+ }
11825
+ } catch {
11826
+ }
11827
+ if (!latest) return null;
11828
+ await writeState(latest);
11829
+ return isOutdated(VERSION, latest) ? updateHint(latest, env) : null;
11830
+ }
11831
+
11524
11832
  // src/index.ts
11525
11833
  function parseArgs(argv) {
11526
11834
  const _ = [];
@@ -11554,6 +11862,7 @@ Usage:
11554
11862
  vibed login Authenticate this machine (opens the browser)
11555
11863
  vibed check [path] Check if a project can be published (no network)
11556
11864
  vibed storage Print the persistence guide (leaderboards, saves, shared state)
11865
+ vibed remixable Print the remix-friendly authoring guide (CONFIG block, readable code)
11557
11866
  vibed preview [path] Bundle + open the artifact locally (no publish)
11558
11867
  vibed draft [path] Upload a private, hosted preview link (no publish)
11559
11868
  vibed publish [path] Bundle the project and publish it to vibed
@@ -11561,6 +11870,7 @@ Usage:
11561
11870
  vibed delete <id|url> Delete one of your posts
11562
11871
  vibed whoami Show the signed-in account
11563
11872
  vibed logout Remove the saved token
11873
+ vibed version Print the connector version (--version works too)
11564
11874
 
11565
11875
  Flags:
11566
11876
  --api <url> API base (or VIBED_API env). Default: production
@@ -11572,7 +11882,7 @@ Publish only:
11572
11882
  tags (pick from): ${TAGS.join(", ")}`;
11573
11883
  async function cmdCheck(args) {
11574
11884
  const json = isTrue(args.flags.json);
11575
- const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
11885
+ const dir2 = (0, import_node_path5.resolve)(args._[1] ?? ".");
11576
11886
  const cfg = await loadConfig({ api: str(args.flags.api) });
11577
11887
  const policy = isTrue(args.flags["local-policy"]) ? void 0 : await fetchPolicy(cfg.apiBase);
11578
11888
  if (!json && policy) console.error(`Rules: ${cfg.apiBase}`);
@@ -11596,7 +11906,7 @@ async function cmdCheck(args) {
11596
11906
  }
11597
11907
  async function cmdPreview(args) {
11598
11908
  const json = isTrue(args.flags.json);
11599
- const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
11909
+ const dir2 = (0, import_node_path5.resolve)(args._[1] ?? ".");
11600
11910
  const cfg = await loadConfig({ api: str(args.flags.api) });
11601
11911
  const policy = isTrue(args.flags["local-policy"]) ? void 0 : await fetchPolicy(cfg.apiBase);
11602
11912
  const result = await bundleProject(dir2, {
@@ -11610,8 +11920,8 @@ async function cmdPreview(args) {
11610
11920
  if (json) out(json, "", { ok: false, fatal: result.fatal });
11611
11921
  return 1;
11612
11922
  }
11613
- const file2 = (0, import_node_path4.join)((0, import_node_os3.tmpdir)(), `vibed-preview-${result.sizeBytes ?? 0}.html`);
11614
- await (0, import_promises3.writeFile)(file2, result.html, "utf8");
11923
+ const file2 = (0, import_node_path5.join)((0, import_node_os4.tmpdir)(), `vibed-preview-${result.sizeBytes ?? 0}.html`);
11924
+ await (0, import_promises4.writeFile)(file2, result.html, "utf8");
11615
11925
  const url = (0, import_node_url.pathToFileURL)(file2).href;
11616
11926
  if (json) {
11617
11927
  out(json, "", { ok: result.ok, previewFile: file2, previewUrl: url });
@@ -11648,7 +11958,7 @@ function reportPublishError(e) {
11648
11958
  }
11649
11959
  async function cmdDraft(args) {
11650
11960
  const json = isTrue(args.flags.json);
11651
- const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
11961
+ const dir2 = (0, import_node_path5.resolve)(args._[1] ?? ".");
11652
11962
  const cfg = await loadConfig({ api: str(args.flags.api) });
11653
11963
  if (!cfg.token) {
11654
11964
  console.error("Not signed in. Run `vibed login` first.");
@@ -11700,7 +12010,7 @@ async function cmdPublish(args) {
11700
12010
  return reportPublishError(e);
11701
12011
  }
11702
12012
  }
11703
- const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
12013
+ const dir2 = (0, import_node_path5.resolve)(args._[1] ?? ".");
11704
12014
  const policy = await fetchPolicy(cfg.apiBase);
11705
12015
  const result = await bundleProject(dir2, {
11706
12016
  entry: str(args.flags.entry),
@@ -11710,7 +12020,7 @@ async function cmdPublish(args) {
11710
12020
  });
11711
12021
  if (!result.ok || !result.html) {
11712
12022
  out(json, formatReport(result), { ...result, html: void 0 });
11713
- console.error("\nFix the blockers above, then run `vibed publish` again.");
12023
+ console.error("\nFix everything above, then run `vibed publish` again.");
11714
12024
  return 1;
11715
12025
  }
11716
12026
  if (!json) console.log(formatReport(result));
@@ -11791,7 +12101,12 @@ async function cmdLogout() {
11791
12101
  return 0;
11792
12102
  }
11793
12103
  async function main() {
11794
- const args = parseArgs(process.argv.slice(2));
12104
+ const raw = process.argv.slice(2);
12105
+ if (raw.includes("--version") || raw.includes("-v")) {
12106
+ console.log(VERSION);
12107
+ return 0;
12108
+ }
12109
+ const args = parseArgs(raw);
11795
12110
  const cmd = args._[0];
11796
12111
  switch (cmd) {
11797
12112
  case "login":
@@ -11805,6 +12120,9 @@ async function main() {
11805
12120
  case "storage":
11806
12121
  console.log(STORAGE_GUIDE);
11807
12122
  return 0;
12123
+ case "remixable":
12124
+ console.log(REMIX_GUIDE);
12125
+ return 0;
11808
12126
  case "preview":
11809
12127
  return cmdPreview(args);
11810
12128
  case "draft":
@@ -11814,6 +12132,11 @@ async function main() {
11814
12132
  case "delete":
11815
12133
  case "rm":
11816
12134
  return cmdDelete(args);
12135
+ case "version":
12136
+ case "--version":
12137
+ case "-v":
12138
+ console.log(VERSION);
12139
+ return 0;
11817
12140
  case void 0:
11818
12141
  case "help":
11819
12142
  case "--help":
@@ -11826,7 +12149,15 @@ ${USAGE}`);
11826
12149
  return 1;
11827
12150
  }
11828
12151
  }
11829
- main().then((code) => process.exit(code)).catch((e) => {
12152
+ main().then(async (code) => {
12153
+ const nudge = await Promise.race([
12154
+ checkForUpdate().catch(() => null),
12155
+ new Promise((r) => setTimeout(() => r(null), 2500))
12156
+ ]);
12157
+ if (nudge) console.error(`
12158
+ ${nudge}`);
12159
+ process.exit(code);
12160
+ }).catch((e) => {
11830
12161
  console.error(e instanceof Error ? e.message : String(e));
11831
12162
  process.exit(1);
11832
12163
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@amirhosseinnateghi/vibed-cli",
3
- "version": "0.1.8",
4
- "description": "make it vibed check, bundle, and publish a project to vibed from the terminal",
3
+ "version": "0.2.0",
4
+ "description": "make it vibed \u2014 check, bundle, and publish a project to vibed from the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -19,8 +19,8 @@
19
19
  "access": "public"
20
20
  },
21
21
  "scripts": {
22
- "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js && chmod +x dist/index.js",
23
- "build:cjs": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/vibed.cjs && chmod +x dist/vibed.cjs",
22
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --define:__VIBED_VERSION__='\"0.2.0\"' --outfile=dist/index.js && chmod +x dist/index.js",
23
+ "build:cjs": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --define:__VIBED_VERSION__='\"0.2.0\"' --outfile=dist/vibed.cjs && chmod +x dist/vibed.cjs",
24
24
  "prepublishOnly": "pnpm build",
25
25
  "dev": "tsx src/index.ts",
26
26
  "test": "vitest run",