codehost 0.5.0 → 0.5.1

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,11 @@
1
+ ## [0.5.1](https://github.com/snomiao/codehost/compare/v0.5.0...v0.5.1) (2026-06-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * don't time out (and oxmgr-restart-loop) on first-run VS Code server download ([5ad1b06](https://github.com/snomiao/codehost/commit/5ad1b068668afda4d19f403286240827a0a4b9c4))
7
+ * Windows ?folder= path must be /C:/ws (file-URI form), not git-bash /c/ws ([f6d2485](https://github.com/snomiao/codehost/commit/f6d24858c943dd2b7b5f1d5a96f71468e9552140))
8
+
1
9
  # [0.5.0](https://github.com/snomiao/codehost/compare/v0.4.0...v0.5.0) (2026-06-08)
2
10
 
3
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehost",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,8 +86,8 @@ export const devCommand: CommandModule<{}, DevArgs> = {
86
86
  const id = repoIdentity(dir);
87
87
  const meta: PeerMeta = {
88
88
  name: argv.name ?? host,
89
- // POSIX-drive form for the browser (C:\ws -> /c/ws); `dir` stays the real
90
- // OS path for the local VS Code working dir.
89
+ // VS Code-web ?folder= form for the browser (C:\ws -> /C:/ws); `dir` stays
90
+ // the real OS path for the local VS Code working dir.
91
91
  cwd: toPosixPath(dir),
92
92
  host,
93
93
  kind: "repo",
@@ -87,7 +87,7 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
87
87
  // onto subfolders via VS Code's ?folder= using this layout.
88
88
  const meta: PeerMeta = {
89
89
  name: argv.name ?? host,
90
- // POSIX-drive form for the browser ?folder= URI (C:\ws -> /c/ws); the real
90
+ // VS Code-web ?folder= form for the browser (C:\ws -> /C:/ws); the real
91
91
  // OS path `dir` is still what we spawn VS Code in.
92
92
  cwd: toPosixPath(dir),
93
93
  host,
package/src/cli/vscode.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { spawn, type Subprocess } from "bun";
2
2
  import { resolveCodeBinary } from "./vscode-install";
3
3
 
4
+ // How long to wait for `code serve-web` to answer. The default is generous
5
+ // because the FIRST run downloads the server component, which can take minutes
6
+ // on a slow link or a fresh Windows box — and under the oxmgr daemon
7
+ // (`--restart on-failure`) a too-short timeout makes us exit mid-download, get
8
+ // restarted, and re-download forever. Override with CODEHOST_VSCODE_READY_TIMEOUT_MS.
9
+ const READY_TIMEOUT_MS = Number(process.env.CODEHOST_VSCODE_READY_TIMEOUT_MS) || 10 * 60_000;
10
+
4
11
  export interface VscodeServer {
5
12
  port: number;
6
13
  basePath: string;
@@ -49,7 +56,7 @@ export async function launchVscode(opts: LaunchOptions): Promise<VscodeServer> {
49
56
  });
50
57
 
51
58
  const base = `http://127.0.0.1:${port}${opts.basePath}/`;
52
- await waitForHttp(base, 30_000);
59
+ await waitForHttp(base, READY_TIMEOUT_MS, proc);
53
60
  console.log(`[codehost] VS Code ready at ${base}`);
54
61
 
55
62
  const stop = () => {
@@ -62,18 +69,33 @@ export async function launchVscode(opts: LaunchOptions): Promise<VscodeServer> {
62
69
  return { port, basePath: opts.basePath, proc, stop };
63
70
  }
64
71
 
65
- async function waitForHttp(url: string, timeoutMs: number): Promise<void> {
66
- const deadline = Date.now() + timeoutMs;
72
+ async function waitForHttp(url: string, timeoutMs: number, proc?: Subprocess): Promise<void> {
73
+ const started = Date.now();
74
+ const deadline = started + timeoutMs;
75
+ let nextHeartbeat = started + 15_000;
67
76
  while (Date.now() < deadline) {
77
+ // If the server process died (bad flag, crash), fail now instead of waiting
78
+ // out the whole timeout — and surface that it exited rather than hung.
79
+ if (proc && proc.exitCode !== null) {
80
+ throw new Error(`VS Code server exited (code ${proc.exitCode}) before becoming ready at ${url}`);
81
+ }
68
82
  try {
69
83
  const res = await fetch(url, { redirect: "manual" });
70
84
  if (res.status > 0) return;
71
85
  } catch {
72
86
  // not up yet
73
87
  }
88
+ if (Date.now() >= nextHeartbeat) {
89
+ const secs = Math.round((Date.now() - started) / 1000);
90
+ console.log(`[codehost] waiting for VS Code server to start… (${secs}s; first run downloads the server component)`);
91
+ nextHeartbeat += 15_000;
92
+ }
74
93
  await Bun.sleep(300);
75
94
  }
76
- throw new Error(`VS Code did not become ready at ${url} within ${timeoutMs}ms`);
95
+ throw new Error(
96
+ `VS Code did not become ready at ${url} within ${timeoutMs}ms ` +
97
+ `(first-run server download can be slow — raise CODEHOST_VSCODE_READY_TIMEOUT_MS)`,
98
+ );
77
99
  }
78
100
 
79
101
  async function freePort(): Promise<number> {
@@ -3,23 +3,25 @@ import { parseDeepLink, pickRoomMatch, repoKey, shareableDeepLink, toPosixPath }
3
3
  import { parseGitRemote } from "../cli/git";
4
4
 
5
5
  describe("toPosixPath", () => {
6
- test("Windows drive path -> POSIX drive form", () => {
7
- expect(toPosixPath("C:\\ws")).toBe("/c/ws");
8
- expect(toPosixPath("C:\\Users\\x")).toBe("/c/Users/x");
6
+ // VS Code web's ?folder= on Windows wants the file-URI authority form
7
+ // (/C:/ws), NOT git-bash /c/ws — the latter reports "workspace does not exist".
8
+ test("Windows drive path -> /<Drive>:/... (file-URI form)", () => {
9
+ expect(toPosixPath("C:\\ws")).toBe("/C:/ws");
10
+ expect(toPosixPath("C:\\Users\\taku")).toBe("/C:/Users/taku");
9
11
  });
10
12
 
11
- test("lowercases the drive letter", () => {
12
- expect(toPosixPath("D:\\foo")).toBe("/d/foo");
13
- expect(toPosixPath("c:\\ws")).toBe("/c/ws");
13
+ test("preserves drive-letter case (drive is case-insensitive on Windows)", () => {
14
+ expect(toPosixPath("D:\\foo")).toBe("/D:/foo");
15
+ expect(toPosixPath("c:\\ws")).toBe("/c:/ws");
14
16
  });
15
17
 
16
- test("drive root collapses to /<letter> (no trailing slash)", () => {
17
- expect(toPosixPath("C:\\")).toBe("/c");
18
- expect(toPosixPath("C:")).toBe("/c");
18
+ test("drive root collapses to /<Drive>: (no trailing slash)", () => {
19
+ expect(toPosixPath("C:\\")).toBe("/C:");
20
+ expect(toPosixPath("C:")).toBe("/C:");
19
21
  });
20
22
 
21
23
  test("forward-slash Windows paths normalize too", () => {
22
- expect(toPosixPath("C:/ws")).toBe("/c/ws");
24
+ expect(toPosixPath("C:/ws")).toBe("/C:/ws");
23
25
  });
24
26
 
25
27
  test("POSIX absolute paths are unchanged (mac/linux not broken)", () => {
@@ -28,12 +30,20 @@ describe("toPosixPath", () => {
28
30
  expect(toPosixPath("/")).toBe("/");
29
31
  });
30
32
 
31
- test("already-normalized POSIX-drive path is a no-op", () => {
32
- expect(toPosixPath("/c/ws")).toBe("/c/ws");
33
+ test("already-normalized path is idempotent", () => {
34
+ expect(toPosixPath("/C:/ws")).toBe("/C:/ws");
33
35
  });
34
36
 
35
37
  test("trims trailing backslashes/slashes on a drive path", () => {
36
- expect(toPosixPath("C:\\ws\\")).toBe("/c/ws");
38
+ expect(toPosixPath("C:\\ws\\")).toBe("/C:/ws");
39
+ });
40
+
41
+ // Regression: the value VS Code web receives via ?folder= (URL-decoded) must
42
+ // be exactly /C:/ws so serve-web resolves it to the real C:\ws on disk.
43
+ test("?folder= round-trip: encode(toPosixPath) decodes back to /C:/ws", () => {
44
+ const folderParam = encodeURIComponent(toPosixPath("C:\\ws"));
45
+ expect(folderParam).toBe("%2FC%3A%2Fws");
46
+ expect(decodeURIComponent(folderParam)).toBe("/C:/ws");
37
47
  });
38
48
  });
39
49
 
@@ -87,8 +97,16 @@ describe("parseDeepLink + repoKey round-trip", () => {
87
97
  });
88
98
 
89
99
  test("/dev/<path> -> dev target with leading slash", () => {
90
- const dl = parseDeepLink("/dev/c/ws");
91
- expect(dl?.type === "dev" && dl.target.path).toBe("/c/ws");
100
+ const dl = parseDeepLink("/dev/Users/sno/ws");
101
+ expect(dl?.type === "dev" && dl.target.path).toBe("/Users/sno/ws");
102
+ });
103
+
104
+ test("/dev/<Windows drive path> round-trips (colon in a non-leading segment)", () => {
105
+ // shareableDeepLink -> address bar -> parseDeepLink must preserve /C:/ws.
106
+ const path = shareableDeepLink({ folder: toPosixPath("C:\\ws") })!;
107
+ expect(path).toBe("/dev/C:/ws");
108
+ const dl = parseDeepLink(path);
109
+ expect(dl?.type === "dev" && dl.target.path).toBe("/C:/ws");
92
110
  });
93
111
 
94
112
  test("non-deep-link -> null", () => {
@@ -111,8 +129,9 @@ describe("shareableDeepLink", () => {
111
129
  );
112
130
  });
113
131
 
114
- test("no repo -> /dev/<folder>", () => {
115
- expect(shareableDeepLink({ folder: "/c/ws" })).toBe("/dev/c/ws");
132
+ test("no repo -> /dev/<folder> (Windows drive path preserved)", () => {
133
+ expect(shareableDeepLink({ folder: "/C:/ws" })).toBe("/dev/C:/ws");
134
+ expect(shareableDeepLink({ folder: "/Users/sno/ws" })).toBe("/dev/Users/sno/ws");
116
135
  });
117
136
 
118
137
  test("nothing addressable -> null", () => {
@@ -63,19 +63,22 @@ export function repoKey(t: Pick<RepoTarget, "host" | "owner" | "name">): string
63
63
  }
64
64
 
65
65
  /**
66
- * Normalize a served workspace path to the POSIX-drive form the browser side
67
- * and VS Code web expect. A Windows drive path becomes a `/c/...` style path
68
- * (lowercased drive, backslashes -> slashes): `C:\ws` -> `/c/ws`,
69
- * `C:\Users\x` -> `/c/Users/x`, `D:\` -> `/d`. POSIX absolute paths (mac/linux)
70
- * are returned unchanged. Used for `PeerMeta.cwd`, which feeds the `?folder=`
71
- * URI the real OS path is still used for the local VS Code working dir.
66
+ * Normalize a served workspace path to the form VS Code web's `?folder=` query
67
+ * expects. On Windows that's the file-URI authority form: a leading slash, the
68
+ * drive letter and colon preserved, backslashes -> slashes
69
+ * `C:\ws` -> `/C:/ws`, `C:\Users\x` -> `/C:/Users/x`, `D:\` -> `/D:`. (The
70
+ * git-bash `/c/ws` form does NOT resolve serve-web reports "workspace does not
71
+ * exist".) POSIX absolute paths (mac/linux) are returned unchanged, and the
72
+ * result is idempotent. Used for `PeerMeta.cwd`, which feeds the `?folder=` URI
73
+ * (URL-encoded in transit, decoded back to this by VS Code) and the `/dev/<path>`
74
+ * deep link — the real OS path is still used for the local VS Code working dir.
72
75
  */
73
76
  export function toPosixPath(p: string): string {
74
77
  const drive = /^([A-Za-z]):(?:[\\/](.*))?$/.exec(p);
75
78
  if (drive) {
76
- const letter = drive[1].toLowerCase();
79
+ const letter = drive[1]; // preserve drive-letter case
77
80
  const rest = (drive[2] ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
78
- return rest ? `/${letter}/${rest}` : `/${letter}`;
81
+ return rest ? `/${letter}:/${rest}` : `/${letter}:`;
79
82
  }
80
83
  // Already POSIX (or a relative path): just unify any stray backslashes.
81
84
  return p.replace(/\\/g, "/");