@amirhosseinnateghi/vibed-cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,8 +11,8 @@ vibed whoami | logout
11
11
  ```
12
12
 
13
13
  Flags: `--api <url>` (or `VIBED_API`), `--entry <file>`, `--no-build`,
14
- `--title`, `--caption`, `--category`, `--tags a,b`, `--unlisted`, `--no-remix`,
15
- `--yes`, `--json`.
14
+ `--title`, `--tags a,b` (up to 5 from the curated set — `vibed publish --help`),
15
+ `--unlisted`, `--no-remix`, `--yes`, `--json`.
16
16
 
17
17
  See [docs/connector.md](../../docs/connector.md) for the full guide (login flow,
18
18
  what "vibeable" means, agent/AGENTS.md integration). The bundler reuses
package/dist/index.js CHANGED
@@ -6047,6 +6047,25 @@ var CATEGORIES = [
6047
6047
  "Art",
6048
6048
  "Other"
6049
6049
  ];
6050
+ var TAGS = [
6051
+ "game",
6052
+ "puzzle",
6053
+ "quiz",
6054
+ "simulation",
6055
+ "music",
6056
+ "art",
6057
+ "animation",
6058
+ "story",
6059
+ "invitation",
6060
+ "love",
6061
+ "education",
6062
+ "science",
6063
+ "news",
6064
+ "tool",
6065
+ "productivity"
6066
+ ];
6067
+ var MAX_TAGS = 5;
6068
+ var FEED_TAG_CHIPS = ["All", ...TAGS];
6050
6069
  var VISIBILITY = ["public", "unlisted"];
6051
6070
  var COVER_MODES = ["image", "gif"];
6052
6071
  var EXPERIENCE_SOURCE = ["builder", "import", "remix"];
@@ -10323,15 +10342,22 @@ var categorySchema = external_exports.enum(CATEGORIES);
10323
10342
  var visibilitySchema = external_exports.enum(VISIBILITY);
10324
10343
  var coverModeSchema = external_exports.enum(COVER_MODES);
10325
10344
  var sourceSchema = external_exports.enum(EXPERIENCE_SOURCE);
10326
- var tagsSchema = external_exports.array(external_exports.string().min(1).max(40)).max(10).transform(
10345
+ var KNOWN_TAGS = new Set(TAGS);
10346
+ var tagsSchema = external_exports.array(external_exports.string().max(40)).transform(
10327
10347
  (tags) => Array.from(
10328
- new Set(tags.map((t) => t.replace(/^#/, "").trim().toLowerCase()).filter(Boolean))
10329
- )
10348
+ new Set(
10349
+ tags.map((t) => t.replace(/^#/, "").trim().toLowerCase()).filter((t) => KNOWN_TAGS.has(t))
10350
+ )
10351
+ ).slice(0, MAX_TAGS)
10330
10352
  ).default([]);
10331
10353
  var publishSchema = external_exports.object({
10332
10354
  title: external_exports.string().min(1).max(120),
10355
+ // Caption was removed from the publish UI; kept optional so old clients / the
10356
+ // CLI don't break and existing captions stay searchable.
10333
10357
  caption: external_exports.string().max(2e3).default(""),
10334
- category: categorySchema,
10358
+ // Legacy: category is no longer chosen in the UI. The connector CLI may still
10359
+ // send it; publishExperience folds it into `tags` and leaves the column null.
10360
+ category: categorySchema.optional(),
10335
10361
  tags: tagsSchema,
10336
10362
  remixable: external_exports.boolean().default(true),
10337
10363
  visibility: visibilitySchema.default("public"),
@@ -10444,7 +10470,8 @@ var aiConnectionSchema = external_exports.object({
10444
10470
  });
10445
10471
  var feedQuerySchema = external_exports.object({
10446
10472
  tab: external_exports.enum(["explore", "following"]).default("explore"),
10447
- category: external_exports.string().optional(),
10473
+ // Filter the explore feed to a single tag (replaces the old `category` param).
10474
+ tag: external_exports.string().optional(),
10448
10475
  cursor: external_exports.string().optional(),
10449
10476
  limit: external_exports.coerce.number().int().min(1).max(30).default(10)
10450
10477
  });
@@ -11229,6 +11256,15 @@ function formatReport(result) {
11229
11256
  out2.push(` \u2022 \u2026and ${result.inlineWarnings.length - 12} more`);
11230
11257
  }
11231
11258
  }
11259
+ const usesBrowserStorage = [...result.warnings.map((w) => w.message), ...result.inlineWarnings].some(
11260
+ (m) => /BROWSER_STORAGE|localStorage|sessionStorage|indexedDB/i.test(m)
11261
+ );
11262
+ if (usesBrowserStorage) {
11263
+ out2.push(
11264
+ "",
11265
+ "\u2139 Uses browser storage (blocked in the sandbox). Port it to `window.vibed.storage` \u2014 run `vibed storage` (or the vibed_storage_guide MCP tool) for the local + shared SDK."
11266
+ );
11267
+ }
11232
11268
  return out2.join("\n");
11233
11269
  }
11234
11270
 
@@ -11371,10 +11407,14 @@ function inferTitle(html, projectDir) {
11371
11407
  const dir2 = basename(projectDir).replace(/[-_]+/g, " ").trim();
11372
11408
  return (dir2 || "Untitled").slice(0, 120);
11373
11409
  }
11374
- function normalizeCategory(input) {
11375
- if (!input) return "Other";
11376
- const match = CATEGORIES.find((c) => c.toLowerCase() === input.toLowerCase());
11377
- return match ?? "Other";
11410
+ function normalizeTags(tags, category) {
11411
+ const known = new Set(TAGS);
11412
+ const out2 = [];
11413
+ for (const t of [...tags ?? [], ...category ? [category] : []]) {
11414
+ const s = t.replace(/^#/, "").trim().toLowerCase();
11415
+ if (known.has(s) && !out2.includes(s)) out2.push(s);
11416
+ }
11417
+ return out2.slice(0, MAX_TAGS);
11378
11418
  }
11379
11419
  async function importDraft(client, html) {
11380
11420
  const r = await apiFetch(client, "POST", "/imports", { html });
@@ -11383,9 +11423,7 @@ async function importDraft(client, html) {
11383
11423
  async function publishDraft(client, draftArtifactKey, meta, html, projectDir) {
11384
11424
  const exp = await apiFetch(client, "POST", "/experiences", {
11385
11425
  title: meta.title?.trim() || (html ? inferTitle(html, projectDir ?? ".") : "Untitled"),
11386
- caption: meta.caption ?? "",
11387
- category: normalizeCategory(meta.category),
11388
- tags: meta.tags ?? [],
11426
+ tags: normalizeTags(meta.tags, meta.category),
11389
11427
  remixable: meta.remixable ?? true,
11390
11428
  visibility: meta.visibility ?? "public",
11391
11429
  draftArtifactKey
@@ -11406,6 +11444,82 @@ async function deleteExperience(client, idOrUrl) {
11406
11444
  return id;
11407
11445
  }
11408
11446
 
11447
+ // src/storageGuide.ts
11448
+ var STORAGE_GUIDE = `# vibed storage \u2014 add persistence to a vibe (\`window.vibed.storage\`)
11449
+
11450
+ A vibe is ONE static sandboxed HTML file with no backend. \`localStorage\`,
11451
+ \`sessionStorage\`, \`indexedDB\`, and cookies are BLOCKED \u2014 they throw in the
11452
+ sandbox (\`vibed check\` warns \`BROWSER_STORAGE\`). For anything you'd persist,
11453
+ use the injected \`window.vibed.storage\` SDK instead. The platform is the
11454
+ backend; you write zero server code.
11455
+
11456
+ ## Two rules that apply to EVERY call
11457
+ 1. Every call returns a Promise that can reject \u2014 always guard it. When the vibe
11458
+ runs OUTSIDE vibed (e.g. a local file, your \`vibed draft\`/\`preview\`), calls
11459
+ reject with \`{ code: "no_host" }\`. The vibe must stay playable standalone, so
11460
+ treat missing storage as "no saved data yet", never a crash.
11461
+ 2. WRITES require the viewer to be signed in. If not, they reject with
11462
+ \`{ code: "unauthenticated" }\` \u2014 catch it and show a gentle "sign in to save"
11463
+ nudge. READS of shared data work for everyone.
11464
+
11465
+ Publishing auto-detects SDK usage and turns storage on for the post \u2014 no flags.
11466
+
11467
+ ## Tier 1 \u2014 \`vibed.storage.local\` (LOCAL: private, per-viewer, per-post)
11468
+ Each viewer gets their own private bucket for THIS post. Nobody else can read it.
11469
+ Use for: this player's high score, their settings, "resume where I left off".
11470
+ \`\`\`js
11471
+ await vibed.storage.local.set("best", 8200);
11472
+ const best = await vibed.storage.local.get("best"); // null if never set
11473
+ await vibed.storage.local.delete("best");
11474
+ \`\`\`
11475
+
11476
+ ## Tier 2 \u2014 \`vibed.storage.shared\` (GLOBAL: one collective store for the whole post)
11477
+ Data every viewer of the post shares \u2014 leaderboards, votes, guestbooks, shared
11478
+ state. Organized into named "collections" (like tables) you invent.
11479
+ \`\`\`js
11480
+ // key/value doc with optimistic concurrency
11481
+ await vibed.storage.shared.set("state", "doc", { phase: "open" }); // \u2192 { value, version, ... }
11482
+ await vibed.storage.shared.set("state", "doc", next, { ifVersion: 3 }); // rejects { code: "conflict" }
11483
+ const doc = await vibed.storage.shared.get("state", "doc"); // null if missing
11484
+ await vibed.storage.shared.delete("state", "doc");
11485
+
11486
+ // append-only list + query (guestbook, bets, submissions)
11487
+ await vibed.storage.shared.append("bets", { pick: "Argentina", stake: 50 });
11488
+ const mine = await vibed.storage.shared.query("bets", { mine: true });
11489
+ const feed = await vibed.storage.shared.query("bets", { sort: "-created", limit: 30 });
11490
+
11491
+ // counter (poll / reactions)
11492
+ const { num } = await vibed.storage.shared.increment("votes", "argentina", 1);
11493
+
11494
+ // leaderboard: submit a score (keep the best), read the top, get MY rank
11495
+ await vibed.storage.shared.submit("leaderboard", { score, keep: "max", meta: { emoji: "\u{1F427}" } });
11496
+ const top = await vibed.storage.shared.query("leaderboard", { sort: "-num", limit: 10 });
11497
+ const rank = await vibed.storage.shared.rank("leaderboard"); // { rank, of, score } | null
11498
+
11499
+ // live updates (polling now): re-renders when the collection changes; returns an unsubscribe fn
11500
+ const off = vibed.storage.shared.subscribe("leaderboard", (r) => render(r.rows), { intervalMs: 4000 });
11501
+ \`\`\`
11502
+
11503
+ ## Who is watching
11504
+ \`\`\`js
11505
+ const me = await vibed.storage.identity(); // { id, signedIn, name? } \u2014 stable per viewer; identify entries
11506
+ const m = await vibed.storage.meta(); // { postId, ... } \u2014 this post's context
11507
+ \`\`\`
11508
+
11509
+ ## Limits (free tier \u2014 design within these)
11510
+ - Each value must be JSON-serializable and under the per-value byte cap
11511
+ (rejects \`{ code: "too_large" }\`); \`invalid\` if it can't be JSON-stringified.
11512
+ - shared collections are for structured game/app state, NOT open chat/DM/messaging.
11513
+
11514
+ ## Porting a project that used browser storage (the \`BROWSER_STORAGE\` warning)
11515
+ 1. Find every \`localStorage\`/\`sessionStorage\`/\`indexedDB\`/cookie use.
11516
+ 2. Per-viewer state (settings, progress, personal best) \u2192 \`vibed.storage.local\`.
11517
+ 3. Anything others should see (scores to compare, shared counters, submissions)
11518
+ \u2192 the matching \`vibed.storage.shared\` primitive above.
11519
+ 4. Make every call \`await\` + \`try/catch\`, with a standalone fallback (no_host).
11520
+ 5. Re-run \`vibed check\` \u2014 the \`BROWSER_STORAGE\` warning should be gone.
11521
+ `;
11522
+
11409
11523
  // src/index.ts
11410
11524
  function parseArgs(argv) {
11411
11525
  const _ = [];
@@ -11438,6 +11552,7 @@ var USAGE = `vibed \u2014 make it vibed
11438
11552
  Usage:
11439
11553
  vibed login Authenticate this machine (opens the browser)
11440
11554
  vibed check [path] Check if a project can be published (no network)
11555
+ vibed storage Print the persistence guide (leaderboards, saves, shared state)
11441
11556
  vibed preview [path] Bundle + open the artifact locally (no publish)
11442
11557
  vibed draft [path] Upload a private, hosted preview link (no publish)
11443
11558
  vibed publish [path] Bundle the project and publish it to vibed
@@ -11452,8 +11567,8 @@ Flags:
11452
11567
  --no-build Don't run the project's build step
11453
11568
  --json Machine-readable output
11454
11569
  Publish only:
11455
- --title <t> --caption <c> --category <Game|Tool|Art|Story|Quiz|News|Invitation|Other>
11456
- --tags a,b,c --unlisted --no-remix --yes (skip confirmation)`;
11570
+ --title <t> --tags <a,b,c> --unlisted --no-remix --yes (skip confirmation)
11571
+ tags (pick from): ${TAGS.join(", ")}`;
11457
11572
  async function cmdCheck(args) {
11458
11573
  const json = isTrue(args.flags.json);
11459
11574
  const dir2 = resolve2(args._[1] ?? ".");
@@ -11509,7 +11624,8 @@ Preview \u2192 ${file2}`);
11509
11624
  function metaFromArgs(args) {
11510
11625
  return {
11511
11626
  title: str(args.flags.title),
11512
- caption: str(args.flags.caption),
11627
+ // --category is deprecated (folded into tags server- and client-side); still
11628
+ // parsed so old scripts keep working. Caption was removed.
11513
11629
  category: str(args.flags.category),
11514
11630
  tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11515
11631
  remixable: !isTrue(args.flags["no-remix"]),
@@ -11555,7 +11671,7 @@ async function cmdDraft(args) {
11555
11671
  json,
11556
11672
  `
11557
11673
  \u2713 Preview (not published): ${d.previewUrl ?? "(no preview URL)"}
11558
- Publish it with: vibed publish --draft-key ${d.draftArtifactKey} --title "\u2026" --category <Cat>`,
11674
+ Publish it with: vibed publish --draft-key ${d.draftArtifactKey} --title "\u2026" --tags <a,b>`,
11559
11675
  { ok: true, ...d }
11560
11676
  );
11561
11677
  return 0;
@@ -11685,6 +11801,9 @@ async function main() {
11685
11801
  return cmdWhoami(args);
11686
11802
  case "check":
11687
11803
  return cmdCheck(args);
11804
+ case "storage":
11805
+ console.log(STORAGE_GUIDE);
11806
+ return 0;
11688
11807
  case "preview":
11689
11808
  return cmdPreview(args);
11690
11809
  case "draft":
package/dist/vibed.cjs CHANGED
@@ -6048,6 +6048,25 @@ var CATEGORIES = [
6048
6048
  "Art",
6049
6049
  "Other"
6050
6050
  ];
6051
+ var TAGS = [
6052
+ "game",
6053
+ "puzzle",
6054
+ "quiz",
6055
+ "simulation",
6056
+ "music",
6057
+ "art",
6058
+ "animation",
6059
+ "story",
6060
+ "invitation",
6061
+ "love",
6062
+ "education",
6063
+ "science",
6064
+ "news",
6065
+ "tool",
6066
+ "productivity"
6067
+ ];
6068
+ var MAX_TAGS = 5;
6069
+ var FEED_TAG_CHIPS = ["All", ...TAGS];
6051
6070
  var VISIBILITY = ["public", "unlisted"];
6052
6071
  var COVER_MODES = ["image", "gif"];
6053
6072
  var EXPERIENCE_SOURCE = ["builder", "import", "remix"];
@@ -10324,15 +10343,22 @@ var categorySchema = external_exports.enum(CATEGORIES);
10324
10343
  var visibilitySchema = external_exports.enum(VISIBILITY);
10325
10344
  var coverModeSchema = external_exports.enum(COVER_MODES);
10326
10345
  var sourceSchema = external_exports.enum(EXPERIENCE_SOURCE);
10327
- var tagsSchema = external_exports.array(external_exports.string().min(1).max(40)).max(10).transform(
10346
+ var KNOWN_TAGS = new Set(TAGS);
10347
+ var tagsSchema = external_exports.array(external_exports.string().max(40)).transform(
10328
10348
  (tags) => Array.from(
10329
- new Set(tags.map((t) => t.replace(/^#/, "").trim().toLowerCase()).filter(Boolean))
10330
- )
10349
+ new Set(
10350
+ tags.map((t) => t.replace(/^#/, "").trim().toLowerCase()).filter((t) => KNOWN_TAGS.has(t))
10351
+ )
10352
+ ).slice(0, MAX_TAGS)
10331
10353
  ).default([]);
10332
10354
  var publishSchema = external_exports.object({
10333
10355
  title: external_exports.string().min(1).max(120),
10356
+ // Caption was removed from the publish UI; kept optional so old clients / the
10357
+ // CLI don't break and existing captions stay searchable.
10334
10358
  caption: external_exports.string().max(2e3).default(""),
10335
- category: categorySchema,
10359
+ // Legacy: category is no longer chosen in the UI. The connector CLI may still
10360
+ // send it; publishExperience folds it into `tags` and leaves the column null.
10361
+ category: categorySchema.optional(),
10336
10362
  tags: tagsSchema,
10337
10363
  remixable: external_exports.boolean().default(true),
10338
10364
  visibility: visibilitySchema.default("public"),
@@ -10445,7 +10471,8 @@ var aiConnectionSchema = external_exports.object({
10445
10471
  });
10446
10472
  var feedQuerySchema = external_exports.object({
10447
10473
  tab: external_exports.enum(["explore", "following"]).default("explore"),
10448
- category: external_exports.string().optional(),
10474
+ // Filter the explore feed to a single tag (replaces the old `category` param).
10475
+ tag: external_exports.string().optional(),
10449
10476
  cursor: external_exports.string().optional(),
10450
10477
  limit: external_exports.coerce.number().int().min(1).max(30).default(10)
10451
10478
  });
@@ -11230,6 +11257,15 @@ function formatReport(result) {
11230
11257
  out2.push(` \u2022 \u2026and ${result.inlineWarnings.length - 12} more`);
11231
11258
  }
11232
11259
  }
11260
+ const usesBrowserStorage = [...result.warnings.map((w) => w.message), ...result.inlineWarnings].some(
11261
+ (m) => /BROWSER_STORAGE|localStorage|sessionStorage|indexedDB/i.test(m)
11262
+ );
11263
+ if (usesBrowserStorage) {
11264
+ out2.push(
11265
+ "",
11266
+ "\u2139 Uses browser storage (blocked in the sandbox). Port it to `window.vibed.storage` \u2014 run `vibed storage` (or the vibed_storage_guide MCP tool) for the local + shared SDK."
11267
+ );
11268
+ }
11233
11269
  return out2.join("\n");
11234
11270
  }
11235
11271
 
@@ -11372,10 +11408,14 @@ function inferTitle(html, projectDir) {
11372
11408
  const dir2 = (0, import_node_path3.basename)(projectDir).replace(/[-_]+/g, " ").trim();
11373
11409
  return (dir2 || "Untitled").slice(0, 120);
11374
11410
  }
11375
- function normalizeCategory(input) {
11376
- if (!input) return "Other";
11377
- const match = CATEGORIES.find((c) => c.toLowerCase() === input.toLowerCase());
11378
- return match ?? "Other";
11411
+ function normalizeTags(tags, category) {
11412
+ const known = new Set(TAGS);
11413
+ const out2 = [];
11414
+ for (const t of [...tags ?? [], ...category ? [category] : []]) {
11415
+ const s = t.replace(/^#/, "").trim().toLowerCase();
11416
+ if (known.has(s) && !out2.includes(s)) out2.push(s);
11417
+ }
11418
+ return out2.slice(0, MAX_TAGS);
11379
11419
  }
11380
11420
  async function importDraft(client, html) {
11381
11421
  const r = await apiFetch(client, "POST", "/imports", { html });
@@ -11384,9 +11424,7 @@ async function importDraft(client, html) {
11384
11424
  async function publishDraft(client, draftArtifactKey, meta, html, projectDir) {
11385
11425
  const exp = await apiFetch(client, "POST", "/experiences", {
11386
11426
  title: meta.title?.trim() || (html ? inferTitle(html, projectDir ?? ".") : "Untitled"),
11387
- caption: meta.caption ?? "",
11388
- category: normalizeCategory(meta.category),
11389
- tags: meta.tags ?? [],
11427
+ tags: normalizeTags(meta.tags, meta.category),
11390
11428
  remixable: meta.remixable ?? true,
11391
11429
  visibility: meta.visibility ?? "public",
11392
11430
  draftArtifactKey
@@ -11407,6 +11445,82 @@ async function deleteExperience(client, idOrUrl) {
11407
11445
  return id;
11408
11446
  }
11409
11447
 
11448
+ // src/storageGuide.ts
11449
+ var STORAGE_GUIDE = `# vibed storage \u2014 add persistence to a vibe (\`window.vibed.storage\`)
11450
+
11451
+ A vibe is ONE static sandboxed HTML file with no backend. \`localStorage\`,
11452
+ \`sessionStorage\`, \`indexedDB\`, and cookies are BLOCKED \u2014 they throw in the
11453
+ sandbox (\`vibed check\` warns \`BROWSER_STORAGE\`). For anything you'd persist,
11454
+ use the injected \`window.vibed.storage\` SDK instead. The platform is the
11455
+ backend; you write zero server code.
11456
+
11457
+ ## Two rules that apply to EVERY call
11458
+ 1. Every call returns a Promise that can reject \u2014 always guard it. When the vibe
11459
+ runs OUTSIDE vibed (e.g. a local file, your \`vibed draft\`/\`preview\`), calls
11460
+ reject with \`{ code: "no_host" }\`. The vibe must stay playable standalone, so
11461
+ treat missing storage as "no saved data yet", never a crash.
11462
+ 2. WRITES require the viewer to be signed in. If not, they reject with
11463
+ \`{ code: "unauthenticated" }\` \u2014 catch it and show a gentle "sign in to save"
11464
+ nudge. READS of shared data work for everyone.
11465
+
11466
+ Publishing auto-detects SDK usage and turns storage on for the post \u2014 no flags.
11467
+
11468
+ ## Tier 1 \u2014 \`vibed.storage.local\` (LOCAL: private, per-viewer, per-post)
11469
+ Each viewer gets their own private bucket for THIS post. Nobody else can read it.
11470
+ Use for: this player's high score, their settings, "resume where I left off".
11471
+ \`\`\`js
11472
+ await vibed.storage.local.set("best", 8200);
11473
+ const best = await vibed.storage.local.get("best"); // null if never set
11474
+ await vibed.storage.local.delete("best");
11475
+ \`\`\`
11476
+
11477
+ ## Tier 2 \u2014 \`vibed.storage.shared\` (GLOBAL: one collective store for the whole post)
11478
+ Data every viewer of the post shares \u2014 leaderboards, votes, guestbooks, shared
11479
+ state. Organized into named "collections" (like tables) you invent.
11480
+ \`\`\`js
11481
+ // key/value doc with optimistic concurrency
11482
+ await vibed.storage.shared.set("state", "doc", { phase: "open" }); // \u2192 { value, version, ... }
11483
+ await vibed.storage.shared.set("state", "doc", next, { ifVersion: 3 }); // rejects { code: "conflict" }
11484
+ const doc = await vibed.storage.shared.get("state", "doc"); // null if missing
11485
+ await vibed.storage.shared.delete("state", "doc");
11486
+
11487
+ // append-only list + query (guestbook, bets, submissions)
11488
+ await vibed.storage.shared.append("bets", { pick: "Argentina", stake: 50 });
11489
+ const mine = await vibed.storage.shared.query("bets", { mine: true });
11490
+ const feed = await vibed.storage.shared.query("bets", { sort: "-created", limit: 30 });
11491
+
11492
+ // counter (poll / reactions)
11493
+ const { num } = await vibed.storage.shared.increment("votes", "argentina", 1);
11494
+
11495
+ // leaderboard: submit a score (keep the best), read the top, get MY rank
11496
+ await vibed.storage.shared.submit("leaderboard", { score, keep: "max", meta: { emoji: "\u{1F427}" } });
11497
+ const top = await vibed.storage.shared.query("leaderboard", { sort: "-num", limit: 10 });
11498
+ const rank = await vibed.storage.shared.rank("leaderboard"); // { rank, of, score } | null
11499
+
11500
+ // live updates (polling now): re-renders when the collection changes; returns an unsubscribe fn
11501
+ const off = vibed.storage.shared.subscribe("leaderboard", (r) => render(r.rows), { intervalMs: 4000 });
11502
+ \`\`\`
11503
+
11504
+ ## Who is watching
11505
+ \`\`\`js
11506
+ const me = await vibed.storage.identity(); // { id, signedIn, name? } \u2014 stable per viewer; identify entries
11507
+ const m = await vibed.storage.meta(); // { postId, ... } \u2014 this post's context
11508
+ \`\`\`
11509
+
11510
+ ## Limits (free tier \u2014 design within these)
11511
+ - Each value must be JSON-serializable and under the per-value byte cap
11512
+ (rejects \`{ code: "too_large" }\`); \`invalid\` if it can't be JSON-stringified.
11513
+ - shared collections are for structured game/app state, NOT open chat/DM/messaging.
11514
+
11515
+ ## Porting a project that used browser storage (the \`BROWSER_STORAGE\` warning)
11516
+ 1. Find every \`localStorage\`/\`sessionStorage\`/\`indexedDB\`/cookie use.
11517
+ 2. Per-viewer state (settings, progress, personal best) \u2192 \`vibed.storage.local\`.
11518
+ 3. Anything others should see (scores to compare, shared counters, submissions)
11519
+ \u2192 the matching \`vibed.storage.shared\` primitive above.
11520
+ 4. Make every call \`await\` + \`try/catch\`, with a standalone fallback (no_host).
11521
+ 5. Re-run \`vibed check\` \u2014 the \`BROWSER_STORAGE\` warning should be gone.
11522
+ `;
11523
+
11410
11524
  // src/index.ts
11411
11525
  function parseArgs(argv) {
11412
11526
  const _ = [];
@@ -11439,6 +11553,7 @@ var USAGE = `vibed \u2014 make it vibed
11439
11553
  Usage:
11440
11554
  vibed login Authenticate this machine (opens the browser)
11441
11555
  vibed check [path] Check if a project can be published (no network)
11556
+ vibed storage Print the persistence guide (leaderboards, saves, shared state)
11442
11557
  vibed preview [path] Bundle + open the artifact locally (no publish)
11443
11558
  vibed draft [path] Upload a private, hosted preview link (no publish)
11444
11559
  vibed publish [path] Bundle the project and publish it to vibed
@@ -11453,8 +11568,8 @@ Flags:
11453
11568
  --no-build Don't run the project's build step
11454
11569
  --json Machine-readable output
11455
11570
  Publish only:
11456
- --title <t> --caption <c> --category <Game|Tool|Art|Story|Quiz|News|Invitation|Other>
11457
- --tags a,b,c --unlisted --no-remix --yes (skip confirmation)`;
11571
+ --title <t> --tags <a,b,c> --unlisted --no-remix --yes (skip confirmation)
11572
+ tags (pick from): ${TAGS.join(", ")}`;
11458
11573
  async function cmdCheck(args) {
11459
11574
  const json = isTrue(args.flags.json);
11460
11575
  const dir2 = (0, import_node_path4.resolve)(args._[1] ?? ".");
@@ -11510,7 +11625,8 @@ Preview \u2192 ${file2}`);
11510
11625
  function metaFromArgs(args) {
11511
11626
  return {
11512
11627
  title: str(args.flags.title),
11513
- caption: str(args.flags.caption),
11628
+ // --category is deprecated (folded into tags server- and client-side); still
11629
+ // parsed so old scripts keep working. Caption was removed.
11514
11630
  category: str(args.flags.category),
11515
11631
  tags: typeof args.flags.tags === "string" ? args.flags.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0,
11516
11632
  remixable: !isTrue(args.flags["no-remix"]),
@@ -11556,7 +11672,7 @@ async function cmdDraft(args) {
11556
11672
  json,
11557
11673
  `
11558
11674
  \u2713 Preview (not published): ${d.previewUrl ?? "(no preview URL)"}
11559
- Publish it with: vibed publish --draft-key ${d.draftArtifactKey} --title "\u2026" --category <Cat>`,
11675
+ Publish it with: vibed publish --draft-key ${d.draftArtifactKey} --title "\u2026" --tags <a,b>`,
11560
11676
  { ok: true, ...d }
11561
11677
  );
11562
11678
  return 0;
@@ -11686,6 +11802,9 @@ async function main() {
11686
11802
  return cmdWhoami(args);
11687
11803
  case "check":
11688
11804
  return cmdCheck(args);
11805
+ case "storage":
11806
+ console.log(STORAGE_GUIDE);
11807
+ return 0;
11689
11808
  case "preview":
11690
11809
  return cmdPreview(args);
11691
11810
  case "draft":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amirhosseinnateghi/vibed-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",