codebay 0.2.0 → 0.3.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 (43) hide show
  1. package/.mochi/manifest.json +144 -140
  2. package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-faqf4hmc.js +2 -0
  4. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-3w16y9jh.js +13 -0
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-5g8meb6a.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-jsw4edxb.js +23 -0
  7. package/.mochi/svelte-client/chunk-04qv4w6w.js +2 -0
  8. package/.mochi/svelte-client/{chunk-x843txjc.js → chunk-0jtmkpx4.js} +2 -2
  9. package/.mochi/svelte-client/chunk-597d17x0.js +1 -0
  10. package/.mochi/svelte-client/chunk-5p31aqh0.js +2 -0
  11. package/.mochi/svelte-client/chunk-gjjhya9m.js +2 -0
  12. package/.mochi/svelte-client/{chunk-kgn1b2t1.js → chunk-qxwt25hn.js} +1 -1
  13. package/.mochi/svelte-compile/{App-8k1gyxdc.server.js → App-f0q5n4rk.server.js} +3 -2
  14. package/.mochi/svelte-compile/{Avatars-f7c6rp96.server.js → Avatars-p8gfef7x.server.js} +35 -54
  15. package/.mochi/svelte-compile/{Instance-yvps12zg.server.js → Instance-atjwsczx.server.js} +3 -2
  16. package/.mochi/svelte-compile/{Settings-jwg3httb.server.js → Settings-q88xw6h8.server.js} +9 -5
  17. package/.mochi/svelte-compile/{chunk-dqgstte4.js → chunk-btmcxhqy.js} +7 -1
  18. package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +1 -0
  19. package/README.md +1 -0
  20. package/bin/codebay.ts +42 -1
  21. package/package.json +1 -1
  22. package/src/avatars/name-prompts.ts +30 -0
  23. package/src/components/AvatarEditor.svelte +3 -27
  24. package/src/components/AvatarsView.svelte +3 -1
  25. package/src/components/InstanceView.svelte +4 -2
  26. package/src/components/PortsBox.svelte +3 -2
  27. package/src/components/SettingsView.svelte +11 -1
  28. package/src/lib/config.server.ts +18 -0
  29. package/src/lib/devcontainer.server.ts +10 -2
  30. package/src/lib/links.ts +9 -0
  31. package/src/pages/Avatars.svelte +3 -1
  32. package/src/pages/Settings.svelte +4 -1
  33. package/src/routes.ts +9 -5
  34. package/.mochi/svelte-client/HydratableIsland-ps09gjy2.js +0 -1
  35. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-62w5zwww.js +0 -2
  36. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jmm501zf.js +0 -13
  37. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-ewk4n2c0.js +0 -1
  38. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-e7e89md0.js +0 -23
  39. package/.mochi/svelte-client/chunk-jx5cekhh.js +0 -2
  40. package/.mochi/svelte-client/chunk-k3my1snp.js +0 -2
  41. package/.mochi/svelte-client/chunk-ry7q5zk5.js +0 -1
  42. package/.mochi/svelte-client/chunk-zjcg8kw5.js +0 -2
  43. package/.mochi/svelte-css/SettingsView-3bbkzsvnrth3v.css +0 -1
@@ -13,7 +13,8 @@ import {
13
13
  } from "./chunk-sxqys3nw.js";
14
14
  import {
15
15
  HydratableBoundary,
16
- Icon
16
+ Icon,
17
+ emitIslandProps
17
18
  } from "./chunk-v6796ga4.js";
18
19
  import"./chunk-we0ggtws.js";
19
20
 
@@ -146,31 +147,7 @@ function toIssueUrl(art) {
146
147
  // src/components/AvatarEditor.svelte
147
148
  function AvatarEditor($$renderer, $$props) {
148
149
  $$renderer.component(($$renderer2) => {
149
- const NAME_PROMPTS = [
150
- "dragon",
151
- "unicorn",
152
- "cactus",
153
- "comet",
154
- "pretzel",
155
- "volcano",
156
- "narwhal",
157
- "waffle",
158
- "satellite",
159
- "pineapple",
160
- "kraken",
161
- "yeti",
162
- "moose",
163
- "lighthouse",
164
- "toaster",
165
- "hedgehog",
166
- "walrus",
167
- "pumpkin",
168
- "jellyfish",
169
- "tornado",
170
- "igloo",
171
- "boomerang"
172
- ];
173
- const namePlaceholder = NAME_PROMPTS[Math.floor(Math.random() * NAME_PROMPTS.length)];
150
+ let { namePlaceholder } = $$props;
174
151
  let cells = Array(ROWS * COLS).fill(OFF);
175
152
  let avatarName = "";
176
153
  const slug = $5.derived(() => normalizeName(avatarName));
@@ -223,7 +200,7 @@ function AvatarEditor($$renderer, $$props) {
223
200
  dist_default.error(`Copy failed: ${err.message}`);
224
201
  }
225
202
  }
226
- $$renderer2.push(`<section class="editor svelte-qu6vp4"><div class="head svelte-qu6vp4"><h2 class="svelte-qu6vp4">Draw your avatar</h2></div> <div class="body svelte-qu6vp4"><div class="grid-frame svelte-qu6vp4"><!--[-->`);
203
+ $$renderer2.push(`<section class="editor svelte-qu6vp4"><div class="head svelte-qu6vp4"><h2 class="svelte-qu6vp4">Draw your own avatar</h2></div> <div class="body svelte-qu6vp4"><div class="grid-frame svelte-qu6vp4"><!--[-->`);
227
204
  const each_array = $5.ensure_array_like(RING_CELLS);
228
205
  for (let i = 0, $$length = each_array.length;i < $$length; i++) {
229
206
  let { r, c } = each_array[i];
@@ -291,6 +268,7 @@ function AvatarEditor($$renderer, $$props) {
291
268
  // src/components/AvatarsView.svelte
292
269
  function AvatarsView($$renderer, $$props) {
293
270
  $$renderer.component(($$renderer2) => {
271
+ let { namePlaceholder } = $$props;
294
272
  $$renderer2.push(`<div class="page svelte-fkq7sr">`);
295
273
  AppBar($$renderer2, {
296
274
  children: ($$renderer3) => {
@@ -299,7 +277,7 @@ function AvatarsView($$renderer, $$props) {
299
277
  $$slots: { default: true }
300
278
  });
301
279
  $$renderer2.push(`<!----> <main class="content svelte-fkq7sr">`);
302
- AvatarEditor($$renderer2, {});
280
+ AvatarEditor($$renderer2, { namePlaceholder });
303
281
  $$renderer2.push(`<!----> <section class="catalog svelte-fkq7sr"><header class="svelte-fkq7sr"><h2 class="svelte-fkq7sr">The set</h2> <p class="svelte-fkq7sr">${$6.escape(avatars.length)} hand-crafted 8\xD78 sprites</p></header> <ul class="svelte-fkq7sr"><!--[-->`);
304
282
  const each_array = $6.ensure_array_like(avatars);
305
283
  for (let $$index = 0, $$length = each_array.length;$$index < $$length; $$index++) {
@@ -319,32 +297,35 @@ function AvatarsView($$renderer, $$props) {
319
297
  }
320
298
 
321
299
  // src/pages/Avatars.svelte
322
- function Avatars($$renderer) {
323
- if (true) {
324
- let failed = function($$renderer2, error) {
325
- $$renderer2.push(`<mochi-island-failure data-component="AvatarsView"${$7.attr("data-message", error.message)}></mochi-island-failure>`);
326
- };
327
- $$renderer.push("<!--[0-->");
328
- $$renderer.boundary({ failed }, ($$renderer2) => {
329
- $$renderer2.push(`<!--[-->`);
330
- {
331
- HydratableBoundary($$renderer2, {
332
- children: ($$renderer3) => {
333
- $$renderer3.push(`<mochi-hydratable-island component-name="AvatarsView_257s9yq1wwr4f" component-url="__MOCHI_COMPONENT_URL__AvatarsView_257s9yq1wwr4f__">`);
334
- $$renderer3.push(`<!--[-->`);
335
- {
336
- AvatarsView($$renderer3, {});
337
- }
338
- $$renderer3.push(`<!--]-->`);
339
- $$renderer3.push(`</mochi-hydratable-island>`);
340
- },
341
- $$slots: { default: true }
342
- });
343
- }
344
- $$renderer2.push(`<!--]-->`);
345
- });
346
- }
347
- $$renderer.push(`<!--]-->`);
300
+ function Avatars($$renderer, $$props) {
301
+ $$renderer.component(($$renderer2) => {
302
+ let { namePlaceholder } = $$props;
303
+ if (true) {
304
+ let failed = function($$renderer3, error) {
305
+ $$renderer3.push(`<mochi-island-failure data-component="AvatarsView"${$7.attr("data-message", error.message)}></mochi-island-failure>`);
306
+ };
307
+ $$renderer2.push("<!--[0-->");
308
+ $$renderer2.boundary({ failed }, ($$renderer3) => {
309
+ $$renderer3.push(`<!--[-->`);
310
+ {
311
+ HydratableBoundary($$renderer3, {
312
+ children: ($$renderer4) => {
313
+ $$renderer4.push(`<mochi-hydratable-island component-name="AvatarsView_257s9yq1wwr4f" component-url="__MOCHI_COMPONENT_URL__AvatarsView_257s9yq1wwr4f__"${$7.attr("props-ref", emitIslandProps({ namePlaceholder }))}>`);
314
+ $$renderer4.push(`<!--[-->`);
315
+ {
316
+ AvatarsView($$renderer4, { namePlaceholder });
317
+ }
318
+ $$renderer4.push(`<!--]-->`);
319
+ $$renderer4.push(`</mochi-hydratable-island>`);
320
+ },
321
+ $$slots: { default: true }
322
+ });
323
+ }
324
+ $$renderer3.push(`<!--]-->`);
325
+ });
326
+ }
327
+ $$renderer2.push(`<!--]-->`);
328
+ });
348
329
  }
349
330
  export {
350
331
  Avatars as default
@@ -3,8 +3,9 @@ import {
3
3
  StatusBadge,
4
4
  apiDelete,
5
5
  apiPost,
6
+ forwardedPortUrl,
6
7
  ideUrl
7
- } from "./chunk-dqgstte4.js";
8
+ } from "./chunk-btmcxhqy.js";
8
9
  import {
9
10
  X
10
11
  } from "./chunk-4kwy539a.js";
@@ -285,7 +286,7 @@ function InstanceView($$renderer, $$props) {
285
286
  const each_array = $6.ensure_array_like(forwards());
286
287
  for (let $$index = 0, $$length = each_array.length;$$index < $$length; $$index++) {
287
288
  let f = each_array[$$index];
288
- $$renderer2.push(`<li class="svelte-15fxkvm"><span class="cp svelte-15fxkvm">:${$6.escape(f.container_port)}</span> <span class="arr svelte-15fxkvm">\u2192</span> <a${$6.attr("href", `http://localhost:${f.host_port}`)} target="_blank" rel="noopener" class="svelte-15fxkvm">localhost:${$6.escape(f.host_port)} `);
289
+ $$renderer2.push(`<li class="svelte-15fxkvm"><span class="cp svelte-15fxkvm">:${$6.escape(f.container_port)}</span> <span class="arr svelte-15fxkvm">\u2192</span> <a${$6.attr("href", forwardedPortUrl(f.host_port))} target="_blank" rel="noopener" class="svelte-15fxkvm">${$6.escape(forwardedPortUrl(f.host_port).replace("http://", ""))} `);
289
290
  Arrow_up_right($$renderer2, { size: 12 });
290
291
  $$renderer2.push(`<!----></a> <button class="rm svelte-15fxkvm" title="Remove">\xD7</button></li>`);
291
292
  }
@@ -367,7 +367,8 @@ function SettingsView($$renderer, $$props) {
367
367
  customEndpointModel,
368
368
  hostEnvVarsEnabled,
369
369
  hostEnvVarNames,
370
- hostEnvVarPresence
370
+ hostEnvVarPresence,
371
+ version
371
372
  } = $$props;
372
373
  let sound = soundEnabled();
373
374
  let shuttingDown = false;
@@ -1060,7 +1061,7 @@ function SettingsView($$renderer, $$props) {
1060
1061
  }
1061
1062
  $$renderer2.push(`<!--]--></form></section> `);
1062
1063
  CoinButton($$renderer2, {});
1063
- $$renderer2.push(`<!----></main></div> `);
1064
+ $$renderer2.push(`<!----> <div class="version svelte-1n6ht58">Codebay v${$17.escape(version)}</div></main></div> `);
1064
1065
  Toaster($$renderer2, {
1065
1066
  toastOptions: {
1066
1067
  style: "border:1px solid var(--ink); background:var(--bg-card); color:var(--ink); box-shadow:4px 4px 0 var(--ink); font-family:var(--font-mono); font-size:13px;"
@@ -1093,7 +1094,8 @@ function Settings($$renderer, $$props) {
1093
1094
  customEndpointModel,
1094
1095
  hostEnvVarsEnabled,
1095
1096
  hostEnvVarNames,
1096
- hostEnvVarPresence
1097
+ hostEnvVarPresence,
1098
+ version
1097
1099
  } = $$props;
1098
1100
  if (true) {
1099
1101
  let failed = function($$renderer3, error) {
@@ -1125,7 +1127,8 @@ function Settings($$renderer, $$props) {
1125
1127
  customEndpointModel,
1126
1128
  hostEnvVarsEnabled,
1127
1129
  hostEnvVarNames,
1128
- hostEnvVarPresence
1130
+ hostEnvVarPresence,
1131
+ version
1129
1132
  }))}>`);
1130
1133
  $$renderer4.push(`<!--[-->`);
1131
1134
  {
@@ -1149,7 +1152,8 @@ function Settings($$renderer, $$props) {
1149
1152
  customEndpointModel,
1150
1153
  hostEnvVarsEnabled,
1151
1154
  hostEnvVarNames,
1152
- hostEnvVarPresence
1155
+ hostEnvVarPresence,
1156
+ version
1153
1157
  });
1154
1158
  }
1155
1159
  $$renderer4.push(`<!--]-->`);
@@ -35,6 +35,12 @@ function apiJson(method, url, body, fallbackMessage) {
35
35
  }, fallbackMessage);
36
36
  }
37
37
 
38
+ // src/lib/links.ts
39
+ function forwardedPortUrl(hostPort) {
40
+ const host = typeof window === "undefined" ? "localhost" : window.location.hostname;
41
+ return `http://${host}:${hostPort}`;
42
+ }
43
+
38
44
  // src/components/StatusBadge.svelte
39
45
  import"svelte/internal/flags/async";
40
46
  import * as $ from "svelte/internal/server";
@@ -49,4 +55,4 @@ function StatusBadge($$renderer, $$props) {
49
55
  $$renderer.push(`<span${$.attr_class(`status ${$.stringify(status)}`, "svelte-1hqr9xd")}>${$.escape(label[status])}</span>`);
50
56
  }
51
57
 
52
- export { ideUrl, apiFetch, apiPost, apiDelete, StatusBadge };
58
+ export { ideUrl, apiFetch, apiPost, apiDelete, forwardedPortUrl, StatusBadge };
@@ -0,0 +1 @@
1
+ .page.svelte-1n6ht58{display:flex;flex-direction:column;min-height:100vh}.title.svelte-1n6ht58{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-soft);align-items: center;padding:0 14px;font-size:12px}.content.svelte-1n6ht58{display:flex;flex-direction:column;flex:1;align-items: center;gap:16px;padding:32px 20px}.card.svelte-1n6ht58{background:var(--bg-card);border:1px solid var(--rule);width:100%;max-width:560px;height:max-content}.danger-card.svelte-1n6ht58{border-color:var(--danger)}.disabled-card.svelte-1n6ht58{opacity:.55;pointer-events:none}.danger-card.svelte-1n6ht58 .btn{white-space:nowrap;flex:none}.version.svelte-1n6ht58{font-family:var(--font-mono);letter-spacing:.08em;color:var(--ink-soft);font-size:11px}.shutting.svelte-1n6ht58{font-family:var(--font-mono);color:var(--danger);text-align:right;flex:none;max-width:200px;font-size:12px;line-height:1.4}.row.svelte-1n6ht58{display:flex;justify-content:space-between;align-items: center;gap:16px;padding:18px}.row.divided.svelte-1n6ht58{border-top:1px solid var(--rule);align-items: flex-start}.row.divided.svelte-1n6ht58 .btn{white-space:nowrap;flex:none}.sub.svelte-1n6ht58{padding:0 18px 14px}code.svelte-1n6ht58{font-family:var(--font-mono);background:var(--bg);border:1px solid var(--rule);border-radius:3px;padding:1px 4px;font-size:.92em}.label.svelte-1n6ht58{display:flex;color:var(--ink);align-items: flex-start;gap:12px;min-width:0}.text.svelte-1n6ht58{min-width:0}.name.svelte-1n6ht58{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;font-size:13px}.arch.svelte-1n6ht58{letter-spacing:.04em;color:var(--ink-soft);border:1px solid var(--rule);vertical-align:middle;border-radius:3px;margin-left:8px;padding:2px 6px;font-size:11px}.tip.svelte-1n6ht58{color:var(--ink-soft);background:color-mix(in srgb,var(--info)10%,transparent);border:1px solid color-mix(in srgb,var(--info)35%,transparent);border-radius:4px;width:100%;margin-top:4px;padding:8px 10px}.desc.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink-faint);margin-top:4px;font-size:12px;line-height:1.4}.image-row.svelte-1n6ht58,.token-row.svelte-1n6ht58{flex-wrap:wrap}.image-controls.svelte-1n6ht58{display:flex;flex:1;justify-content:flex-end;align-items: center;gap:8px;min-width:220px}.image-input.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink);background:var(--bg);border:1px solid var(--rule);flex:1;min-width:0;padding:8px 10px;font-size:12px}.image-input.svelte-1n6ht58:focus-visible{outline:2px solid var(--ink);outline-offset:1px}.image-controls.svelte-1n6ht58 .btn{flex:none}.msg.svelte-1n6ht58{font-family:var(--font-mono);width:100%;margin-top:2px;font-size:12px;line-height:1.4}.msg.error.svelte-1n6ht58{color:var(--danger)}.msg.ok.svelte-1n6ht58{color:var(--ink-soft)}.switch.svelte-1n6ht58{position:relative;cursor:pointer;flex:none}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58){position:absolute;opacity:0;cursor:pointer;margin:0;inset:0}.track.svelte-1n6ht58{display:block;border:1px solid var(--rule);background:var(--bg);border-radius:999px;width:44px;height:24px;transition:background .15s}.thumb.svelte-1n6ht58{display:block;background:var(--ink-faint);border-radius:999px;width:18px;height:18px;margin:2px;transition:transform .15s,background .15s}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58):checked+.track:where(.svelte-1n6ht58){background:var(--switch-on-bg)}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58):checked+.track:where(.svelte-1n6ht58) .thumb:where(.svelte-1n6ht58){background:var(--ink);transform:translate(20px)}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58):focus-visible+.track:where(.svelte-1n6ht58){outline:2px solid var(--ink);outline-offset:2px}.model-fields.svelte-1n6ht58{display:flex;flex-direction:column;flex:1;gap:6px;min-width:220px}.model-row.svelte-1n6ht58{display:flex;align-items: center;gap:8px}.model-label.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink-soft);text-align:right;flex:none;width:72px;font-size:11px}.model-row.svelte-1n6ht58 .image-input:where(.svelte-1n6ht58){flex:1}.model-save-row.svelte-1n6ht58{display:flex;justify-content:flex-end;margin-top:4px}.var-row.svelte-1n6ht58{flex-direction:column;align-items:stretch;gap:10px}.var-list.svelte-1n6ht58{display:flex;flex-direction:column;gap:6px}.var-item.svelte-1n6ht58{display:flex;background:var(--bg);border:1px solid var(--rule);align-items: center;gap:8px;padding:6px 8px}.var-name.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink);word-break:break-all;flex:1;min-width:0;font-size:12px}.var-status.svelte-1n6ht58{font-family:var(--font-mono);letter-spacing:.04em;color:var(--danger);flex:none;font-size:11px}.var-status.present.svelte-1n6ht58{color:var(--ink-soft)}.var-remove.svelte-1n6ht58{display:inline-flex;color:var(--ink-soft);border:1px solid var(--rule);cursor:pointer;background:0 0;flex:none;justify-content:center;align-items: center;width:22px;height:22px;padding:0}.var-remove.svelte-1n6ht58:hover:not(:disabled){color:var(--danger);border-color:var(--danger)}.var-remove.svelte-1n6ht58:disabled{opacity:.4;cursor:not-allowed}.var-empty.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink-faint);font-size:12px}.var-add.svelte-1n6ht58{display:flex;gap:8px}.var-add.svelte-1n6ht58 .btn{flex:none}
package/README.md CHANGED
@@ -17,6 +17,7 @@ The UI opens at `http://localhost:6969`. State (SQLite DB + per-instance workspa
17
17
  - `PORT` — server port (default `6969`)
18
18
  - `DATA_DIR` — where state lives (default `~/.codebay`)
19
19
  - `DOCKER_HOST` — Docker socket/URL (defaults to your active Docker context)
20
+ - `HOST` — bind address (default `127.0.0.1`). Set `0.0.0.0` to reach codebay from other machines; your instances' forwarded app ports are then published on all interfaces too. Each container's code-server port stays loopback-only regardless — it runs without a password of its own and is reached through the Basic-Auth-gated `/p/:id/` proxy instead. Existing instances need a **Restart** to pick up the new binding.
20
21
  - `BASIC_AUTH_PASSWORD` — enables HTTP Basic Auth over the whole UI (disabled when unset); required if you bind beyond loopback with `HOST=0.0.0.0`
21
22
  - `CODEBAY_CLAUDE_CODE_TOKEN` — Claude Code token to inject into every container (e.g. from `claude setup-token`) instead of discovering the host's credentials
22
23
  - `CODEBAY_GITHUB_TOKEN` — GitHub token to inject instead of reading `gh auth token` from the host
package/bin/codebay.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  // ./public, and the .mochi manifest relative to process.cwd(), so chdir into the
4
4
  // package root before booting. Dynamic import() of a relative specifier resolves
5
5
  // against this module's URL (not cwd), so the chdir doesn't affect it.
6
+ import { readFileSync } from 'node:fs';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { dirname, isAbsolute, join, resolve } from 'node:path';
8
9
 
@@ -11,6 +12,46 @@ if (typeof Bun === 'undefined') {
11
12
  process.exit(1);
12
13
  }
13
14
 
15
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
16
+
17
+ function readVersion(): string {
18
+ try {
19
+ return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version ?? 'unknown';
20
+ } catch {
21
+ return 'unknown';
22
+ }
23
+ }
24
+
25
+ const args = process.argv.slice(2);
26
+
27
+ if (args.includes('--version') || args.includes('-v')) {
28
+ console.log(readVersion());
29
+ process.exit(0);
30
+ }
31
+
32
+ if (args.includes('--help') || args.includes('-h')) {
33
+ console.log(`codebay ${readVersion()} — spin up isolated devcontainers with a browser IDE.
34
+
35
+ Usage: bunx codebay [options]
36
+
37
+ Starts the web UI (default http://localhost:6969) and opens it in your browser.
38
+
39
+ Options:
40
+ -h, --help Show this help and exit
41
+ -v, --version Print the version and exit
42
+
43
+ Environment variables:
44
+ PORT Server port (default 6969)
45
+ DATA_DIR Where state lives (default ~/.codebay)
46
+ DOCKER_HOST Docker socket/URL (defaults to your active Docker context)
47
+ HOST Bind address (default 127.0.0.1; set 0.0.0.0 for LAN access)
48
+ BASIC_AUTH_PASSWORD Enable HTTP Basic Auth over the whole UI (required for 0.0.0.0)
49
+ CODEBAY_CLAUDE_CODE_TOKEN Claude Code token to inject into every container
50
+ CODEBAY_GITHUB_TOKEN GitHub token to inject into every container
51
+ DISABLE_OPEN_BROWSER=1 Skip opening the browser on startup`);
52
+ process.exit(0);
53
+ }
54
+
14
55
  // config.server.ts resolves a relative DATA_DIR against process.cwd(). Pin it to the
15
56
  // directory the user actually ran the command from, before the chdir below moves cwd
16
57
  // into the (possibly npm-cached) package root.
@@ -18,5 +59,5 @@ if (process.env.DATA_DIR && !isAbsolute(process.env.DATA_DIR)) {
18
59
  process.env.DATA_DIR = resolve(process.cwd(), process.env.DATA_DIR);
19
60
  }
20
61
 
21
- process.chdir(join(dirname(fileURLToPath(import.meta.url)), '..'));
62
+ process.chdir(pkgRoot);
22
63
  await import('../src/index.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebay",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Web UI for spinning up isolated devcontainer instances from any local folder, each with a browser-based VS Code (code-server) — built for parallel Claude Code workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,30 @@
1
+ // Lives outside the editor component so the route can roll the hint server-side —
2
+ // rolling it during component init would re-roll it again at hydration.
3
+ export const NAME_PROMPTS = [
4
+ 'dragon',
5
+ 'unicorn',
6
+ 'cactus',
7
+ 'comet',
8
+ 'pretzel',
9
+ 'volcano',
10
+ 'narwhal',
11
+ 'waffle',
12
+ 'satellite',
13
+ 'pineapple',
14
+ 'kraken',
15
+ 'yeti',
16
+ 'moose',
17
+ 'lighthouse',
18
+ 'toaster',
19
+ 'hedgehog',
20
+ 'walrus',
21
+ 'pumpkin',
22
+ 'jellyfish',
23
+ 'tornado',
24
+ 'igloo',
25
+ 'boomerang'
26
+ ];
27
+
28
+ export function pickNamePrompt(): string {
29
+ return NAME_PROMPTS[Math.floor(Math.random() * NAME_PROMPTS.length)]!;
30
+ }
@@ -14,32 +14,8 @@
14
14
  import Avatar from './Avatar.svelte';
15
15
  import Button from './Button.svelte';
16
16
 
17
- // Picked randomly so the hint varies each time the page loads.
18
- const NAME_PROMPTS = [
19
- 'dragon',
20
- 'unicorn',
21
- 'cactus',
22
- 'comet',
23
- 'pretzel',
24
- 'volcano',
25
- 'narwhal',
26
- 'waffle',
27
- 'satellite',
28
- 'pineapple',
29
- 'kraken',
30
- 'yeti',
31
- 'moose',
32
- 'lighthouse',
33
- 'toaster',
34
- 'hedgehog',
35
- 'walrus',
36
- 'pumpkin',
37
- 'jellyfish',
38
- 'tornado',
39
- 'igloo',
40
- 'boomerang'
41
- ];
42
- const namePlaceholder = NAME_PROMPTS[Math.floor(Math.random() * NAME_PROMPTS.length)];
17
+ // Rolled by the route, not here, so hydration doesn't pick a different word.
18
+ let { namePlaceholder }: { namePlaceholder: string } = $props();
43
19
 
44
20
  // Same shape `decode()` produces, so the two round-trip.
45
21
  let cells = $state<number[]>(Array(ROWS * COLS).fill(OFF));
@@ -109,7 +85,7 @@
109
85
 
110
86
  <section class="editor">
111
87
  <div class="head">
112
- <h2>Draw your avatar</h2>
88
+ <h2>Draw your own avatar</h2>
113
89
  </div>
114
90
 
115
91
  <div class="body">
@@ -4,6 +4,8 @@
4
4
  import Avatar from './Avatar.svelte';
5
5
  import AvatarEditor from './AvatarEditor.svelte';
6
6
  import { avatars } from '../avatars/index.ts';
7
+
8
+ let { namePlaceholder }: { namePlaceholder: string } = $props();
7
9
  </script>
8
10
 
9
11
  <div class="page">
@@ -12,7 +14,7 @@
12
14
  </AppBar>
13
15
 
14
16
  <main class="content">
15
- <AvatarEditor />
17
+ <AvatarEditor {namePlaceholder} />
16
18
 
17
19
  <section class="catalog">
18
20
  <header>
@@ -5,6 +5,7 @@
5
5
  import HealthBox from './HealthBox.svelte';
6
6
  import StatusBadge from './StatusBadge.svelte';
7
7
  import Skeleton from './Skeleton.svelte';
8
+ import { forwardedPortUrl } from '../lib/links.ts';
8
9
  import { liveSocket, liveStream } from '../live.ts';
9
10
  import { apiPost, apiDelete } from '../api.ts';
10
11
  import { enhance } from 'mochi-framework';
@@ -189,8 +190,9 @@
189
190
  <li>
190
191
  <span class="cp">:{f.container_port}</span>
191
192
  <span class="arr">→</span>
192
- <a href={`http://localhost:${f.host_port}`} target="_blank" rel="noopener"
193
- >localhost:{f.host_port} <ArrowUpRight size={12} /></a
193
+ <a href={forwardedPortUrl(f.host_port)} target="_blank" rel="noopener"
194
+ >{forwardedPortUrl(f.host_port).replace('http://', '')}
195
+ <ArrowUpRight size={12} /></a
194
196
  >
195
197
  <button class="rm" title="Remove" onclick={() => removePort(f.container_port)}
196
198
  >×</button
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Cable from '@lucide/svelte/icons/cable';
3
+ import { forwardedPortUrl } from '../lib/links.ts';
3
4
  import type { PortForward } from '../types.ts';
4
5
 
5
6
  let { ports }: { ports: PortForward[] } = $props();
@@ -13,10 +14,10 @@
13
14
  <a
14
15
  class="fport"
15
16
  class:open={f.open}
16
- href={`http://localhost:${f.host_port}`}
17
+ href={forwardedPortUrl(f.host_port)}
17
18
  target="_blank"
18
19
  rel="noopener"
19
- title={`container :${f.container_port} → http://localhost:${f.host_port} ${f.open ? '(open)' : '(not published yet)'}`}
20
+ title={`container :${f.container_port} → ${forwardedPortUrl(f.host_port)} ${f.open ? '(open)' : '(not published yet)'}`}
20
21
  >{f.container_port}</a
21
22
  >
22
23
  {/each}
@@ -46,7 +46,8 @@
46
46
  customEndpointModel,
47
47
  hostEnvVarsEnabled,
48
48
  hostEnvVarNames,
49
- hostEnvVarPresence
49
+ hostEnvVarPresence,
50
+ version
50
51
  }: {
51
52
  defaultImage: string;
52
53
  builtinImage: string;
@@ -68,6 +69,7 @@
68
69
  hostEnvVarsEnabled: boolean;
69
70
  hostEnvVarNames: string[];
70
71
  hostEnvVarPresence: Record<string, boolean>;
72
+ version: string;
71
73
  } = $props();
72
74
 
73
75
  // Defaults to on during SSR, where localStorage doesn't exist.
@@ -1164,6 +1166,8 @@
1164
1166
  </section>
1165
1167
 
1166
1168
  <CoinButton />
1169
+
1170
+ <div class="version">Codebay v{version}</div>
1167
1171
  </main>
1168
1172
  </div>
1169
1173
 
@@ -1218,6 +1222,12 @@
1218
1222
  flex: none;
1219
1223
  white-space: nowrap;
1220
1224
  }
1225
+ .version {
1226
+ font-family: var(--font-mono);
1227
+ font-size: 11px;
1228
+ letter-spacing: 0.08em;
1229
+ color: var(--ink-soft);
1230
+ }
1221
1231
  .shutting {
1222
1232
  flex: none;
1223
1233
  max-width: 200px;
@@ -37,6 +37,17 @@ export function ensureMochiKey(): void {
37
37
  }
38
38
  }
39
39
 
40
+ /** Read relative to this module, not cwd, because under `bunx codebay` cwd is the user's folder. */
41
+ export const APP_VERSION: string = (() => {
42
+ try {
43
+ const path = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
44
+ const version = JSON.parse(readFileSync(path, 'utf8')).version;
45
+ return typeof version === 'string' ? version : 'unknown';
46
+ } catch {
47
+ return 'unknown';
48
+ }
49
+ })();
50
+
40
51
  /** Per-instance working copies live here: <INSTANCES_DIR>/<id>/workspace. */
41
52
  export const INSTANCES_DIR = join(DATA_DIR, 'instances');
42
53
 
@@ -58,6 +69,13 @@ export const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || '';
58
69
  /** Loopback by default so an instance with no password isn't exposed to the LAN. */
59
70
  export const HOST = process.env.HOST || '127.0.0.1';
60
71
 
72
+ /**
73
+ * Forwarded app ports follow the server's bind; code-server's own port never does
74
+ * (it runs with auth disabled). Anything but `0.0.0.0` degrades to loopback rather
75
+ * than risking an appPort string Docker rejects.
76
+ */
77
+ export const PUBLISH_HOST = HOST === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1';
78
+
61
79
  export const PORT = Number(process.env.PORT) || 6969;
62
80
 
63
81
  /** Mochi's CSRF origin check compares against these; override when behind a proxy. */
@@ -3,7 +3,13 @@ import { existsSync, statSync } from 'node:fs';
3
3
  import { basename, dirname, join, relative } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { INSTALL_SCRIPT as TMUX_INSTALL_SCRIPT } from '../container-injections/tmux.ts';
6
- import { CODE_SERVER_PORT, DEFAULT_IMAGE, devcontainerBin, dockerEnv } from './config.server.ts';
6
+ import {
7
+ CODE_SERVER_PORT,
8
+ DEFAULT_IMAGE,
9
+ PUBLISH_HOST,
10
+ devcontainerBin,
11
+ dockerEnv
12
+ } from './config.server.ts';
7
13
  import { spawnCapture } from './spawn.server.ts';
8
14
  import type { PortForward } from '../types.ts';
9
15
 
@@ -294,9 +300,11 @@ export async function writeOverrideConfig(
294
300
  };
295
301
 
296
302
  // Rendered from scratch rather than merged, so removing a forward actually drops its mapping.
303
+ // code-server stays loopback even under HOST=0.0.0.0 — it has no auth of its own, so the LAN
304
+ // reaches it only through the Basic-Auth-gated /p/:id/ proxy.
297
305
  config.appPort = [
298
306
  `127.0.0.1:${hostPort}:${CODE_SERVER_PORT}`,
299
- ...forwards.map((f) => `127.0.0.1:${f.host_port}:${f.container_port}`)
307
+ ...forwards.map((f) => `${PUBLISH_HOST}:${f.host_port}:${f.container_port}`)
300
308
  ];
301
309
 
302
310
  // host.docker.internal isn't automatic on Colima/Linux Docker.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Forwarded ports are published on the machine running codebay, which is only
3
+ * `localhost` when the browser is on that same machine — under HOST=0.0.0.0 it
4
+ * isn't. SSR has no `window`, so the first paint falls back to localhost.
5
+ */
6
+ export function forwardedPortUrl(hostPort: number): string {
7
+ const host = typeof window === 'undefined' ? 'localhost' : window.location.hostname;
8
+ return `http://${host}:${hostPort}`;
9
+ }
@@ -2,6 +2,8 @@
2
2
  import '@fontsource-variable/doto';
3
3
  import '@fontsource-variable/jetbrains-mono';
4
4
  import AvatarsView from '../components/AvatarsView.svelte';
5
+
6
+ let { namePlaceholder }: { namePlaceholder: string } = $props();
5
7
  </script>
6
8
 
7
- <AvatarsView mochi:hydrate />
9
+ <AvatarsView {namePlaceholder} mochi:hydrate />
@@ -23,7 +23,8 @@
23
23
  customEndpointModel,
24
24
  hostEnvVarsEnabled,
25
25
  hostEnvVarNames,
26
- hostEnvVarPresence
26
+ hostEnvVarPresence,
27
+ version
27
28
  }: {
28
29
  defaultImage: string;
29
30
  builtinImage: string;
@@ -45,6 +46,7 @@
45
46
  hostEnvVarsEnabled: boolean;
46
47
  hostEnvVarNames: string[];
47
48
  hostEnvVarPresence: Record<string, boolean>;
49
+ version: string;
48
50
  } = $props();
49
51
  </script>
50
52
 
@@ -69,5 +71,6 @@
69
71
  {hostEnvVarsEnabled}
70
72
  {hostEnvVarNames}
71
73
  {hostEnvVarPresence}
74
+ {version}
72
75
  mochi:hydrate
73
76
  />
package/src/routes.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  } from './container-injections/claude-code-custom.ts';
21
21
  import { hostEnvVarPresence, parseHostEnvVarNames } from './container-injections/host-env-vars.ts';
22
22
  import { browse } from './lib/picker.server.ts';
23
+ import { pickNamePrompt } from './avatars/name-prompts.ts';
23
24
  import {
24
25
  addForwardedPort,
25
26
  createInstance,
@@ -45,7 +46,7 @@ import {
45
46
  listFolderHistory,
46
47
  setOption
47
48
  } from './lib/db.server.ts';
48
- import { DEFAULT_COPY_IGNORE, DEFAULT_IMAGE } from './lib/config.server.ts';
49
+ import { APP_VERSION, DEFAULT_COPY_IGNORE, DEFAULT_IMAGE } from './lib/config.server.ts';
49
50
  import { wsUpgradeAllowed } from './lib/auth.server.ts';
50
51
  import { clearAttention, setAttention } from './lib/bridge.server.ts';
51
52
  import { timingSafeEqualStr } from './lib/crypto.server.ts';
@@ -144,9 +145,11 @@ export const routes: Record<string, MochiRouteValue> = {
144
145
  }
145
146
  }),
146
147
 
147
- // Reached from the settings page's coin; the sprite registry is a static client
148
- // import, so there is nothing for the server to hand over.
149
- '/avatars': Mochi.page('./src/pages/Avatars.svelte'),
148
+ // Reached from the settings page's coin. The name hint is rolled here rather than in
149
+ // the editor so hydration can't re-roll it and swap the word out mid-load.
150
+ '/avatars': Mochi.page('./src/pages/Avatars.svelte', {
151
+ serverProps: () => ({ namePlaceholder: pickNamePrompt() })
152
+ }),
150
153
 
151
154
  // Settings mutations are Form actions rather than JSON routes, so each control
152
155
  // still works without JS and `enhance` only upgrades it.
@@ -181,7 +184,8 @@ export const routes: Record<string, MochiRouteValue> = {
181
184
  customEndpointModel: getOption('custom_endpoint_model') ?? DEFAULT_MODEL,
182
185
  hostEnvVarsEnabled: getOption('host_env_vars_enabled') === '1',
183
186
  hostEnvVarNames,
184
- hostEnvVarPresence: hostEnvVarPresence(hostEnvVarNames)
187
+ hostEnvVarPresence: hostEnvVarPresence(hostEnvVarNames),
188
+ version: APP_VERSION
185
189
  };
186
190
  },
187
191
  actions: {
@@ -1 +0,0 @@
1
- import{Ta as a}from"/_mochi/client/chunk-kgn1b2t1.js";export{a as registerComponent};