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 +14 -0
- package/CLAUDE.md +43 -0
- package/package.json +1 -1
- package/src/cli/commands/dev.ts +2 -1
- package/src/cli/commands/expose.ts +2 -1
- package/src/cli/commands/serve.ts +2 -1
- package/src/cli/config.ts +12 -1
- package/src/shared/signaling.ts +7 -0
- package/src/web/discovery.tsx +73 -15
- package/.claude/scheduled_tasks.lock +0 -1
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
package/src/cli/commands/dev.ts
CHANGED
|
@@ -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);
|
package/src/shared/signaling.ts
CHANGED
|
@@ -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
|
package/src/web/discovery.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
style={styles.
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
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}
|