codehost 0.1.0 → 0.1.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/README.md +36 -5
- package/package.json +1 -1
- package/public/install.ps1 +43 -0
- package/public/install.sh +55 -0
- package/src/cli/commands/serve.ts +10 -27
- package/src/cli/commands/setup.ts +96 -0
- package/src/cli/commands/update.ts +19 -0
- package/src/cli/config.ts +27 -0
- package/src/cli/daemonize.ts +54 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/rtc-daemon.ts +57 -1
- package/src/cli/vscode-install.ts +222 -0
- package/src/cli/vscode.ts +4 -2
package/README.md
CHANGED
|
@@ -20,15 +20,32 @@ directly; a tiny Cloudflare Worker only brokers the handshake.
|
|
|
20
20
|
|
|
21
21
|
## Quickstart
|
|
22
22
|
|
|
23
|
-
On the machine you want to edit
|
|
23
|
+
On the machine you want to edit, run the one-line installer — it installs
|
|
24
|
+
[Bun](https://bun.sh) if needed, the `codehost` CLI, and then `codehost setup`
|
|
25
|
+
(picks a token, installs VS Code, and starts a server):
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
|
|
28
|
+
# macOS / Linux
|
|
29
|
+
curl -fsSL https://codehost.dev/install.sh | sh
|
|
30
|
+
|
|
31
|
+
# Windows (PowerShell)
|
|
32
|
+
powershell -c "irm codehost.dev/install.ps1 | iex"
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
Already have Bun? `bun add -g codehost && codehost setup` does the same. Or, for
|
|
36
|
+
a specific directory/token in one shot:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bunx codehost setup ~/my-project -t <token>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then open **https://codehost.dev**, enter the token printed by `setup`, and your
|
|
43
|
+
server appears in the list. Click **Connect** — VS Code loads in the page,
|
|
44
|
+
served entirely over the peer-to-peer data channel.
|
|
45
|
+
|
|
46
|
+
> Use `setup` (not `bunx codehost serve`) for the first run: `setup` installs
|
|
47
|
+
> everything and self-heals the native WebRTC module that a bare `bunx` can't
|
|
48
|
+
> fetch on its own.
|
|
32
49
|
|
|
33
50
|
The `<token>` is a shared secret: anyone with it can see and connect to the
|
|
34
51
|
servers in that room, so treat it like a password. It must be **at least 12
|
|
@@ -39,11 +56,25 @@ weaker tokens (e.g. `Str0ng-Token-99`).
|
|
|
39
56
|
## CLI
|
|
40
57
|
|
|
41
58
|
```bash
|
|
59
|
+
codehost setup [dir] [-t <token>] # token + VS Code + daemon, one shot
|
|
42
60
|
codehost serve [dir] -t <token> [options] # serve a directory (default: cwd)
|
|
43
61
|
codehost list # list daemonized servers (oxmgr)
|
|
44
62
|
codehost stop <name> # stop a daemonized server
|
|
63
|
+
codehost update # fetch the latest VS Code CLI now
|
|
45
64
|
```
|
|
46
65
|
|
|
66
|
+
**`codehost setup`** is the easy path: it reuses (or generates and saves to
|
|
67
|
+
`~/.codehost/config.json`) a strong room token, ensures a working VS Code,
|
|
68
|
+
starts the server under oxmgr, and prints the token + URL. Pass `-t` to set a
|
|
69
|
+
token explicitly, or `--new-token` to rotate the saved one.
|
|
70
|
+
|
|
71
|
+
**VS Code is auto-installed.** On first `serve`, codehost uses a `code` already
|
|
72
|
+
on your `PATH` if present; otherwise it downloads Microsoft's standalone VS Code
|
|
73
|
+
CLI for your OS/arch (verifying its sha256), caches it under `~/.codehost/vscode/`,
|
|
74
|
+
and re-checks the stable channel at most once per day. Force a refresh with
|
|
75
|
+
`codehost update`, or point at a specific binary with the `CODEHOST_CODE_BIN`
|
|
76
|
+
environment variable.
|
|
77
|
+
|
|
47
78
|
`serve` options:
|
|
48
79
|
|
|
49
80
|
| flag | default | meaning |
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# codehost installer — https://codehost.dev
|
|
2
|
+
#
|
|
3
|
+
# powershell -c "irm codehost.dev/install.ps1 | iex"
|
|
4
|
+
#
|
|
5
|
+
# Ensures Bun is installed, installs the `codehost` CLI globally (which fetches
|
|
6
|
+
# the native WebRTC binary via Bun's lifecycle scripts), then runs `codehost
|
|
7
|
+
# setup` to pick a token, install VS Code, and start a server daemon.
|
|
8
|
+
#
|
|
9
|
+
# Env override: $env:CODEHOST_NO_SETUP = "1" -> install only, skip setup.
|
|
10
|
+
|
|
11
|
+
$ErrorActionPreference = "Stop"
|
|
12
|
+
|
|
13
|
+
function Info($m) { Write-Host "[codehost] $m" -ForegroundColor Cyan }
|
|
14
|
+
function Fail($m) { Write-Host "[codehost] $m" -ForegroundColor Red; exit 1 }
|
|
15
|
+
|
|
16
|
+
# Bun installs its global bin under %USERPROFILE%\.bun\bin by default.
|
|
17
|
+
$bunBin = Join-Path $env:USERPROFILE ".bun\bin"
|
|
18
|
+
if (Test-Path $bunBin) { $env:Path = "$bunBin;$env:Path" }
|
|
19
|
+
|
|
20
|
+
if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
|
|
21
|
+
Info "Bun not found - installing from bun.sh..."
|
|
22
|
+
Invoke-RestMethod https://bun.sh/install.ps1 | Invoke-Expression
|
|
23
|
+
if (Test-Path $bunBin) { $env:Path = "$bunBin;$env:Path" }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
|
|
27
|
+
Fail "Bun install did not land on PATH. Open a new terminal and re-run."
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Info "installing the codehost CLI (bun add -g codehost)..."
|
|
31
|
+
bun add -g codehost
|
|
32
|
+
|
|
33
|
+
if (-not (Get-Command codehost -ErrorAction SilentlyContinue)) {
|
|
34
|
+
Fail "codehost installed but not on PATH. Add $bunBin to PATH and run: codehost setup"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ($env:CODEHOST_NO_SETUP -eq "1") {
|
|
38
|
+
Info "installed. Run ``codehost setup`` in the directory you want to serve."
|
|
39
|
+
exit 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Info "running ``codehost setup``..."
|
|
43
|
+
codehost setup
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# codehost installer — https://codehost.dev
|
|
3
|
+
#
|
|
4
|
+
# curl -fsSL https://codehost.dev/install.sh | sh
|
|
5
|
+
#
|
|
6
|
+
# Ensures Bun is installed, installs the `codehost` CLI globally (which fetches
|
|
7
|
+
# the native WebRTC binary via Bun's lifecycle scripts), then runs `codehost
|
|
8
|
+
# setup` to pick a token, install VS Code, and start a server daemon.
|
|
9
|
+
#
|
|
10
|
+
# Env overrides:
|
|
11
|
+
# CODEHOST_NO_SETUP=1 install only; don't run `codehost setup`
|
|
12
|
+
set -eu
|
|
13
|
+
|
|
14
|
+
info() { printf '\033[1;36m[codehost]\033[0m %s\n' "$1"; }
|
|
15
|
+
err() { printf '\033[1;31m[codehost]\033[0m %s\n' "$1" >&2; }
|
|
16
|
+
|
|
17
|
+
# Bun installs its global bin here by default; make sure it's on PATH for both
|
|
18
|
+
# the bun we may install and the `codehost` we're about to install.
|
|
19
|
+
BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
|
|
20
|
+
export BUN_INSTALL
|
|
21
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
22
|
+
|
|
23
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
24
|
+
info "Bun not found — installing from bun.sh…"
|
|
25
|
+
if command -v curl >/dev/null 2>&1; then
|
|
26
|
+
curl -fsSL https://bun.sh/install | bash
|
|
27
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
28
|
+
wget -qO- https://bun.sh/install | bash
|
|
29
|
+
else
|
|
30
|
+
err "need curl or wget to install Bun. Install one and re-run."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
37
|
+
err "Bun install did not land on PATH. Open a new shell or add $BUN_INSTALL/bin to PATH, then re-run."
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
info "installing the codehost CLI (bun add -g codehost)…"
|
|
42
|
+
bun add -g codehost
|
|
43
|
+
|
|
44
|
+
if ! command -v codehost >/dev/null 2>&1; then
|
|
45
|
+
err "codehost installed but not on PATH. Add $BUN_INSTALL/bin to your PATH and run: codehost setup"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ "${CODEHOST_NO_SETUP:-}" = "1" ]; then
|
|
50
|
+
info "installed. Run \`codehost setup\` in the directory you want to serve."
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
info "running \`codehost setup\`…"
|
|
55
|
+
exec codehost setup
|
|
@@ -7,7 +7,7 @@ import { SignalingClient } from "../../shared/signaling-client";
|
|
|
7
7
|
import { RtcDaemon } from "../rtc-daemon";
|
|
8
8
|
import { launchVscode } from "../vscode";
|
|
9
9
|
import { Tunnel } from "../tunnel";
|
|
10
|
-
import {
|
|
10
|
+
import { launchServeDaemon } from "../daemonize";
|
|
11
11
|
|
|
12
12
|
export const DEFAULT_SIGNAL_URL = "wss://signal.codehost.dev";
|
|
13
13
|
|
|
@@ -74,16 +74,15 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
|
74
74
|
|
|
75
75
|
// `-d`: re-launch this same `serve` (without -d) under oxmgr, then exit.
|
|
76
76
|
if (argv.daemon) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
process.exit(1);
|
|
77
|
+
const { ok } = launchServeDaemon({
|
|
78
|
+
dir,
|
|
79
|
+
token: argv.token,
|
|
80
|
+
signal: argv.signal,
|
|
81
|
+
name: argv.name,
|
|
82
|
+
port: argv.port,
|
|
83
|
+
host,
|
|
84
|
+
});
|
|
85
|
+
process.exit(ok ? 0 : 1);
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
// peerId is fixed up front so VS Code can be mounted under /vs/<peerId>,
|
|
@@ -135,19 +134,3 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
|
135
134
|
await new Promise<never>(() => {});
|
|
136
135
|
},
|
|
137
136
|
};
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Reconstruct the exact foreground `serve` invocation (without -d) for oxmgr to
|
|
141
|
-
* run. Uses the same runtime + entry script that launched us, so it works both
|
|
142
|
-
* for `bunx codehost` and local `bun src/cli/index.ts`.
|
|
143
|
-
*/
|
|
144
|
-
function buildForegroundCommand(dir: string, argv: ServeArgs): string {
|
|
145
|
-
const parts = [process.execPath, process.argv[1], "serve", dir, "-t", argv.token, "--signal", argv.signal];
|
|
146
|
-
if (argv.name) parts.push("--name", argv.name);
|
|
147
|
-
if (argv.port) parts.push("--port", String(argv.port));
|
|
148
|
-
return parts.map(quote).join(" ");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function quote(s: string): string {
|
|
152
|
-
return /[^A-Za-z0-9_\/:=.-]/.test(s) ? `'${s.replace(/'/g, `'\\''`)}'` : s;
|
|
153
|
-
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { CommandModule } from "yargs";
|
|
4
|
+
import { generateToken, validateToken, TOKEN_REQUIREMENTS } from "../../shared/token";
|
|
5
|
+
import { readConfig, writeConfig } from "../config";
|
|
6
|
+
import { launchServeDaemon } from "../daemonize";
|
|
7
|
+
import { resolveCodeBinary } from "../vscode-install";
|
|
8
|
+
import { DEFAULT_SIGNAL_URL } from "./serve";
|
|
9
|
+
|
|
10
|
+
const PAGE_URL = "https://codehost.dev";
|
|
11
|
+
|
|
12
|
+
interface SetupArgs {
|
|
13
|
+
dir: string;
|
|
14
|
+
token?: string;
|
|
15
|
+
newToken: boolean;
|
|
16
|
+
name?: string;
|
|
17
|
+
signal: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const setupCommand: CommandModule<{}, SetupArgs> = {
|
|
22
|
+
command: "setup [dir]",
|
|
23
|
+
describe: "One-shot: pick a token, ensure VS Code, and start a daemonized server",
|
|
24
|
+
builder: (y) =>
|
|
25
|
+
y
|
|
26
|
+
.positional("dir", { describe: "Directory to serve (defaults to cwd)", type: "string", default: "." })
|
|
27
|
+
.option("token", {
|
|
28
|
+
alias: "t",
|
|
29
|
+
describe: "Room token (generated + saved if omitted)",
|
|
30
|
+
type: "string",
|
|
31
|
+
})
|
|
32
|
+
.option("new-token", {
|
|
33
|
+
describe: "Generate a fresh token even if one is saved",
|
|
34
|
+
type: "boolean",
|
|
35
|
+
default: false,
|
|
36
|
+
})
|
|
37
|
+
.option("name", { describe: "Display name for this server (defaults to hostname)", type: "string" })
|
|
38
|
+
.option("signal", { describe: "Signaling server URL", type: "string", default: DEFAULT_SIGNAL_URL })
|
|
39
|
+
.option("port", { describe: "Fixed port for the local VS Code server", type: "number" }) as any,
|
|
40
|
+
handler: async (argv) => {
|
|
41
|
+
const dir = resolve(process.cwd(), argv.dir);
|
|
42
|
+
const host = hostname();
|
|
43
|
+
|
|
44
|
+
// 1. Resolve the room token: validate an explicit one, otherwise reuse the
|
|
45
|
+
// saved token (stable room URL) or mint and persist a strong new one.
|
|
46
|
+
const token = resolveToken(argv);
|
|
47
|
+
console.log(`[codehost] room token: ${token}`);
|
|
48
|
+
|
|
49
|
+
// 2. Make sure a working VS Code is available, installing/upgrading the
|
|
50
|
+
// managed CLI if the system `code` is missing or broken. Doing it here
|
|
51
|
+
// surfaces download progress before we hand off to the daemon.
|
|
52
|
+
console.log("[codehost] checking VS Code…");
|
|
53
|
+
const codeBin = await resolveCodeBinary();
|
|
54
|
+
console.log(`[codehost] using VS Code: ${codeBin}`);
|
|
55
|
+
|
|
56
|
+
// 3. Start the WebRTC + VS Code server under oxmgr.
|
|
57
|
+
const { ok, name } = launchServeDaemon({
|
|
58
|
+
dir,
|
|
59
|
+
token,
|
|
60
|
+
signal: argv.signal,
|
|
61
|
+
name: argv.name,
|
|
62
|
+
port: argv.port,
|
|
63
|
+
host,
|
|
64
|
+
});
|
|
65
|
+
if (!ok) process.exit(1);
|
|
66
|
+
|
|
67
|
+
// 4. Tell the user how to connect.
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(`[codehost] ✓ server "${name}" is live, serving ${dir}`);
|
|
70
|
+
console.log(`[codehost] open ${PAGE_URL} and enter your token to connect.`);
|
|
71
|
+
console.log(`[codehost] manage it with: codehost list · codehost stop ${name}`);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function resolveToken(argv: SetupArgs): string {
|
|
76
|
+
if (argv.token) {
|
|
77
|
+
const t = argv.token.trim();
|
|
78
|
+
const check = validateToken(t);
|
|
79
|
+
if (!check.ok) {
|
|
80
|
+
console.error(`[codehost] ${check.reason}`);
|
|
81
|
+
console.error(`[codehost] room token requires: ${TOKEN_REQUIREMENTS}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
// Persist an explicit token too, so later `setup` runs reuse the same room.
|
|
85
|
+
writeConfig({ ...readConfig(), token: t });
|
|
86
|
+
return t;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = readConfig();
|
|
90
|
+
if (config.token && !argv.newToken) return config.token;
|
|
91
|
+
|
|
92
|
+
const token = generateToken();
|
|
93
|
+
writeConfig({ ...config, token });
|
|
94
|
+
console.log("[codehost] generated a new room token (saved to ~/.codehost/config.json)");
|
|
95
|
+
return token;
|
|
96
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
import { resolveCodeBinary } from "../vscode-install";
|
|
3
|
+
|
|
4
|
+
export const updateCommand: CommandModule = {
|
|
5
|
+
command: "update",
|
|
6
|
+
describe: "Download the latest VS Code CLI now (ignores the daily check throttle)",
|
|
7
|
+
handler: async () => {
|
|
8
|
+
if (process.env.CODEHOST_CODE_BIN) {
|
|
9
|
+
console.log(`[codehost] CODEHOST_CODE_BIN is set; nothing to update (${process.env.CODEHOST_CODE_BIN})`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (Bun.which("code")) {
|
|
13
|
+
console.log("[codehost] using system `code` on PATH; update it with your package manager.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const bin = await resolveCodeBinary({ force: true });
|
|
17
|
+
console.log(`[codehost] up to date: ${bin}`);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Persistent CLI config under ~/.codehost (same root as the managed VS Code
|
|
6
|
+
// cache in vscode-install.ts). Currently just the reusable room token so
|
|
7
|
+
// `codehost setup` lands on a stable room URL across runs.
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILE = join(homedir(), ".codehost", "config.json");
|
|
10
|
+
|
|
11
|
+
export interface CliConfig {
|
|
12
|
+
/** Room token reused by `setup` until --new-token or an explicit -t. */
|
|
13
|
+
token?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readConfig(): CliConfig {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8")) as CliConfig;
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeConfig(config: CliConfig): void {
|
|
25
|
+
mkdirSync(dirname(CONFIG_FILE), { recursive: true });
|
|
26
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { daemonName, startDaemon } from "./oxmgr";
|
|
2
|
+
|
|
3
|
+
export interface ServeDaemonOptions {
|
|
4
|
+
/** Absolute directory to serve. */
|
|
5
|
+
dir: string;
|
|
6
|
+
/** Room token (already validated). */
|
|
7
|
+
token: string;
|
|
8
|
+
/** Signaling server URL. */
|
|
9
|
+
signal: string;
|
|
10
|
+
/** Display name; also seeds the oxmgr process label. */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Fixed local VS Code port. */
|
|
13
|
+
port?: number;
|
|
14
|
+
/** Hostname, used as a label fallback. */
|
|
15
|
+
host: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServeDaemonResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** oxmgr process name this server was registered under. */
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Launch a foreground `codehost serve` (without -d) under oxmgr so it survives
|
|
26
|
+
* the shell and restarts on failure. Shared by `serve -d` and `setup`.
|
|
27
|
+
*/
|
|
28
|
+
export function launchServeDaemon(opts: ServeDaemonOptions): ServeDaemonResult {
|
|
29
|
+
const label = opts.name ?? opts.dir.split("/").pop() ?? opts.host;
|
|
30
|
+
const name = daemonName(label);
|
|
31
|
+
const command = buildForegroundCommand(opts);
|
|
32
|
+
console.log(`[codehost] starting daemon "${name}" via oxmgr`);
|
|
33
|
+
const ok = startDaemon({ name, command, cwd: opts.dir });
|
|
34
|
+
if (ok) {
|
|
35
|
+
console.log(`[codehost] daemon started. View: codehost list · Stop: codehost stop ${name}`);
|
|
36
|
+
}
|
|
37
|
+
return { ok, name };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reconstruct the exact foreground `serve` invocation (without -d) for oxmgr to
|
|
42
|
+
* run. Uses the same runtime + entry script that launched us, so it works both
|
|
43
|
+
* for `bunx codehost` and local `bun src/cli/index.ts`.
|
|
44
|
+
*/
|
|
45
|
+
function buildForegroundCommand(opts: ServeDaemonOptions): string {
|
|
46
|
+
const parts = [process.execPath, process.argv[1], "serve", opts.dir, "-t", opts.token, "--signal", opts.signal];
|
|
47
|
+
if (opts.name) parts.push("--name", opts.name);
|
|
48
|
+
if (opts.port) parts.push("--port", String(opts.port));
|
|
49
|
+
return parts.map(quote).join(" ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function quote(s: string): string {
|
|
53
|
+
return /[^A-Za-z0-9_\/:=.-]/.test(s) ? `'${s.replace(/'/g, `'\\''`)}'` : s;
|
|
54
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import yargs from "yargs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
|
+
import { setupCommand } from "./commands/setup";
|
|
4
5
|
import { serveCommand } from "./commands/serve";
|
|
5
6
|
import { listCommand } from "./commands/list";
|
|
6
7
|
import { stopCommand } from "./commands/stop";
|
|
8
|
+
import { updateCommand } from "./commands/update";
|
|
7
9
|
|
|
8
10
|
yargs(hideBin(process.argv))
|
|
9
11
|
.scriptName("codehost")
|
|
10
12
|
.usage("$0 <command> [options]")
|
|
13
|
+
.command(setupCommand)
|
|
11
14
|
.command(serveCommand)
|
|
12
15
|
.command(listCommand)
|
|
13
16
|
.command(stopCommand)
|
|
17
|
+
.command(updateCommand)
|
|
14
18
|
.demandCommand(1, "Specify a command, e.g. `codehost serve`")
|
|
15
19
|
.strict()
|
|
16
20
|
.help()
|
package/src/cli/rtc-daemon.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import { createRequire } from "node:module";
|
|
2
4
|
import type {
|
|
3
5
|
DataChannel,
|
|
@@ -10,7 +12,61 @@ import { ICE_SERVERS, type RtcSignal } from "../shared/rtc";
|
|
|
10
12
|
// resolves correctly from the project's node_modules. So load it via require;
|
|
11
13
|
// the `import type` above is erased at build time and triggers no runtime load.
|
|
12
14
|
const require = createRequire(import.meta.url);
|
|
13
|
-
const ndc =
|
|
15
|
+
const ndc = loadNodeDataChannel();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load the native node-datachannel addon, self-healing the common failure where
|
|
19
|
+
* its prebuilt `.node` was never fetched. That happens under `bunx codehost`:
|
|
20
|
+
* bunx skips install lifecycle scripts (and `trustedDependencies` only applies
|
|
21
|
+
* to `bun install`), so node-datachannel's `install` step — which downloads the
|
|
22
|
+
* binary via `prebuild-install` — never runs. On the first load failure we run
|
|
23
|
+
* that prebuild-install ourselves, then retry. A normal `bun add -g` install
|
|
24
|
+
* already has the binary, so this is a no-op there.
|
|
25
|
+
*/
|
|
26
|
+
function loadNodeDataChannel(): typeof import("node-datachannel") {
|
|
27
|
+
try {
|
|
28
|
+
return require("node-datachannel") as typeof import("node-datachannel");
|
|
29
|
+
} catch (firstErr) {
|
|
30
|
+
if (!fetchNodeDataChannelBinary()) throw nativeLoadError(firstErr);
|
|
31
|
+
try {
|
|
32
|
+
return require("node-datachannel") as typeof import("node-datachannel");
|
|
33
|
+
} catch (secondErr) {
|
|
34
|
+
throw nativeLoadError(secondErr);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run node-datachannel's bundled `prebuild-install` to fetch the prebuilt
|
|
40
|
+
* binary. Returns true if it exited cleanly. */
|
|
41
|
+
function fetchNodeDataChannelBinary(): boolean {
|
|
42
|
+
let pkgDir: string;
|
|
43
|
+
let prebuildBin: string;
|
|
44
|
+
try {
|
|
45
|
+
pkgDir = dirname(require.resolve("node-datachannel/package.json"));
|
|
46
|
+
// prebuild-install is a dependency of node-datachannel; resolve its CLI
|
|
47
|
+
// entry from the package's own module scope.
|
|
48
|
+
prebuildBin = require.resolve("prebuild-install/bin.js", { paths: [pkgDir] });
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
console.log("[codehost] fetching node-datachannel native binary (prebuild-install)…");
|
|
53
|
+
const r = spawnSync(process.execPath, [prebuildBin, "-r", "napi"], {
|
|
54
|
+
cwd: pkgDir,
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
});
|
|
57
|
+
return r.status === 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nativeLoadError(cause: unknown): Error {
|
|
61
|
+
return new Error(
|
|
62
|
+
"Failed to load the native WebRTC module (node-datachannel). Its prebuilt " +
|
|
63
|
+
"binary could not be fetched automatically. Install codehost globally so " +
|
|
64
|
+
"install scripts run — `bun add -g codehost` (or `npm i -g codehost`) — " +
|
|
65
|
+
"and ensure network access. If your platform has no prebuilt, a C++ " +
|
|
66
|
+
"toolchain + cmake is needed to build from source. " +
|
|
67
|
+
`(cause: ${(cause as Error)?.message ?? cause})`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
14
70
|
|
|
15
71
|
export interface RtcDaemonOptions {
|
|
16
72
|
/** Relay a signal to a viewer peer via the signaling channel. */
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { readdir } from "node:fs/promises";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
// Resolves a runnable VS Code CLI binary for `code serve-web`. Strategy
|
|
8
|
+
// ("managed, prefer system"): honor an explicit override, then a system `code`
|
|
9
|
+
// on PATH, otherwise download + cache Microsoft's standalone ~31MB "VS Code
|
|
10
|
+
// CLI" binary for this platform. The standalone CLI fully supports serve-web
|
|
11
|
+
// with the same flags as a full install. Update cadence is "check but cache":
|
|
12
|
+
// the managed binary's version is re-checked against the stable channel at most
|
|
13
|
+
// once per ~24h; otherwise the cached binary is used with no network call.
|
|
14
|
+
|
|
15
|
+
const CACHE_ROOT = join(homedir(), ".codehost", "vscode");
|
|
16
|
+
const STATE_FILE = join(CACHE_ROOT, "state.json");
|
|
17
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const UPDATE_API = "https://update.code.visualstudio.com/api/update";
|
|
19
|
+
|
|
20
|
+
interface Manifest {
|
|
21
|
+
url: string;
|
|
22
|
+
version: string;
|
|
23
|
+
productVersion: string;
|
|
24
|
+
sha256hash: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface State {
|
|
28
|
+
/** Commit-style version id from the manifest. */
|
|
29
|
+
version: string;
|
|
30
|
+
/** Human version, e.g. "1.123.0". */
|
|
31
|
+
productVersion: string;
|
|
32
|
+
/** Absolute path to the extracted `code`/`code.exe`. */
|
|
33
|
+
binPath: string;
|
|
34
|
+
/** Wall-clock ms of the last manifest check. */
|
|
35
|
+
checkedAt: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return a path (or bare `"code"`) that can be spawned to run `serve-web`.
|
|
40
|
+
* Downloads and caches the standalone CLI on first use when no system VS Code
|
|
41
|
+
* is available. `opts.force` skips the 24h throttle (used by `codehost update`).
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveCodeBinary(opts: { force?: boolean } = {}): Promise<string> {
|
|
44
|
+
const override = process.env.CODEHOST_CODE_BIN;
|
|
45
|
+
if (override) {
|
|
46
|
+
if (!existsSync(override)) {
|
|
47
|
+
throw new Error(`CODEHOST_CODE_BIN points to a missing file: ${override}`);
|
|
48
|
+
}
|
|
49
|
+
return override;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Prefer a user-owned `code` on PATH — but only if it actually runs. A broken
|
|
53
|
+
// or stub `code` should fall through to a managed install/upgrade.
|
|
54
|
+
const system = Bun.which("code");
|
|
55
|
+
if (system && runsOk(system)) return system;
|
|
56
|
+
|
|
57
|
+
return ensureManagedBinary(opts.force ?? false);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function ensureManagedBinary(force: boolean): Promise<string> {
|
|
61
|
+
const key = platformKey();
|
|
62
|
+
const state = readState();
|
|
63
|
+
|
|
64
|
+
const fresh = state && existsSync(state.binPath) && Date.now() - state.checkedAt < CHECK_INTERVAL_MS;
|
|
65
|
+
if (fresh && !force) return state!.binPath;
|
|
66
|
+
|
|
67
|
+
let manifest: Manifest;
|
|
68
|
+
try {
|
|
69
|
+
manifest = await fetchManifest(key);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// Offline: fall back to whatever we have cached, otherwise fail loudly.
|
|
72
|
+
if (state && existsSync(state.binPath)) {
|
|
73
|
+
console.warn(`[codehost] could not check for VS Code updates (${(err as Error).message}); using cached ${state.productVersion}`);
|
|
74
|
+
return state.binPath;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Unable to download VS Code and none is cached: ${(err as Error).message}. ` +
|
|
78
|
+
`Install VS Code's \`code\` CLI manually, or set CODEHOST_CODE_BIN to an existing binary.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Already on the latest stable: just refresh the throttle timestamp.
|
|
83
|
+
if (state && state.version === manifest.version && existsSync(state.binPath)) {
|
|
84
|
+
writeState({ ...state, checkedAt: Date.now() });
|
|
85
|
+
return state.binPath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`[codehost] installing VS Code ${manifest.productVersion} (${key})…`);
|
|
89
|
+
const binPath = await downloadAndExtract(manifest, key);
|
|
90
|
+
writeState({
|
|
91
|
+
version: manifest.version,
|
|
92
|
+
productVersion: manifest.productVersion,
|
|
93
|
+
binPath,
|
|
94
|
+
checkedAt: Date.now(),
|
|
95
|
+
});
|
|
96
|
+
console.log(`[codehost] VS Code ${manifest.productVersion} ready at ${binPath}`);
|
|
97
|
+
return binPath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Map this host to a `cli-<os>-<arch>` key understood by the update API. */
|
|
101
|
+
function platformKey(): string {
|
|
102
|
+
const arch = process.arch; // "x64" | "arm64" | "arm" | ...
|
|
103
|
+
if (process.platform === "darwin") {
|
|
104
|
+
return arch === "arm64" ? "cli-darwin-arm64" : "cli-darwin-x64";
|
|
105
|
+
}
|
|
106
|
+
if (process.platform === "win32") {
|
|
107
|
+
return arch === "arm64" ? "cli-win32-arm64" : "cli-win32-x64";
|
|
108
|
+
}
|
|
109
|
+
// Linux: distinguish musl (Alpine) from glibc, and map arch.
|
|
110
|
+
const os = isMusl() ? "alpine" : "linux";
|
|
111
|
+
if (arch === "arm64") return `cli-${os}-arm64`;
|
|
112
|
+
if (arch === "arm") return `cli-${os}-armhf`;
|
|
113
|
+
return `cli-${os}-x64`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** True if `<bin> --version` exits cleanly — i.e. the binary actually works. */
|
|
117
|
+
function runsOk(bin: string): boolean {
|
|
118
|
+
const r = spawnSync(bin, ["--version"], { stdio: "ignore", timeout: 10_000 });
|
|
119
|
+
return r.status === 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isMusl(): boolean {
|
|
123
|
+
if (existsSync("/etc/alpine-release")) return true;
|
|
124
|
+
// `ldd --version` mentions "musl" on musl systems; ignore failures.
|
|
125
|
+
const r = spawnSync("ldd", ["--version"], { encoding: "utf8" });
|
|
126
|
+
return /musl/i.test(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function fetchManifest(key: string): Promise<Manifest> {
|
|
130
|
+
const res = await fetch(`${UPDATE_API}/${key}/stable/latest`);
|
|
131
|
+
if (!res.ok) throw new Error(`manifest HTTP ${res.status} for ${key}`);
|
|
132
|
+
const m = (await res.json()) as Partial<Manifest>;
|
|
133
|
+
if (!m.url || !m.version || !m.sha256hash) {
|
|
134
|
+
throw new Error(`malformed manifest for ${key}`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
url: m.url,
|
|
138
|
+
version: m.version,
|
|
139
|
+
productVersion: m.productVersion ?? m.version,
|
|
140
|
+
sha256hash: m.sha256hash,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function downloadAndExtract(manifest: Manifest, key: string): Promise<string> {
|
|
145
|
+
mkdirSync(CACHE_ROOT, { recursive: true });
|
|
146
|
+
const isZip = manifest.url.endsWith(".zip");
|
|
147
|
+
const tmpArchive = join(tmpdir(), `codehost-vscode-${manifest.version}${isZip ? ".zip" : ".tar.gz"}`);
|
|
148
|
+
|
|
149
|
+
const res = await fetch(manifest.url);
|
|
150
|
+
if (!res.ok) throw new Error(`download HTTP ${res.status}`);
|
|
151
|
+
await Bun.write(tmpArchive, res);
|
|
152
|
+
|
|
153
|
+
await verifySha256(tmpArchive, manifest.sha256hash);
|
|
154
|
+
|
|
155
|
+
// Extract into a fresh temp dir, then move the single binary into the cache.
|
|
156
|
+
const extractDir = join(tmpdir(), `codehost-vscode-x-${manifest.version}`);
|
|
157
|
+
rmSync(extractDir, { recursive: true, force: true });
|
|
158
|
+
mkdirSync(extractDir, { recursive: true });
|
|
159
|
+
// bsdtar (macOS / Windows 10+) extracts .zip via `tar -xf`; GNU tar handles
|
|
160
|
+
// the Linux .tar.gz with `-xzf`.
|
|
161
|
+
const tarArgs = isZip ? ["-xf", tmpArchive] : ["-xzf", tmpArchive];
|
|
162
|
+
const tar = spawnSync("tar", [...tarArgs, "-C", extractDir], { encoding: "utf8" });
|
|
163
|
+
if (tar.status !== 0) {
|
|
164
|
+
throw new Error(`failed to extract VS Code archive: ${tar.stderr ?? tar.error?.message ?? "tar error"}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const exe = process.platform === "win32" ? "code.exe" : "code";
|
|
168
|
+
const found = await findFile(extractDir, exe);
|
|
169
|
+
if (!found) throw new Error(`extracted archive did not contain ${exe}`);
|
|
170
|
+
|
|
171
|
+
const destDir = join(CACHE_ROOT, manifest.version);
|
|
172
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
173
|
+
mkdirSync(destDir, { recursive: true });
|
|
174
|
+
const destBin = join(destDir, exe);
|
|
175
|
+
renameSync(found, destBin);
|
|
176
|
+
if (process.platform !== "win32") {
|
|
177
|
+
const { chmodSync } = await import("node:fs");
|
|
178
|
+
chmodSync(destBin, 0o755);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
rmSync(tmpArchive, { force: true });
|
|
182
|
+
rmSync(extractDir, { recursive: true, force: true });
|
|
183
|
+
return destBin;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function verifySha256(path: string, expected: string): Promise<void> {
|
|
187
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
188
|
+
hasher.update(await Bun.file(path).arrayBuffer());
|
|
189
|
+
const actual = hasher.digest("hex");
|
|
190
|
+
if (actual.toLowerCase() !== expected.toLowerCase()) {
|
|
191
|
+
rmSync(path, { force: true });
|
|
192
|
+
throw new Error(`sha256 mismatch (expected ${expected}, got ${actual})`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Shallow-first search for a file named `name` under `dir`. */
|
|
197
|
+
async function findFile(dir: string, name: string): Promise<string | null> {
|
|
198
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
199
|
+
for (const e of entries) {
|
|
200
|
+
if (e.isFile() && e.name === name) return join(dir, e.name);
|
|
201
|
+
}
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
if (e.isDirectory()) {
|
|
204
|
+
const hit = await findFile(join(dir, e.name), name);
|
|
205
|
+
if (hit) return hit;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readState(): State | null {
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf8")) as State;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function writeState(state: State): void {
|
|
220
|
+
mkdirSync(CACHE_ROOT, { recursive: true });
|
|
221
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
222
|
+
}
|
package/src/cli/vscode.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, type Subprocess } from "bun";
|
|
2
|
+
import { resolveCodeBinary } from "./vscode-install";
|
|
2
3
|
|
|
3
4
|
export interface VscodeServer {
|
|
4
5
|
port: number;
|
|
@@ -37,8 +38,9 @@ export async function launchVscode(opts: LaunchOptions): Promise<VscodeServer> {
|
|
|
37
38
|
"--accept-server-license-terms",
|
|
38
39
|
];
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const codeBin = await resolveCodeBinary();
|
|
42
|
+
console.log(`[codehost] launching: ${codeBin} ${args.join(" ")}`);
|
|
43
|
+
const proc = spawn([codeBin, ...args], {
|
|
42
44
|
cwd: opts.dir,
|
|
43
45
|
stdout: "inherit",
|
|
44
46
|
stderr: "inherit",
|