@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 +2 -2
- package/dist/index.js +135 -16
- package/dist/vibed.cjs +135 -16
- package/package.json +1 -1
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`, `--
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
11375
|
-
|
|
11376
|
-
const
|
|
11377
|
-
|
|
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
|
-
|
|
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>
|
|
11456
|
-
|
|
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
|
-
|
|
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" --
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
11376
|
-
|
|
11377
|
-
const
|
|
11378
|
-
|
|
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
|
-
|
|
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>
|
|
11457
|
-
|
|
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
|
-
|
|
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" --
|
|
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":
|