codehost 0.22.0 → 0.23.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.23.0](https://github.com/snomiao/codehost/compare/v0.22.1...v0.23.0) (2026-06-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * **web:** show the serving user@host in the connected header, link the repo URL out ([a10c243](https://github.com/snomiao/codehost/commit/a10c2435d0b6b15572708696854ccf65c74a268e))
7
+
8
+ ## [0.22.1](https://github.com/snomiao/codehost/compare/v0.22.0...v0.22.1) (2026-06-15)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **web:** stack the setup-card command rows on narrow screens ([4348ef7](https://github.com/snomiao/codehost/commit/4348ef72d766aba4254ecf693277a6064b06d333))
14
+
1
15
  # [0.22.0](https://github.com/snomiao/codehost/compare/v0.21.0...v0.22.0) (2026-06-14)
2
16
 
3
17
 
package/CLAUDE.md ADDED
@@ -0,0 +1,43 @@
1
+ # codehost — repo-scoped agent notes
2
+
3
+ WebRTC-tunneled VS Code: a CLI daemon (`codehost serve`/`dev`) serves a machine; the
4
+ codehost.dev page (Cloudflare Pages) connects over a WebRTC data channel; a Worker + Durable
5
+ Object (signal.codehost.dev) only relays signaling. See README.md for the architecture.
6
+
7
+ ## Build / typecheck / test / deploy
8
+
9
+ - **Typecheck:** `bun run typecheck` — three tsconfigs (root, `src/web/tsconfig.sw.json`,
10
+ `worker/tsconfig.json`). All three must pass.
11
+ - **Build:** `bun run build` (vite app + service worker) and `bun run build:lib` (the standalone
12
+ `room-client` bundle for embedding).
13
+ - **Tests:** `bun test` globs into the **gitignored `tmp/` scratch checkout** (a second project
14
+ missing test deps) and reports false failures. Run project tests only:
15
+ `bun test $(git ls-files '*.test.ts' '*.test.tsx')`.
16
+ - **Deploy:** `bun run deploy:signal` (the Worker/DO — only needed when the signaling protocol
17
+ changes) and `bun run deploy:pages` (the codehost.dev site). Wrangler reads
18
+ `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` from `.env.local` (gitignored). The token is
19
+ **account-owned**, so verify it via `/accounts/<id>/tokens/verify`, not `/user/tokens/verify`;
20
+ it has no Zone/DNS scope.
21
+ - A push to `main` triggers a semantic-release version bump + npm publish — keep `main` green.
22
+
23
+ ## The signaling protocol is shared AND has live peers in the field
24
+
25
+ `src/shared/signaling.ts` is the wire contract for the page, the daemon, AND the Worker. Deployed
26
+ daemons run many npm versions, so **only make additive/optional changes** to the protocol.
27
+
28
+ The connecting role is `"client"`, but `"viewer"` remains a valid wire value: receivers accept
29
+ either via `isClientRole`, and new code still EMITS `CLIENT_WIRE_ROLE` (currently `"viewer"`)
30
+ during an "accept both, emit old" transition — so old and new daemons/pages interoperate. Don't
31
+ rename or repurpose wire values, or flip the emitted role, without keeping that compatibility.
32
+
33
+ ## Verifying codehost.dev visually (rech)
34
+
35
+ To screenshot/verify the live site, drive a real Chrome with `rech` (the `rechrome` package — see
36
+ its own CLAUDE.md for setup and the session/scroll caveats). codehost.dev quirks:
37
+
38
+ - The page scrolls an **inner `<main>` (overflow:auto)**, not the document — so
39
+ `screenshot --full-page` only captures the viewport. To capture below the fold, `resize` the
40
+ window tall enough that everything lays out without inner-scroll, then screenshot and crop.
41
+ - The mobile layout kicks in under a **560px** width breakpoint — `resize` to a phone width to
42
+ check it.
43
+ - After a Pages deploy, open with a `?cb=<ts>` cache-buster to dodge a stale cached bundle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehost",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,7 +4,7 @@ import type { CommandModule } from "yargs";
4
4
  import type { PeerMeta } from "../../shared/signaling";
5
5
  import type { ApprovePolicy } from "../approver";
6
6
  import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
7
- import { ensureHostId } from "../config";
7
+ import { currentUser, ensureHostId } from "../config";
8
8
  import { launchServeDaemon } from "../daemonize";
9
9
  import { announceConnect } from "../open-url";
10
10
  import { runServer } from "../run-server";
@@ -137,6 +137,7 @@ export const devCommand: CommandModule<{}, DevArgs> = {
137
137
  // the real OS path for the local VS Code working dir.
138
138
  cwd: toPosixPath(dir),
139
139
  host,
140
+ user: currentUser(),
140
141
  hostId: ensureHostId(),
141
142
  kind: "repo",
142
143
  repo: id.repo,
@@ -2,7 +2,7 @@ import { hostname } from "node:os";
2
2
  import type { CommandModule } from "yargs";
3
3
  import type { PeerMeta } from "../../shared/signaling";
4
4
  import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
5
- import { ensureHostId } from "../config";
5
+ import { currentUser, ensureHostId } from "../config";
6
6
  import { launchServeDaemon } from "../daemonize";
7
7
  import { runServer } from "../run-server";
8
8
  import { DEFAULT_SIGNAL_URL } from "./serve";
@@ -79,6 +79,7 @@ export const exposeCommand: CommandModule<{}, ExposeArgs> = {
79
79
  name: argv.name ?? `localhost:${argv.port}`,
80
80
  cwd: `localhost:${argv.port}`,
81
81
  host,
82
+ user: currentUser(),
82
83
  hostId: ensureHostId(),
83
84
  };
84
85
 
@@ -6,7 +6,7 @@ import type { PeerMeta } from "../../shared/signaling";
6
6
  import type { ApprovePolicy } from "../approver";
7
7
  import { DEFAULT_LAYOUT, GITHUB_HOST, toPosixPath } from "../../shared/repo";
8
8
  import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
9
- import { defaultRoot, ensureHostId } from "../config";
9
+ import { currentUser, defaultRoot, ensureHostId } from "../config";
10
10
  import { launchServeDaemon } from "../daemonize";
11
11
  import { announceConnect } from "../open-url";
12
12
  import { agentYesPlugin } from "../plugins/agent-yes";
@@ -200,6 +200,7 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
200
200
  // real OS path `dir` is still what we spawn VS Code in.
201
201
  cwd: toPosixPath(dir),
202
202
  host,
203
+ user: currentUser(),
203
204
  hostId: ensureHostId(),
204
205
  kind: "root",
205
206
  layout,
package/src/cli/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
2
+ import { homedir, userInfo } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
 
5
5
  // Persistent CLI config under ~/.codehost (same root as the managed VS Code
@@ -39,6 +39,17 @@ export function defaultRoot(file: string = CONFIG_FILE): string {
39
39
  return readConfig(file).root || join(homedir(), "ws");
40
40
  }
41
41
 
42
+ /** This machine's OS login name, advertised so the web UI can show a
43
+ * `user@host` label. Falls back to $USER/$USERNAME, then "unknown" —
44
+ * os.userInfo() throws when the uid has no passwd entry (some containers). */
45
+ export function currentUser(): string {
46
+ try {
47
+ return userInfo().username;
48
+ } catch {
49
+ return process.env.USER || process.env.USERNAME || "unknown";
50
+ }
51
+ }
52
+
42
53
  /** This machine's persistent hostId, minting + saving it on first call. */
43
54
  export function ensureHostId(file: string = CONFIG_FILE): string {
44
55
  const config = readConfig(file);
@@ -68,6 +68,13 @@ export interface PeerMeta {
68
68
  cwd?: string;
69
69
  /** Server only: hostname of the machine running the daemon. */
70
70
  host?: string;
71
+ /**
72
+ * OS login name of whoever launched the daemon — the web UI shows it as the
73
+ * `user@host` label in front of the workspace URL, so you can tell which
74
+ * machine/account is actually serving a `codehost.dev/gh/...` link. Absent on
75
+ * older daemons (and in environments with no passwd entry) — fall back to `host`.
76
+ */
77
+ user?: string;
71
78
  /**
72
79
  * Stable machine identity (UUID persisted in ~/.codehost/config.json). All
73
80
  * daemons on one machine share it, unlike the per-process peerId, so clients
@@ -102,6 +102,19 @@ function shareLabel(path: string | null): string | null {
102
102
  return path;
103
103
  }
104
104
 
105
+ /** External URL for a repo-shaped share path, so the connected-view label can
106
+ * link out to the real host: /gh/owner/repo/tree/x -> https://github.com/owner/repo/tree/x,
107
+ * /git/<host>/… -> https://<host>/… . Null for non-repo paths (folder mounts),
108
+ * which have no public URL. */
109
+ function shareHref(path: string | null): string | null {
110
+ if (!path) return null;
111
+ const gh = path.match(/^\/gh\/(.+)$/);
112
+ if (gh) return `https://github.com/${gh[1]}`;
113
+ const git = path.match(/^\/git\/(.+)$/);
114
+ if (git) return `https://${git[1]}`;
115
+ return null;
116
+ }
117
+
105
118
  /**
106
119
  * Find which of the user's saved rooms hosts a server matching a token-less deep
107
120
  * link. Opens a short-lived viewer connection to each candidate room in
@@ -196,9 +209,28 @@ function RoomClient(props: {
196
209
  return null;
197
210
  }
198
211
 
199
- /** A copy-to-clipboard command row: label, the command, and a Copy button. */
212
+ /** Track a `(max-width: …)` media query so inline-styled components can go
213
+ * responsive without a stylesheet. SSR-safe and listener-cleaned. */
214
+ function useNarrow(maxWidth = 560): boolean {
215
+ const [narrow, setNarrow] = useState(
216
+ () => typeof window !== "undefined" && window.matchMedia(`(max-width:${maxWidth}px)`).matches,
217
+ );
218
+ useEffect(() => {
219
+ const mq = window.matchMedia(`(max-width:${maxWidth}px)`);
220
+ const on = () => setNarrow(mq.matches);
221
+ on();
222
+ mq.addEventListener("change", on);
223
+ return () => mq.removeEventListener("change", on);
224
+ }, [maxWidth]);
225
+ return narrow;
226
+ }
227
+
228
+ /** A copy-to-clipboard command row: label, the command, and a Copy button. On
229
+ * narrow screens the three stack vertically so the long command doesn't get
230
+ * crushed between a fixed label and the button. */
200
231
  function CopyCommand({ label, command }: { label: string; command: string }) {
201
232
  const [copied, setCopied] = useState(false);
233
+ const narrow = useNarrow();
202
234
  const copy = async () => {
203
235
  try {
204
236
  await navigator.clipboard.writeText(command);
@@ -210,10 +242,10 @@ function CopyCommand({ label, command }: { label: string; command: string }) {
210
242
  setTimeout(() => setCopied(false), 1500);
211
243
  };
212
244
  return (
213
- <div style={styles.cmdRow}>
214
- <span style={styles.cmdLabel}>{label}</span>
245
+ <div style={{ ...styles.cmdRow, ...(narrow ? styles.cmdRowNarrow : null) }}>
246
+ <span style={{ ...styles.cmdLabel, ...(narrow ? styles.cmdLabelNarrow : null) }}>{label}</span>
215
247
  <code style={styles.cmdCode}>{command}</code>
216
- <button style={styles.cmdCopy} onClick={copy}>
248
+ <button style={{ ...styles.cmdCopy, ...(narrow ? styles.cmdCopyNarrow : null) }} onClick={copy}>
217
249
  {copied ? "Copied!" : "Copy"}
218
250
  </button>
219
251
  </div>
@@ -941,16 +973,37 @@ export function Discovery() {
941
973
  <div style={styles.page}>
942
974
  <header style={styles.header}>
943
975
  <span style={styles.brand}>codehost</span>
944
- <span style={styles.dim}>·</span>
945
- <span
946
- style={styles.cwd}
947
- title={`${activeServer?.meta?.name ?? ""} ${activeServer?.meta?.cwd ?? ""}`.trim()}
948
- >
949
- {shareLabel(sharePathRef.current) ??
950
- activeServer?.meta?.cwd ??
951
- activeServer?.meta?.name ??
952
- activePeerId?.slice(0, 8)}
953
- </span>
976
+ {activeServer?.meta?.host ? (
977
+ // Which machine/account is actually serving this codehost.dev link.
978
+ <span style={styles.hostTag} title="machine serving this workspace">
979
+ [{activeServer.meta.user ? `${activeServer.meta.user}@` : ""}
980
+ {activeServer.meta.host}]
981
+ </span>
982
+ ) : (
983
+ <span style={styles.dim}>·</span>
984
+ )}
985
+ {shareHref(sharePathRef.current) ? (
986
+ // Repo-shaped link: clickable out to the real host (GitHub etc.).
987
+ <a
988
+ style={styles.cwdLink}
989
+ href={shareHref(sharePathRef.current) ?? undefined}
990
+ target="_blank"
991
+ rel="noopener noreferrer"
992
+ title={shareHref(sharePathRef.current) ?? undefined}
993
+ >
994
+ {shareLabel(sharePathRef.current)}
995
+ </a>
996
+ ) : (
997
+ <span
998
+ style={styles.cwd}
999
+ title={`${activeServer?.meta?.name ?? ""} ${activeServer?.meta?.cwd ?? ""}`.trim()}
1000
+ >
1001
+ {shareLabel(sharePathRef.current) ??
1002
+ activeServer?.meta?.cwd ??
1003
+ activeServer?.meta?.name ??
1004
+ activePeerId?.slice(0, 8)}
1005
+ </span>
1006
+ )}
954
1007
  {connPath && (
955
1008
  <span
956
1009
  style={styles.dim}
@@ -1281,6 +1334,7 @@ const styles: Record<string, React.CSSProperties> = {
1281
1334
  page: { display: "flex", flexDirection: "column", height: "100%", background: "#1f1f1f", color: "#ccc", fontFamily: "system-ui, sans-serif" },
1282
1335
  header: { display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", background: "#2d2d2d", borderBottom: "1px solid #3d3d3d", fontSize: 13 },
1283
1336
  brand: { fontFamily: "monospace", fontWeight: 700, color: "#fff" },
1337
+ hostTag: { fontFamily: "monospace", fontSize: 12, color: "#dcdcaa" },
1284
1338
  dim: { color: "#888", fontSize: 12 },
1285
1339
  status: { fontSize: 12 },
1286
1340
  // Wide cap + per-host card GRID below: a 4K monitor gets several columns of
@@ -1354,6 +1408,7 @@ const styles: Record<string, React.CSSProperties> = {
1354
1408
  cardName: { fontSize: 14, fontWeight: 600, color: "#fff" },
1355
1409
  cardSub: { display: "flex", gap: 12, fontSize: 12, color: "#888", marginTop: 2 },
1356
1410
  cwd: { fontFamily: "monospace" },
1411
+ cwdLink: { fontFamily: "monospace", color: "#75beff", textDecoration: "none" },
1357
1412
  echo: { marginTop: 6, fontSize: 12, color: "#4ec9b0", fontFamily: "monospace" },
1358
1413
  echoBad: { marginTop: 6, fontSize: 12, color: "#f48771", fontFamily: "monospace" },
1359
1414
  rosterSection: { marginTop: 28 },
@@ -1362,9 +1417,12 @@ const styles: Record<string, React.CSSProperties> = {
1362
1417
  setupHead: { fontSize: 15, color: "#fff", fontWeight: 600, marginBottom: 6 },
1363
1418
  setupSub: { fontSize: 13, color: "#aaa", margin: "0 0 14px", lineHeight: 1.5 },
1364
1419
  cmdRow: { display: "flex", alignItems: "center", gap: 10, marginTop: 8 },
1420
+ cmdRowNarrow: { flexDirection: "column", alignItems: "stretch", gap: 6, marginTop: 12 },
1365
1421
  cmdLabel: { fontSize: 11, color: "#888", width: 88, flexShrink: 0 },
1366
- cmdCode: { flex: 1, minWidth: 0, background: "#1b1b1b", border: "1px solid #3d3d3d", borderRadius: 6, padding: "8px 10px", fontFamily: "monospace", fontSize: 12.5, color: "#dcdcaa", overflow: "auto", whiteSpace: "nowrap" },
1422
+ cmdLabelNarrow: { width: "auto" },
1423
+ cmdCode: { flex: 1, minWidth: 0, background: "#1b1b1b", border: "1px solid #3d3d3d", borderRadius: 6, padding: "8px 10px", fontFamily: "monospace", fontSize: 12.5, color: "#dcdcaa", whiteSpace: "pre-wrap", overflowWrap: "anywhere" },
1367
1424
  cmdCopy: { flexShrink: 0, background: "#0e639c", border: "none", color: "#fff", padding: "8px 12px", borderRadius: 6, cursor: "pointer", fontSize: 12 },
1425
+ cmdCopyNarrow: { width: "100%", padding: "10px 12px" },
1368
1426
  rosterHint: { margin: "10px 0 0", fontSize: 12, color: "#888" },
1369
1427
  personRow: { display: "flex", alignItems: "center", gap: 10, background: "#252525", border: "1px solid #3d3d3d", borderRadius: 8, padding: "8px 14px", fontSize: 13 },
1370
1428
  personDot: { color: "#4ec9b0", fontSize: 10 },
@@ -1 +0,0 @@
1
- {"sessionId":"f8e4e571-944e-43d7-bbfa-267a5251e41c","pid":87968,"procStart":"Mon Jun 8 09:46:02 2026","acquiredAt":1780974562147}