typebulb 0.13.0 → 0.13.2

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
@@ -18,7 +18,7 @@ A `.bulb.md` file bundles code, styles, data, and config in one file.
18
18
  - **CLI logging** — `tb.server.log(...)` prints to the CLI's stdout
19
19
  - **Env files** — `.env` / `.env.local` load from cwd, `.env.local` overriding `.env` (an exported shell var wins over both). `--mode <name>` adds `.env.<name>` to switch environments (local/staging/prod); a startup line reports which keys loaded from where.
20
20
  - **Server mode** — `--server` runs only the `**server.ts**` section in Node, skipping the web server. Bulbs with only `**server.ts**` (no `**code.tsx**`) use this mode automatically.
21
- - **Type-check without running** — `typebulb check <file>` runs `tsc --noEmit` against the bulb and exits non-zero on errors. Useful for AI editors / CI.
21
+ - **Type-check without running** — `typebulb check <file>` runs `tsc --noEmit` against the bulb: non-zero exit with diagnostics on errors, a one-line all-clear on stderr on success.
22
22
  - **Filesystem access** — `tb.fs.read()` (UTF-8 text), `tb.fs.readBytes()` (raw `Uint8Array`), and `tb.fs.write()` (text or bytes) for local files. Requires `--trust`.
23
23
  - **Hot reload** — Recompiles on save and refreshes the browser (on by default; disable with `--no-watch`)
24
24
  - **Package resolution** — Client dependencies are automatically resolved by generating import maps (same resolver as typebulb.com). Server dependencies are automatically installed via npm.
@@ -27,7 +27,7 @@ A `.bulb.md` file bundles code, styles, data, and config in one file.
27
27
  - **AI calls** — `tb.ai()` for general-purpose AI (chatbots, agents, experiments). `tb.models()` lists available models. Set API keys in `.env` (see below). Requires `--trust`.
28
28
  - **Sandboxed by default** — A plain `npx typebulb my-app.bulb.md` runs with no filesystem or `server.ts` (like typebulb.com); `--trust` grants those for a run. Trust is **remembered**: `typebulb trust <file>` elevates a bulb once so later plain runs are trusted, `untrust` revokes it, and `--no-trust` forces sandboxed for a single run.
29
29
  - **Predict trust** — `typebulb predict <file>` reports the capability a bulb will likely need (fs / AI / `server.ts`) without running it, so you can decide on `--trust` up front rather than after a mid-run permission failure.
30
- - **Agent viewer** — `typebulb agent:claude` opens the agent viewer, a browser view over a Claude Code session that renders embedded bulbs, KaTeX, and mermaid live inline, plus runs/stops local bulbs (see [Claude](#claude)). `typebulb agent` (no target) is the first command an agent runs: it tells the agent how to show a bulb inline or build one locally. `typebulb skill` prints this whole README as an Agent Skill the agent can read and save.
30
+ - **Agent mirror** — `typebulb agent:claude` opens the agent mirror, a browser view over a Claude Code session that renders embedded bulbs, KaTeX, and mermaid live inline, plus runs/stops local bulbs (see [Claude](#claude)). `typebulb agent` (no target) is the first command an agent runs: it tells the agent how to show a bulb inline or build one locally. `typebulb skill` prints this whole README as an Agent Skill the agent can read and save.
31
31
 
32
32
  ## Quick Start
33
33
 
@@ -117,18 +117,19 @@ npm install -g typebulb
117
117
 
118
118
  ```
119
119
  typebulb [file.bulb.md] Run a bulb (defaults to .bulb.md in cwd)
120
- typebulb agent:claude Open the agent viewer (a Claude Code session)
120
+ typebulb agent:claude Open the agent mirror (a Claude Code session)
121
121
  typebulb agent Start here — how to show a bulb inline or build one locally
122
- (prints the viewer URL when one is up); always exits 0
122
+ (prints the mirror URL when one is up); always exits 0
123
123
  typebulb skill Print this README as an Agent Skill on stdout
124
+ typebulb call <file> <fn> […] Invoke one server.ts export headlessly: prints its return as JSON to stdout, logs/errors to stderr (needs --trust)
124
125
  typebulb check [file.bulb.md] Type-check a bulb without running it
125
126
  typebulb predict [file] Report the capability a bulb probably needs, without running it
126
127
  typebulb models List AI models for tb.ai, filtered by your .env API keys
127
128
  typebulb logs [file|pid] Print a running bulb's captured console (no arg: list running servers; -f follow, -n N tail)
128
129
  typebulb stop [file|pid] Stop a running bulb (no arg: list this project's running servers)
129
- typebulb stop --bulbs Stop this project's bulbs; the agent viewer keeps running
130
- typebulb stop --agent Stop this project's agent viewer; its bulbs keep running
131
- typebulb stop --global Stop every running bulb and viewer, all projects (housekeeping)
130
+ typebulb stop --bulbs Stop this project's bulbs; the agent mirror keeps running
131
+ typebulb stop --agent Stop this project's agent mirror; its bulbs keep running
132
+ typebulb stop --global Stop every running bulb and mirror, all projects (housekeeping)
132
133
  typebulb trust [file] Remember a bulb as trusted (no arg: list trusted bulbs)
133
134
  typebulb untrust <file> Forget a bulb's trust (back to sandboxed)
134
135
  typebulb --no-watch <file> Disable hot reload
@@ -198,7 +199,7 @@ A bulb is a single **markdown** file — the minimum viable structure for a smal
198
199
  | `**data.txt**` | Read-only data your code processes via `tb.data(n)` (raw string) / `tb.json(n)` (parsed) — JSON, CSV, XML, YAML, or plain text. Multiple chunks are separated by **two blank lines**. |
199
200
  | `**infer.md**` / `**insight.json**` | Runtime one-shot LLM call via `tb.infer()` — a typebulb.com feature; not supported locally. |
200
201
  | `**notes.md**` | Persistent context for the AI assistant, carried across conversations and clones. Not run. |
201
- | `**server.ts**` | Node.js code; its exports become `tb.server.<name>()` in the browser. **Local only.** |
202
+ | `**server.ts**` | Node.js code; its exports become `tb.server.<name>()` in the browser. Plain Node — no `tb`; log with `console.log`. **Local only.** |
202
203
 
203
204
  ## The `tb.*` API, by target
204
205
 
@@ -228,7 +229,7 @@ A local `.bulb.md` can be re-imported into typebulb.com. If it has a `**server.t
228
229
 
229
230
  ## Claude
230
231
 
231
- The agent viewer currently supports Claude Code only. `npx typebulb agent:claude` gives the user a great scratchpad experience:
232
+ The agent mirror currently supports Claude Code only. `npx typebulb agent:claude` gives the user a great scratchpad experience:
232
233
 
233
234
  * a view over the Claude Code session, where assistant messages containing bulbs render as sandboxed embedded bulbs inline in the conversation, alongside KaTeX math, mermaid diagrams and svg.
234
235
  * run and stop any bulb in their project.
@@ -241,7 +242,7 @@ To keep this skill on hand across sessions, run `npx typebulb skill` and copy it
241
242
  ### When Claude should output local vs embedded bulbs
242
243
 
243
244
  - **First, can it even embed?** A bulb needing `tb.ai`, `tb.fs`, or `server.ts` must be **local** — embeds are client-only, so those calls fail there. The choice below is only for client-only bulbs.
244
- - **Is anyone watching?** An embed only renders live when the agent viewer is open; with none it shows as raw text. `typebulb agent` tells you which case you're in. If no viewer is up and you want to show something inline, start it yourself — `npx typebulb agent:claude --no-open` — and share the link; don't make the user do it.
245
+ - **Is anyone watching?** An embed only renders live when the agent mirror is open; with none it shows as raw text. `typebulb agent` tells you which case you're in. If no mirror is up and you want to show something inline, start it yourself — `npx typebulb agent:claude --no-open` — and share the link; don't make the user do it.
245
246
  - **Something to see right now, in the flow of the conversation** — a chart of some numbers, a quick simulation, an illustrative widget. → **embedded**: emit it in a `bulb` block so it renders live inline.
246
247
  - **A tool worth keeping** — something to reuse, run on its own, or refine over several turns. → **local**: write a `.bulb.md` file run with `npx typebulb`. An embedded block is throwaway and can't be edited in place, so it's the wrong fit for anything iterative.
247
248
 
@@ -249,21 +250,21 @@ To keep this skill on hand across sessions, run `npx typebulb skill` and copy it
249
250
 
250
251
  To render a bulb live inline, wrap the **entire** bulb — frontmatter and all blocks — in a fenced code block whose opening line is **four backticks immediately followed by `bulb`**, and whose closing line is four backticks. Four, not three, so the bulb's own triple-backtick code fences nest inside without prematurely closing the outer block.
251
252
 
252
- The agent viewer turns that block into a live, sandboxed app, with a *breakout ↗* control that saves it as a `.bulb.md` in the `typebulbs/` folder — editable with hot reload, and sandboxed unless you trust it. Embedded bulbs are client-only — no `server.ts`, no `tb.fs`/`tb.ai`, no storage.
253
+ The agent mirror turns that block into a live, sandboxed app, with a *breakout ↗* control that saves it as a `.bulb.md` in the `typebulbs/` folder — editable with hot reload, and sandboxed unless you trust it. Embedded bulbs are client-only — no `server.ts`, no `tb.fs`/`tb.ai`, no storage.
253
254
 
254
- **Fixing a broken embed?** Re-emit it under the *same* `name:` — the viewer folds the old version into a stub and keeps your fix as the live one. (A rename is treated as a new bulb.)
255
+ **Fixing a broken embed?** Re-emit it under the *same* `name:` — the mirror folds the old version into a stub and keeps your fix as the live one. (A rename is treated as a new bulb.)
255
256
 
256
- **A broken embed reads back.** Emit it and move on; embeds usually just work. If the user says one broke, the viewer has already forwarded its compile/runtime error to `typebulb logs claude`, name-tagged (`[embed <name>]`) — pull it from there and fix, instead of asking the user to copy-paste.
257
+ **A broken embed reads back.** Emit it and move on; embeds usually just work. If the user says one broke, the mirror has already forwarded its compile/runtime error to `typebulb logs claude`, name-tagged (`[embed <name>]`) — pull it from there and fix, instead of asking the user to copy-paste.
257
258
 
258
259
  ## Sizing
259
260
 
260
261
  The host owns a bulb's **width**; you own its **height**.
261
262
 
262
- **Width is the host's.** Standalone, a bulb fills its browser window; in the agent viewer, an embed fits the conversation column by default, with a per-embed *spread* toggle to the full transcript width — and a cap so a tall embed doesn't run away down the transcript. Don't set a width or guess how much room you'll get.
263
+ **Width is the host's.** Standalone, a bulb fills its browser window; in the agent mirror, an embed fits the conversation column by default, with a per-embed *spread* toggle to the full transcript width — and a cap so a tall embed doesn't run away down the transcript. Don't set a width or guess how much room you'll get.
263
264
 
264
- **Height follows your content.** Prose, a form, a chart flow to their natural height set none. A full-bleed canvas has no natural height: give its root `height: 100dvh` **and** a pixel floor like `min-height: 420px`. Both are needed — `100dvh` fills its own window if the bulb is broken out, and the floor holds a definite band when embedded. Without the floor a bare `100dvh` collapses to zero embedded, because the viewer sizes an embed to its content height and `100dvh` gives it nothing to measure against.
265
+ **Height follows your content.** Set a height that adapts — content-driven or viewport-filling — never a fixed pixel value, which neither grows to fill a broken-out window nor shrinks to its content. Prose, a form, a chart flow to their natural height: set none. A full-bleed surface with no natural height of its own gets `height: 100dvh` **and** a pixel floor like `min-height: 420px`. Both are needed — `100dvh` fills its own window if the bulb is broken out, and the floor holds a definite band when embedded. Without the floor a bare `100dvh` collapses to zero embedded, because the mirror sizes an embed to its content height and `100dvh` gives it nothing to measure against.
265
266
 
266
- **When embedded, keep vertical space on the root in `padding`, not `margin`.** The viewer measures an embed by `document.body.scrollHeight`, and the runtime makes `body` a block formatting context so a root child's vertical margin (yours, or a UA default like `<h1>`'s) is contained rather than escaping the measurement — so you no longer have to get this exactly right. It's still cleaner to keep the horizontal `auto` for centering and move the vertical space to padding:
267
+ **When embedded, keep vertical space on the root in `padding`, not `margin`.** The mirror measures an embed by `document.body.scrollHeight`, and the runtime makes `body` a block formatting context so a root child's vertical margin (yours, or a UA default like `<h1>`'s) is contained rather than escaping the measurement — so you no longer have to get this exactly right. It's still cleaner to keep the horizontal `auto` for centering and move the vertical space to padding:
267
268
 
268
269
  ```css
269
270
  .wrap { margin: 0 auto; padding: 24px 16px; } /* not: margin: 24px auto */
@@ -274,13 +275,14 @@ The host owns a bulb's **width**; you own its **height**.
274
275
  - **`config.json` `description`** is the bulb's SEO meta description — keep it to one or two plain sentences (~150–160 chars), or it gets truncated.
275
276
  - **The frontmatter `name:` is the bulb's title** — a few words, not a sentence — and the filename should be its slug (`name: Counter` → `counter.bulb.md`).
276
277
  - **Self-testing a local bulb** — To confirm a bulb works, run it, instrument with `tb.server.log(...)` (prints to the server's stdout, captured in the log — and works **even on a sandboxed bulb**), and read it back with `typebulb logs`. That's the loop to verify behaviour without asking the user to copy-paste console output. `tb.fs.write(...)` is handy for dumping large outputs.
278
+ - **Testing a `server.ts` export directly** — `typebulb call <file> <fn> [arg…]` boots `server.ts`, invokes one export, and prints its return as JSON to stdout (logs/errors to stderr, so `… | jq` works). Args after `<fn>` are JSON-or-string; `--args '<json-array>'` (or `--args -` for stdin) escapes tricky quoting. Needs `--trust`.
277
279
  - **Mount to the container your `index.html` declares.** The corpus convention is `<div id="root"></div>` with `createRoot(document.getElementById("root")!)`.
278
280
  - **All imports at the top of `code.tsx`.** Bare imports (`react`, `d3`, `three`, …) auto-resolve from a CDN — no install step. Declare them in `config.json` `dependencies` anyway: that's what lets `npx typebulb check` fetch type defs (without it you get errors like `TS2875: react/jsx-runtime`) and pins versions.
279
281
  - **Theme-aware styling.** Style off CSS variables / `currentColor` so the bulb reads correctly in both light and dark; the host sets the theme.
280
282
  - **`tb.ai()` takes more than the basics** — the full shape is `tb.ai({ messages, system?, reasoning?, provider?, model?, webSearch? })` → `Promise<{ text }>` (non-streaming). `webSearch` defaults **on** in the CLI (you supply your own key); pass `webSearch: false` to turn it off.
281
283
  - **`tb.theme` drives the `html[data-theme]` attribute** — style off that selector (`html[data-theme="dark"] { … }`); don't read `tb.theme` to branch your rendering.
282
284
  - **`color-scheme` is set for you** — the host always applies `html[data-theme="dark"] { color-scheme: dark }` / `html[data-theme="light"] { color-scheme: light }` on top of your `styles.css`.
283
- - **Math renders live in the viewer** — write inline math as `$…$` and display math as `$$…$$` (the viewer renders KaTeX; a plain terminal chat doesn't, so reach for real math here). Prefer `$y = x^2$` over inline-code or a Unicode superscript (`y = x²`).
285
+ - **Math renders live in the mirror** — write inline math as `$…$` and display math as `$$…$$` (the mirror renders KaTeX; a plain terminal chat doesn't, so reach for real math here). Prefer `$y = x^2$` over inline-code or a Unicode superscript (`y = x²`).
284
286
  - **`tb.json<T>(n)` is generic** — `tb.json<Album[]>(0)` returns typed parsed JSON; `tb.data(n)` returns the raw string.
285
287
  - **`tb.proxy()` is for same-origin Web Worker / WASM loads** — e.g. ffmpeg or tesseract: `tb.proxy("https://unpkg.com/...")` routes the CDN URL through the local server's origin.
286
288
  - **Prefer an `index.html` fragment** over a full HTML document — usually just the mount stub (`<div id="root"></div>`).
package/description.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Author and run Typebulb bulbs — single-file markdown apps (TypeScript/TSX) that run
2
2
  locally via `npx typebulb` (full power: filesystem, database, `server.ts`, `tb.ai`) or
3
- render live inline in a Claude Code session through Typebulb's agent viewer (sandboxed,
3
+ render live inline in a Claude Code session through Typebulb's agent mirror (sandboxed,
4
4
  client-only). A bulb can be a visual widget (chart, simulation, diagram, calculator, UI),
5
5
  a full-stack tool with a Node backend, or an AI app that calls models at runtime. Covers
6
6
  the bulb format, the `tb.*` API, trust, and the local run/embed workflow. Use when the
@@ -693,7 +693,7 @@ ${a.map(({variableName:d,uniqueLocalName:g})=>` reactHotLoader.register(${d}, "
693
693
  // otherwise fail with cryptic CORS/CSP errors. Detect it and fail clearly.
694
694
  //
695
695
  // The same sandboxed-iframe path also backs the CLI's default (untrusted) launch
696
- // (Specs/Typebulb-CLI-Trust.md), where the right message names \`--trust\` rather
696
+ // (TB-Trust.md), where the right message names \`--trust\` rather
697
697
  // than "no host bridge". The host injects window.__TB_EMBED_ERR__ to override the
698
698
  // text; absent it, the nested-bulb wording applies.
699
699
  const isEmbedded = window.parent !== window;
@@ -897,6 +897,7 @@ ${a.map(({variableName:d,uniqueLocalName:g})=>` reactHotLoader.register(${d}, "
897
897
  }
898
898
  })();
899
899
  `;function REt(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}var PEt=` *, *::before, *::after { box-sizing: border-box; }
900
+ canvas { max-width: 100%; }
900
901
  body { margin: 0; display: flow-root; font-family: system-ui, -apple-system, sans-serif; }
901
902
  html[data-theme="dark"] { color-scheme: dark; }
902
903
  html[data-theme="light"] { color-scheme: light; }`;function FEt(s,o){let a=d=>d.replace(/<\/script/gi,"<\\/script");return` <script>
@@ -967,6 +968,7 @@ ${iwn}
967
968
 
968
969
  <script type="module">
969
970
  ${Q(a)}
971
+ ${B?"globalThis.__tbEntryRan = true; /* load-failure backstop \u2014 see embedProtocol */":""}
970
972
  <\/script>
971
973
  </body>
972
974
  </html>`}var iwn=`<script>
@@ -1018,17 +1020,25 @@ ${Q(a)}
1018
1020
  };
1019
1021
  var update = function () { if (!raf) raf = requestAnimationFrame(apply); };
1020
1022
  de.style.overflow = 'hidden';
1023
+ var errorPosted = false;
1024
+ var postError = function (message) { errorPosted = true; post({ __typebulbEmbed: true, kind: 'error', message: String(message) }); };
1025
+ // Capture phase: a resource/module load failure (a <script>/<link>/<img> that 404s) does NOT
1026
+ // bubble, so it reaches window only here, carrying the failing element as e.target (no message).
1027
+ window.addEventListener('error', function (e) {
1028
+ var t = e && e.target;
1029
+ if (t && t !== window && (t.src || t.href)) postError('Failed to load ' + (t.src || t.href));
1030
+ }, true);
1021
1031
  window.addEventListener('error', function (e) {
1022
1032
  var m = String((e && e.message) || (e && e.error) || 'Error');
1023
1033
  // The benign "ResizeObserver loop completed\u2026" notice surfaces as a window error in
1024
1034
  // some browsers; it's not a bulb fault, so don't forward it to the host as one.
1025
1035
  if (m.indexOf('ResizeObserver') !== -1) return;
1026
- post({ __typebulbEmbed: true, kind: 'error', message: m });
1036
+ postError(m);
1027
1037
  });
1028
1038
  window.addEventListener('unhandledrejection', function (e) {
1029
1039
  var r = e && e.reason;
1030
1040
  var msg = r && r.message ? r.message : (r == null ? 'Unhandled rejection' : r);
1031
- post({ __typebulbEmbed: true, kind: 'error', message: String(msg) });
1041
+ postError(msg);
1032
1042
  });
1033
1043
  // Run at load (module mounted \u2192 real content, not the empty pre-mount body that would
1034
1044
  // collapse the frame and snap back). Observe BOTH body (content growth) and the root
@@ -1038,6 +1048,12 @@ ${Q(a)}
1038
1048
  if (window.ResizeObserver) {
1039
1049
  try { var ro = new ResizeObserver(update); ro.observe(document.body); ro.observe(de); } catch (e) {}
1040
1050
  }
1051
+ // Load-failure backstop: if the entry module never evaluated \u2014 its import graph failed to
1052
+ // resolve (a 404'd dependency, which throws nothing catchable) \u2014 surface it. A module that
1053
+ // only renders asynchronously still evaluates its top level, so this won't fire on a slow bulb.
1054
+ if (!window.__tbEntryRan && !errorPosted) {
1055
+ postError('Bulb failed to load: a module or dependency could not be fetched (see the browser console for the failing URL).');
1056
+ }
1041
1057
  });
1042
1058
  })();
1043
1059
  <\/script>`;function swn(s){let o={};for(let[a,d]of Object.entries(s))o[a]=d.startsWith("https://")?"/proxy/"+d:d;return o}var Y$="https://esm.sh",X$="https://cdn.jsdelivr.net/npm/",zke="https://data.jsdelivr.com/v1/package/npm/";function HEt(s){let o=(s||"").replace(/^\/+/,"").replace(/\/+$/,"");return o?o.split("/"):[]}var Pf=class s{constructor(o,a,d){let g=typeof o=="string"?s.parse(o):o;this.name=g.name,this.version=qM(a??g.version),this.subpath=qM(d??g.subpath)}static parse(o){let a=HEt(o||"");if(!a.length)return new s({name:""});if(a[0].startsWith("@")){let g=a[0],[m,v]=QEt(a[1]??""),A=qM(a.slice(2).join("/"));return new s({name:`${g}/${m}`,version:v,subpath:A})}else{let[g,m]=QEt(a[0]),v=qM(a.slice(1).join("/"));return new s({name:g,version:m,subpath:v})}}static fromUrl(o){try{let a=new URL(o),d=new URL(Y$).host,g=new URL(X$).host;if(a.host===d){let m=HEt(a.pathname.replace(/^\/v\d+\//,"/"));if(!m.length)return;let v=m[0].startsWith("@")?`${m[0]}/${m[1]??""}`:m[0];return s.parse(v)}if(a.host===g){let m=a.pathname.split("/npm/")[1];if(!m)return;let v=m.split("/")[0]||"";return s.parse(v)}return}catch{return}}static versionFromUrl(o){return s.fromUrl(o)?.version}format(){let o=this.version?`${this.name}@${this.version}`:this.name;return this.subpath?`${o}/${this.subpath}`:o}root(){return this.name}static rootOf(o){return s.parse(o).name}withVersion(o){return new s({name:this.name,version:qM(o),subpath:this.subpath})}withPreferredVersion(o,a){let d=o||a;return d?this.withVersion(d):this}static isBare(o){if(!o||o.startsWith(".")||o.startsWith("/"))return!1;let a=o.toLowerCase();return!a.startsWith("http://")&&!a.startsWith("https://")}},qM=s=>s&&s.length?s:void 0,QEt=s=>{let o=s.indexOf("@");return o<0?[s,void 0]:[s.slice(0,o),qM(s.slice(o+1))]};async function gg(s){try{return await s()}catch{return}}var V$=class{constructor(o,a){this.cache=o,this.http=a,this.esmHost=Y$,this.jsDelivrBase=X$,this.jsDelivrMeta=zke,this.pinMs=1e4,this.versionsIndexMs=1440*60*1e3,this.metaTtlMs=10080*60*1e3,this.pinCache=new Map}normalizeRelative(o){let a=o||"";return a.startsWith("./")?a.slice(2):a.replace(/^\/+/,"")}ensureLeadingDotSlash(o){return o.startsWith("./")?o:`./${o}`}baseDir(o){let a=typeof o=="string"?new Pf(o):o;return`${this.jsDelivrBase}${a.name}${a.version?`@${a.version}`:""}/`}file(o,a){return new URL(this.normalizeRelative(a),this.baseDir(o)).toString()}packageJson(o){return this.file(o,"package.json")}buildEsmUrl(o,a={}){let{target:d="es2022",bundle:g=!1,external:m}=a,v=new URLSearchParams({target:d});return g&&v.append("bundle",""),m?.length&&v.append("external",m.join(",")),`${this.esmHost}/${o}?${v.toString()}`}async pinEsmUrl(o,a="es2022"){let d=this.buildEsmUrl(o,{target:a}),g=await gg(()=>this.http.head(d));return g?.ok?g.url||d:void 0}async resolveExactVersion(o){let a=Date.now(),d=this.pinCache.get(o);if(d&&a-d.ts<this.pinMs)return d.value;let g=await this.tryResolveFromUrls([this.buildEsmUrl(o),`${this.esmHost}/${o}`]);return this.pinCache.set(o,{value:g,ts:a}),g}async tryResolveFromUrls(o){for(let a of o){let d=await gg(()=>this.http.head(a)),g=this.parseVersionFromUrl(d?.url||a);if(g)return g}}async fetchVersionsIndex(o){if(await this.cache.isNegative(o))return;let a=await this.cache.getIndex(o);if(a&&Date.now()-a.updatedAt<this.versionsIndexMs)return{versions:a.versions,distTags:a.distTags};let d=await gg(()=>this.http.getJson(`${this.jsDelivrMeta}${encodeURIComponent(o)}`));if(!d?.versions?.length){await this.cache.recordNegative(o);return}await this.cache.clearNegative(o);let g=d.distTags&&Object.keys(d.distTags).length?d.distTags:void 0;return await this.cache.setIndex(o,d.versions,g),d}parseVersionFromUrl(o){let a=Pf.fromUrl(o)?.version;return a&&/\d+\.\d+\.\d+/.test(a)?a:void 0}async fetchPackageMeta(o,a){let d=await this.cache.getMeta(o,a);if(d&&Date.now()-d.updatedAt<this.metaTtlMs){let{dependencies:B,peerDependencies:L,peerDependenciesMeta:R}=d;return{name:o,version:a,dependencies:B,peerDependencies:L,peerDependenciesMeta:R}}let g=this.packageJson(new Pf(`${o}@${a}`)),m=await gg(()=>this.http.getJson(g));if(!m)return;let v=B=>B&&Object.keys(B).length?B:void 0,A=v(m.dependencies),I=v(m.peerDependencies),T=v(m.peerDependenciesMeta);return await this.cache.setMeta(o,a,A,I,T),{name:o,version:a,dependencies:A,peerDependencies:I,peerDependenciesMeta:T}}};var GEt=s=>s.startsWith("@types/"),Z$=s=>Object.keys(s?.peerDependencies||{}).filter(o=>!GEt(o)),Kke=s=>Object.keys(s?.dependencies||{}).filter(o=>!GEt(o)),own=s=>Z$(s).filter(o=>!s?.peerDependenciesMeta?.[o]?.optional),eH=class{constructor(o){this.cdn=o}async resolve(o,a){let d=await this.fetchMeta(o),{allRoots:g,autoAddedPeers:m}=await this.expandWithPeers(d,a),v=this.computeFlags(g);return{allRoots:g,flags:v,autoAddedPeers:m}}async fetchMeta(o){return Promise.all(o.map(async({name:a,version:d})=>({name:a,version:d,meta:await this.cdn.fetchPackageMeta(a,d)})))}async expandWithPeers(o,a){let d=new Map(o.map(m=>[m.name,m])),g=[];for(let m of o)for(let v of own(m.meta))!d.has(v)&&!g.some(A=>A.name===v)&&g.push({name:v,requiredBy:m.name});for(let{name:m,requiredBy:v}of g)try{let A=await a(m),I=await this.cdn.fetchPackageMeta(m,A);d.set(m,{name:m,version:A,meta:I})}catch(A){console.warn(`[typebulb] Failed to resolve peer "${m}" for "${v}":`,A)}return{allRoots:[...d.values()],autoAddedPeers:g.filter(m=>d.has(m.name))}}computeFlags(o){let a=new Set(o.flatMap(m=>Z$(m.meta))),d=new Map;for(let m of o)for(let v of Kke(m.meta))d.set(v,(d.get(v)||0)+1);let g=new Set([...d.entries()].filter(([,m])=>m>=2).map(([m])=>m));return new Map(o.map(m=>[m.name,{isPeerRoot:a.has(m.name),hasPeers:Z$(m.meta).length>0,isSharedDep:g.has(m.name)}]))}};var tH=class{constructor(o,a,d){this.cache=o,this.cdn=a,this.semver=d}selectVersionFromIndex(o,a,d){return this.semver.selectBestVersion(o,{range:a,distTags:d})}async learnExactVersion(o){let a=await gg(()=>this.cdn.fetchVersionsIndex(o));if(a?.versions?.length){let d=this.semver.selectBestVersion(a.versions,{distTags:a.distTags});if(d)return d}return this.cdn.resolveExactVersion(o)}async resolveExactForRoot(o,a){if(!a)return this.learnExactVersion(o);let d=await this.cache.getPinnedExact(o,a);if(d){if(this.semver.isExactVersion(d))return d;console.debug("[typebulb] cached version for",o,"is not exact (",d,"); re-resolving from registry")}let g=await gg(()=>this.cdn.fetchVersionsIndex(o));if(g?.versions?.length){let v=this.selectVersionFromIndex(g.versions,a,g.distTags);if(v){if(this.semver.isExactVersion(v))return await this.cache.setPinnedExact(o,a,v),v}else{console.debug("[typebulb] refreshing version cache for",o,"(",a,"not in cached set \u2014 likely a new release)"),await this.cache.invalidateVersionsCache(o);let A=await gg(()=>this.cdn.fetchVersionsIndex(o));if(A?.versions?.length){let I=this.selectVersionFromIndex(A.versions,a,A.distTags);if(I&&this.semver.isExactVersion(I))return await this.cache.setPinnedExact(o,a,I),I}}}let m=await this.cdn.resolveExactVersion(`${o}@${a}`);if(m&&this.semver.isExactVersion(m))return await this.cache.setPinnedExact(o,a,m),m}async effectivePackage(o,a){let d=new Pf(o),g=d.root(),m=a[g],v=m?await gg(()=>this.cache.getPinnedExact(g,m))??await gg(()=>this.resolveExactForRoot(g,m)):void 0;return{effectivePackage:v?d.withVersion(v).format():o,root:g,range:m,pinned:v}}};var qEt;(function(s){s[s.Static=1]="Static",s[s.Dynamic=2]="Dynamic",s[s.ImportMeta=3]="ImportMeta",s[s.StaticSourcePhase=4]="StaticSourcePhase",s[s.DynamicSourcePhase=5]="DynamicSourcePhase",s[s.StaticDeferPhase=6]="StaticDeferPhase",s[s.DynamicDeferPhase=7]="DynamicDeferPhase"})(qEt||(qEt={}));var cwn=new Uint8Array(new Uint16Array([1]).buffer)[0]===1;function Wke(s,o="@"){if(!Au)return Yke.then((()=>Wke(s)));let a=s.length+1,d=(Au.__heap_base.value||Au.__heap_base)+4*a-Au.memory.buffer.byteLength;d>0&&Au.memory.grow(Math.ceil(d/65536));let g=Au.sa(a-1);if((cwn?awn:uwn)(s,new Uint16Array(Au.memory.buffer,g,a)),!Au.parse())throw Object.assign(new Error(`Parse error ${o}:${s.slice(0,Au.e()).split(`
@@ -272,9 +272,10 @@ body {
272
272
  color: var(--accent); text-decoration: none; cursor: pointer;
273
273
  }
274
274
  .server-name:hover { text-decoration: underline; }
275
- /* Port is a link to the running server (localhost:<port>), same target as the name. */
276
- .server-port { color: var(--muted); font-size: .76rem; font-variant-numeric: tabular-nums; text-decoration: none; text-align: right; margin-right: .4rem; }
277
- a.server-port:hover { color: var(--accent); text-decoration: underline; }
275
+ /* Port links to the running server (localhost:<port>) accent like the name, so a running
276
+ bulb's clickable :port reads as live against a stopped bulb's muted last-run time. */
277
+ .server-port { color: var(--accent); font-size: .76rem; font-variant-numeric: tabular-nums; text-decoration: none; text-align: right; margin-right: .4rem; }
278
+ a.server-port:hover { text-decoration: underline; }
278
279
  /* Logs reads like a link (flips to this server's console), not a button. */
279
280
  .server-logs { color: var(--muted); font-size: .76rem; text-decoration: none; cursor: pointer; margin-left: .5rem; }
280
281
  .server-logs:hover, .server-logs.on { color: var(--accent); text-decoration: underline; }