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 +8 -0
- package/package.json +1 -1
- package/src/cli/commands/dev.ts +2 -2
- package/src/cli/commands/serve.ts +1 -1
- package/src/cli/vscode.ts +26 -4
- package/src/shared/repo.test.ts +36 -17
- package/src/shared/repo.ts +11 -8
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
package/src/cli/commands/dev.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
|
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(
|
|
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> {
|
package/src/shared/repo.test.ts
CHANGED
|
@@ -3,23 +3,25 @@ import { parseDeepLink, pickRoomMatch, repoKey, shareableDeepLink, toPosixPath }
|
|
|
3
3
|
import { parseGitRemote } from "../cli/git";
|
|
4
4
|
|
|
5
5
|
describe("toPosixPath", () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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("
|
|
12
|
-
expect(toPosixPath("D:\\foo")).toBe("/
|
|
13
|
-
expect(toPosixPath("c:\\ws")).toBe("/c
|
|
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 /<
|
|
17
|
-
expect(toPosixPath("C:\\")).toBe("/
|
|
18
|
-
expect(toPosixPath("C:")).toBe("/
|
|
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("/
|
|
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
|
|
32
|
-
expect(toPosixPath("/
|
|
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("/
|
|
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/
|
|
91
|
-
expect(dl?.type === "dev" && dl.target.path).toBe("/
|
|
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: "/
|
|
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", () => {
|
package/src/shared/repo.ts
CHANGED
|
@@ -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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* `C:\Users\x` -> `/
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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]
|
|
79
|
+
const letter = drive[1]; // preserve drive-letter case
|
|
77
80
|
const rest = (drive[2] ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
78
|
-
return 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, "/");
|