codehost 0.22.1 → 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,10 @@
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
+
1
8
  ## [0.22.1](https://github.com/snomiao/codehost/compare/v0.22.0...v0.22.1) (2026-06-15)
2
9
 
3
10
 
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.1",
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
@@ -960,16 +973,37 @@ export function Discovery() {
960
973
  <div style={styles.page}>
961
974
  <header style={styles.header}>
962
975
  <span style={styles.brand}>codehost</span>
963
- <span style={styles.dim}>·</span>
964
- <span
965
- style={styles.cwd}
966
- title={`${activeServer?.meta?.name ?? ""} ${activeServer?.meta?.cwd ?? ""}`.trim()}
967
- >
968
- {shareLabel(sharePathRef.current) ??
969
- activeServer?.meta?.cwd ??
970
- activeServer?.meta?.name ??
971
- activePeerId?.slice(0, 8)}
972
- </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
+ )}
973
1007
  {connPath && (
974
1008
  <span
975
1009
  style={styles.dim}
@@ -1300,6 +1334,7 @@ const styles: Record<string, React.CSSProperties> = {
1300
1334
  page: { display: "flex", flexDirection: "column", height: "100%", background: "#1f1f1f", color: "#ccc", fontFamily: "system-ui, sans-serif" },
1301
1335
  header: { display: "flex", alignItems: "center", gap: 8, padding: "8px 14px", background: "#2d2d2d", borderBottom: "1px solid #3d3d3d", fontSize: 13 },
1302
1336
  brand: { fontFamily: "monospace", fontWeight: 700, color: "#fff" },
1337
+ hostTag: { fontFamily: "monospace", fontSize: 12, color: "#dcdcaa" },
1303
1338
  dim: { color: "#888", fontSize: 12 },
1304
1339
  status: { fontSize: 12 },
1305
1340
  // Wide cap + per-host card GRID below: a 4K monitor gets several columns of
@@ -1373,6 +1408,7 @@ const styles: Record<string, React.CSSProperties> = {
1373
1408
  cardName: { fontSize: 14, fontWeight: 600, color: "#fff" },
1374
1409
  cardSub: { display: "flex", gap: 12, fontSize: 12, color: "#888", marginTop: 2 },
1375
1410
  cwd: { fontFamily: "monospace" },
1411
+ cwdLink: { fontFamily: "monospace", color: "#75beff", textDecoration: "none" },
1376
1412
  echo: { marginTop: 6, fontSize: 12, color: "#4ec9b0", fontFamily: "monospace" },
1377
1413
  echoBad: { marginTop: 6, fontSize: 12, color: "#f48771", fontFamily: "monospace" },
1378
1414
  rosterSection: { marginTop: 28 },
@@ -1384,7 +1420,7 @@ const styles: Record<string, React.CSSProperties> = {
1384
1420
  cmdRowNarrow: { flexDirection: "column", alignItems: "stretch", gap: 6, marginTop: 12 },
1385
1421
  cmdLabel: { fontSize: 11, color: "#888", width: 88, flexShrink: 0 },
1386
1422
  cmdLabelNarrow: { width: "auto" },
1387
- 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" },
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" },
1388
1424
  cmdCopy: { flexShrink: 0, background: "#0e639c", border: "none", color: "#fff", padding: "8px 12px", borderRadius: 6, cursor: "pointer", fontSize: 12 },
1389
1425
  cmdCopyNarrow: { width: "100%", padding: "10px 12px" },
1390
1426
  rosterHint: { margin: "10px 0 0", fontSize: 12, color: "#888" },
@@ -1 +0,0 @@
1
- {"sessionId":"f8e4e571-944e-43d7-bbfa-267a5251e41c","pid":87968,"procStart":"Mon Jun 8 09:46:02 2026","acquiredAt":1780974562147}