codehost 0.1.1 → 0.4.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/.github/workflows/deploy.yml +30 -0
- package/.github/workflows/release.yaml +39 -0
- package/.releaserc.json +17 -0
- package/CHANGELOG.md +41 -0
- package/README.md +5 -0
- package/TODO.md +93 -0
- package/docs/vscode-cdn-proxy.md +117 -0
- package/package.json +12 -1
- package/public/_redirects +10 -0
- package/public/install.ps1 +31 -15
- package/public/install.sh +40 -15
- package/src/cli/commands/dev.ts +110 -0
- package/src/cli/commands/expose.ts +93 -0
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/serve.ts +29 -55
- package/src/cli/commands/setup.ts +9 -6
- package/src/cli/commands/stop.ts +2 -2
- package/src/cli/daemonize.ts +15 -4
- package/src/cli/git.ts +53 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/open-url.ts +47 -0
- package/src/cli/oxmgr-heal.ts +175 -0
- package/src/cli/oxmgr.ts +104 -36
- package/src/cli/run-server.ts +78 -0
- package/src/cli/self-update.ts +89 -0
- package/src/cli/tunnel.ts +30 -4
- package/src/cli/vscode.ts +4 -2
- package/src/shared/repo.test.ts +37 -0
- package/src/shared/repo.ts +123 -0
- package/src/shared/signaling.ts +18 -2
- package/src/web/config.ts +44 -7
- package/src/web/discovery.tsx +131 -6
- package/src/web/history.ts +58 -0
- package/src/web/sw.ts +36 -1
- package/worker/index.ts +49 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { CommandModule } from "yargs";
|
|
4
|
+
import type { PeerMeta } from "../../shared/signaling";
|
|
5
|
+
import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
|
|
6
|
+
import { launchServeDaemon } from "../daemonize";
|
|
7
|
+
import { announceConnect } from "../open-url";
|
|
8
|
+
import { runServer } from "../run-server";
|
|
9
|
+
import { launchVscode } from "../vscode";
|
|
10
|
+
import { repoIdentity } from "../git";
|
|
11
|
+
import { toPosixPath } from "../../shared/repo";
|
|
12
|
+
import { DEFAULT_SIGNAL_URL } from "./serve";
|
|
13
|
+
|
|
14
|
+
interface DevArgs {
|
|
15
|
+
dir: string;
|
|
16
|
+
token: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
signal: string;
|
|
19
|
+
daemon: boolean;
|
|
20
|
+
port?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const devCommand: CommandModule<{}, DevArgs> = {
|
|
24
|
+
command: "dev [dir]",
|
|
25
|
+
describe:
|
|
26
|
+
"Serve a single folder over WebRTC; open it at codehost.dev/dev/<path> (or /gh/<owner>/<repo> when it's a GitHub repo)",
|
|
27
|
+
builder: (y) =>
|
|
28
|
+
y
|
|
29
|
+
.positional("dir", {
|
|
30
|
+
describe: "Directory to serve (defaults to cwd)",
|
|
31
|
+
type: "string",
|
|
32
|
+
default: ".",
|
|
33
|
+
})
|
|
34
|
+
.option("token", {
|
|
35
|
+
alias: "t",
|
|
36
|
+
describe: "Room token shared with the codehost.dev page",
|
|
37
|
+
type: "string",
|
|
38
|
+
demandOption: true,
|
|
39
|
+
})
|
|
40
|
+
.option("name", {
|
|
41
|
+
describe: "Display name for this server (defaults to hostname)",
|
|
42
|
+
type: "string",
|
|
43
|
+
})
|
|
44
|
+
.option("signal", {
|
|
45
|
+
describe: "Signaling server URL",
|
|
46
|
+
type: "string",
|
|
47
|
+
default: DEFAULT_SIGNAL_URL,
|
|
48
|
+
})
|
|
49
|
+
.option("daemon", {
|
|
50
|
+
alias: "d",
|
|
51
|
+
describe: "Run in the background under oxmgr (auto-starts on login)",
|
|
52
|
+
type: "boolean",
|
|
53
|
+
default: false,
|
|
54
|
+
})
|
|
55
|
+
.option("port", {
|
|
56
|
+
describe: "Fixed port for the local VS Code server (default: ephemeral)",
|
|
57
|
+
type: "number",
|
|
58
|
+
}) as any,
|
|
59
|
+
handler: async (argv) => {
|
|
60
|
+
argv.token = argv.token.trim();
|
|
61
|
+
const check = validateToken(argv.token);
|
|
62
|
+
if (!check.ok) {
|
|
63
|
+
console.error(`[codehost] ${check.reason}`);
|
|
64
|
+
console.error(`[codehost] room token requires: ${TOKEN_REQUIREMENTS}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dir = resolve(process.cwd(), argv.dir);
|
|
69
|
+
const host = hostname();
|
|
70
|
+
|
|
71
|
+
if (argv.daemon) {
|
|
72
|
+
const { ok } = await launchServeDaemon({
|
|
73
|
+
command: "dev",
|
|
74
|
+
dir,
|
|
75
|
+
token: argv.token,
|
|
76
|
+
signal: argv.signal,
|
|
77
|
+
name: argv.name,
|
|
78
|
+
port: argv.port,
|
|
79
|
+
host,
|
|
80
|
+
});
|
|
81
|
+
if (ok) announceConnect(argv.token);
|
|
82
|
+
process.exit(ok ? 0 : 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A single folder: git-identified so GitHub deep links resolve to it.
|
|
86
|
+
const id = repoIdentity(dir);
|
|
87
|
+
const meta: PeerMeta = {
|
|
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.
|
|
91
|
+
cwd: toPosixPath(dir),
|
|
92
|
+
host,
|
|
93
|
+
kind: "repo",
|
|
94
|
+
repo: id.repo,
|
|
95
|
+
branch: id.branch,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
announceConnect(argv.token);
|
|
99
|
+
await runServer({
|
|
100
|
+
token: argv.token,
|
|
101
|
+
signal: argv.signal,
|
|
102
|
+
meta,
|
|
103
|
+
label: `serving ${dir}`,
|
|
104
|
+
launch: async (basePath) => {
|
|
105
|
+
const v = await launchVscode({ dir, basePath, port: argv.port });
|
|
106
|
+
return { port: v.port, stop: v.stop };
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import type { CommandModule } from "yargs";
|
|
3
|
+
import type { PeerMeta } from "../../shared/signaling";
|
|
4
|
+
import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
|
|
5
|
+
import { launchServeDaemon } from "../daemonize";
|
|
6
|
+
import { runServer } from "../run-server";
|
|
7
|
+
import { DEFAULT_SIGNAL_URL } from "./serve";
|
|
8
|
+
|
|
9
|
+
interface ExposeArgs {
|
|
10
|
+
port: number;
|
|
11
|
+
token: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
signal: string;
|
|
14
|
+
daemon: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const exposeCommand: CommandModule<{}, ExposeArgs> = {
|
|
18
|
+
command: "expose <port>",
|
|
19
|
+
describe:
|
|
20
|
+
"Tunnel an existing local HTTP/WS server (any port) over WebRTC — reachable at codehost.dev/vs/<peerId>/",
|
|
21
|
+
builder: (y) =>
|
|
22
|
+
y
|
|
23
|
+
.positional("port", {
|
|
24
|
+
describe: "Local port to expose (e.g. 7432)",
|
|
25
|
+
type: "number",
|
|
26
|
+
demandOption: true,
|
|
27
|
+
})
|
|
28
|
+
.option("token", {
|
|
29
|
+
alias: "t",
|
|
30
|
+
describe: "Room token shared with the codehost.dev page",
|
|
31
|
+
type: "string",
|
|
32
|
+
demandOption: true,
|
|
33
|
+
})
|
|
34
|
+
.option("name", {
|
|
35
|
+
describe: "Display name for this server (defaults to localhost:<port>)",
|
|
36
|
+
type: "string",
|
|
37
|
+
})
|
|
38
|
+
.option("signal", {
|
|
39
|
+
describe: "Signaling server URL",
|
|
40
|
+
type: "string",
|
|
41
|
+
default: DEFAULT_SIGNAL_URL,
|
|
42
|
+
})
|
|
43
|
+
.option("daemon", {
|
|
44
|
+
alias: "d",
|
|
45
|
+
describe: "Run in the background under oxmgr (auto-starts on login)",
|
|
46
|
+
type: "boolean",
|
|
47
|
+
default: false,
|
|
48
|
+
}) as any,
|
|
49
|
+
handler: async (argv) => {
|
|
50
|
+
argv.token = argv.token.trim();
|
|
51
|
+
const check = validateToken(argv.token);
|
|
52
|
+
if (!check.ok) {
|
|
53
|
+
console.error(`[codehost] ${check.reason}`);
|
|
54
|
+
console.error(`[codehost] room token requires: ${TOKEN_REQUIREMENTS}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isInteger(argv.port) || argv.port <= 0 || argv.port > 65535) {
|
|
58
|
+
console.error(`[codehost] invalid port: ${argv.port}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const host = hostname();
|
|
63
|
+
|
|
64
|
+
if (argv.daemon) {
|
|
65
|
+
const { ok } = await launchServeDaemon({
|
|
66
|
+
command: "expose",
|
|
67
|
+
dir: process.cwd(),
|
|
68
|
+
arg: String(argv.port),
|
|
69
|
+
token: argv.token,
|
|
70
|
+
signal: argv.signal,
|
|
71
|
+
name: argv.name,
|
|
72
|
+
host,
|
|
73
|
+
});
|
|
74
|
+
process.exit(ok ? 0 : 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const meta: PeerMeta = {
|
|
78
|
+
name: argv.name ?? `localhost:${argv.port}`,
|
|
79
|
+
cwd: `localhost:${argv.port}`,
|
|
80
|
+
host,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// No VS Code: tunnel directly to the given port, stripping the /vs/<peerId>
|
|
84
|
+
// prefix the server doesn't know about.
|
|
85
|
+
await runServer({
|
|
86
|
+
token: argv.token,
|
|
87
|
+
signal: argv.signal,
|
|
88
|
+
meta,
|
|
89
|
+
label: `exposing localhost:${argv.port}`,
|
|
90
|
+
launch: async (basePath) => ({ port: argv.port, stripBasePath: basePath }),
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
package/src/cli/commands/list.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { hostname } from "node:os";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import type { CommandModule } from "yargs";
|
|
4
|
-
import {
|
|
4
|
+
import type { PeerMeta } from "../../shared/signaling";
|
|
5
|
+
import { DEFAULT_LAYOUT, toPosixPath } from "../../shared/repo";
|
|
5
6
|
import { TOKEN_REQUIREMENTS, validateToken } from "../../shared/token";
|
|
6
|
-
import { SignalingClient } from "../../shared/signaling-client";
|
|
7
|
-
import { RtcDaemon } from "../rtc-daemon";
|
|
8
|
-
import { launchVscode } from "../vscode";
|
|
9
|
-
import { Tunnel } from "../tunnel";
|
|
10
7
|
import { launchServeDaemon } from "../daemonize";
|
|
8
|
+
import { announceConnect } from "../open-url";
|
|
9
|
+
import { runServer } from "../run-server";
|
|
10
|
+
import { launchVscode } from "../vscode";
|
|
11
11
|
|
|
12
12
|
export const DEFAULT_SIGNAL_URL = "wss://signal.codehost.dev";
|
|
13
13
|
|
|
@@ -22,7 +22,8 @@ interface ServeArgs {
|
|
|
22
22
|
|
|
23
23
|
export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
24
24
|
command: "serve [dir]",
|
|
25
|
-
describe:
|
|
25
|
+
describe:
|
|
26
|
+
"Serve a workspace root over WebRTC; repos under it open via codehost.dev/gh/<owner>/<repo>",
|
|
26
27
|
builder: (y) =>
|
|
27
28
|
y
|
|
28
29
|
.positional("dir", {
|
|
@@ -47,7 +48,7 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
|
47
48
|
})
|
|
48
49
|
.option("daemon", {
|
|
49
50
|
alias: "d",
|
|
50
|
-
describe: "Run in the background under oxmgr",
|
|
51
|
+
describe: "Run in the background under oxmgr (auto-starts on login)",
|
|
51
52
|
type: "boolean",
|
|
52
53
|
default: false,
|
|
53
54
|
})
|
|
@@ -66,15 +67,11 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
|
66
67
|
|
|
67
68
|
const dir = resolve(process.cwd(), argv.dir);
|
|
68
69
|
const host = hostname();
|
|
69
|
-
const meta: PeerMeta = {
|
|
70
|
-
name: argv.name ?? host,
|
|
71
|
-
cwd: dir,
|
|
72
|
-
host,
|
|
73
|
-
};
|
|
74
70
|
|
|
75
71
|
// `-d`: re-launch this same `serve` (without -d) under oxmgr, then exit.
|
|
76
72
|
if (argv.daemon) {
|
|
77
|
-
const { ok } = launchServeDaemon({
|
|
73
|
+
const { ok } = await launchServeDaemon({
|
|
74
|
+
command: "serve",
|
|
78
75
|
dir,
|
|
79
76
|
token: argv.token,
|
|
80
77
|
signal: argv.signal,
|
|
@@ -82,55 +79,32 @@ export const serveCommand: CommandModule<{}, ServeArgs> = {
|
|
|
82
79
|
port: argv.port,
|
|
83
80
|
host,
|
|
84
81
|
});
|
|
82
|
+
if (ok) announceConnect(argv.token);
|
|
85
83
|
process.exit(ok ? 0 : 1);
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let rtc: RtcDaemon;
|
|
86
|
+
// A workspace root: repos under it open by GitHub-shaped deep link, mapped
|
|
87
|
+
// onto subfolders via VS Code's ?folder= using this layout.
|
|
88
|
+
const meta: PeerMeta = {
|
|
89
|
+
name: argv.name ?? host,
|
|
90
|
+
// POSIX-drive form for the browser ?folder= URI (C:\ws -> /c/ws); the real
|
|
91
|
+
// OS path `dir` is still what we spawn VS Code in.
|
|
92
|
+
cwd: toPosixPath(dir),
|
|
93
|
+
host,
|
|
94
|
+
kind: "root",
|
|
95
|
+
layout: DEFAULT_LAYOUT,
|
|
96
|
+
};
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
announceConnect(argv.token);
|
|
99
|
+
await runServer({
|
|
103
100
|
token: argv.token,
|
|
104
|
-
|
|
105
|
-
peerId,
|
|
101
|
+
signal: argv.signal,
|
|
106
102
|
meta,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
rtc = new RtcDaemon({
|
|
113
|
-
sendSignal: (to, data) => client.sendSignal(to, data),
|
|
114
|
-
// Each viewer's data channel is bridged to the local VS Code server.
|
|
115
|
-
onChannel: (viewerId, channel) => {
|
|
116
|
-
console.log(`[codehost] viewer ${viewerId.slice(0, 8)} connected; bridging to VS Code`);
|
|
117
|
-
new Tunnel(channel, vscode.port);
|
|
103
|
+
label: `serving workspace root ${dir}`,
|
|
104
|
+
launch: async (basePath) => {
|
|
105
|
+
const v = await launchVscode({ dir, basePath, port: argv.port });
|
|
106
|
+
return { port: v.port, stop: v.stop };
|
|
118
107
|
},
|
|
119
108
|
});
|
|
120
|
-
|
|
121
|
-
client.connect();
|
|
122
|
-
|
|
123
|
-
const shutdown = () => {
|
|
124
|
-
console.log("\n[codehost] shutting down");
|
|
125
|
-
rtc.closeAll();
|
|
126
|
-
client.close();
|
|
127
|
-
vscode.stop();
|
|
128
|
-
process.exit(0);
|
|
129
|
-
};
|
|
130
|
-
process.on("SIGINT", shutdown);
|
|
131
|
-
process.on("SIGTERM", shutdown);
|
|
132
|
-
|
|
133
|
-
// Keep the process alive.
|
|
134
|
-
await new Promise<never>(() => {});
|
|
135
109
|
},
|
|
136
110
|
};
|
|
@@ -4,11 +4,11 @@ import type { CommandModule } from "yargs";
|
|
|
4
4
|
import { generateToken, validateToken, TOKEN_REQUIREMENTS } from "../../shared/token";
|
|
5
5
|
import { readConfig, writeConfig } from "../config";
|
|
6
6
|
import { launchServeDaemon } from "../daemonize";
|
|
7
|
+
import { isGitRepo } from "../git";
|
|
7
8
|
import { resolveCodeBinary } from "../vscode-install";
|
|
9
|
+
import { announceConnect } from "../open-url";
|
|
8
10
|
import { DEFAULT_SIGNAL_URL } from "./serve";
|
|
9
11
|
|
|
10
|
-
const PAGE_URL = "https://codehost.dev";
|
|
11
|
-
|
|
12
12
|
interface SetupArgs {
|
|
13
13
|
dir: string;
|
|
14
14
|
token?: string;
|
|
@@ -53,8 +53,10 @@ export const setupCommand: CommandModule<{}, SetupArgs> = {
|
|
|
53
53
|
const codeBin = await resolveCodeBinary();
|
|
54
54
|
console.log(`[codehost] using VS Code: ${codeBin}`);
|
|
55
55
|
|
|
56
|
-
// 3. Start the WebRTC + VS Code server under oxmgr.
|
|
57
|
-
|
|
56
|
+
// 3. Start the WebRTC + VS Code server under oxmgr. A git repo is a single
|
|
57
|
+
// workspace (`dev`); anything else is treated as a root (`serve`).
|
|
58
|
+
const { ok, name } = await launchServeDaemon({
|
|
59
|
+
command: isGitRepo(dir) ? "dev" : "serve",
|
|
58
60
|
dir,
|
|
59
61
|
token,
|
|
60
62
|
signal: argv.signal,
|
|
@@ -64,10 +66,11 @@ export const setupCommand: CommandModule<{}, SetupArgs> = {
|
|
|
64
66
|
});
|
|
65
67
|
if (!ok) process.exit(1);
|
|
66
68
|
|
|
67
|
-
// 4. Tell the user how to connect
|
|
69
|
+
// 4. Tell the user how to connect, and open the browser straight at the
|
|
70
|
+
// token-carrying URL so VS Code loads without typing the token in.
|
|
68
71
|
console.log("");
|
|
69
72
|
console.log(`[codehost] ✓ server "${name}" is live, serving ${dir}`);
|
|
70
|
-
|
|
73
|
+
announceConnect(token);
|
|
71
74
|
console.log(`[codehost] manage it with: codehost list · codehost stop ${name}`);
|
|
72
75
|
},
|
|
73
76
|
};
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const stopCommand: CommandModule<{}, StopArgs> = {
|
|
|
14
14
|
type: "string",
|
|
15
15
|
demandOption: true,
|
|
16
16
|
}) as any,
|
|
17
|
-
handler: (argv) => {
|
|
18
|
-
process.exit(stopDaemon(argv.name));
|
|
17
|
+
handler: async (argv) => {
|
|
18
|
+
process.exit(await stopDaemon(argv.name));
|
|
19
19
|
},
|
|
20
20
|
};
|
package/src/cli/daemonize.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { daemonName, startDaemon } from "./oxmgr";
|
|
2
|
+
import { selfUpdate } from "./self-update";
|
|
2
3
|
|
|
3
4
|
export interface ServeDaemonOptions {
|
|
4
|
-
/**
|
|
5
|
+
/** Subcommand to re-launch under oxmgr. */
|
|
6
|
+
command?: "serve" | "dev" | "expose";
|
|
7
|
+
/** Absolute directory to serve (also the oxmgr working dir). */
|
|
5
8
|
dir: string;
|
|
9
|
+
/** Positional argument for the re-launched command (defaults to `dir`); for
|
|
10
|
+
* `expose` this is the port, while `dir` stays a real cwd for oxmgr. */
|
|
11
|
+
arg?: string;
|
|
6
12
|
/** Room token (already validated). */
|
|
7
13
|
token: string;
|
|
8
14
|
/** Signaling server URL. */
|
|
@@ -25,12 +31,17 @@ export interface ServeDaemonResult {
|
|
|
25
31
|
* Launch a foreground `codehost serve` (without -d) under oxmgr so it survives
|
|
26
32
|
* the shell and restarts on failure. Shared by `serve -d` and `setup`.
|
|
27
33
|
*/
|
|
28
|
-
export function launchServeDaemon(opts: ServeDaemonOptions): ServeDaemonResult {
|
|
34
|
+
export async function launchServeDaemon(opts: ServeDaemonOptions): Promise<ServeDaemonResult> {
|
|
35
|
+
// Upgrade the global install (if that's how we're running) before spawning, so
|
|
36
|
+
// the fresh daemon runs the latest code. startDaemon does delete+start, so a
|
|
37
|
+
// re-launch replaces any live daemon with the updated one. Non-fatal.
|
|
38
|
+
await selfUpdate();
|
|
39
|
+
|
|
29
40
|
const label = opts.name ?? opts.dir.split("/").pop() ?? opts.host;
|
|
30
41
|
const name = daemonName(label);
|
|
31
42
|
const command = buildForegroundCommand(opts);
|
|
32
43
|
console.log(`[codehost] starting daemon "${name}" via oxmgr`);
|
|
33
|
-
const ok = startDaemon({ name, command, cwd: opts.dir });
|
|
44
|
+
const ok = await startDaemon({ name, command, cwd: opts.dir });
|
|
34
45
|
if (ok) {
|
|
35
46
|
console.log(`[codehost] daemon started. View: codehost list · Stop: codehost stop ${name}`);
|
|
36
47
|
}
|
|
@@ -43,7 +54,7 @@ export function launchServeDaemon(opts: ServeDaemonOptions): ServeDaemonResult {
|
|
|
43
54
|
* for `bunx codehost` and local `bun src/cli/index.ts`.
|
|
44
55
|
*/
|
|
45
56
|
function buildForegroundCommand(opts: ServeDaemonOptions): string {
|
|
46
|
-
const parts = [process.execPath, process.argv[1], "serve", opts.dir, "-t", opts.token, "--signal", opts.signal];
|
|
57
|
+
const parts = [process.execPath, process.argv[1], opts.command ?? "serve", opts.arg ?? opts.dir, "-t", opts.token, "--signal", opts.signal];
|
|
47
58
|
if (opts.name) parts.push("--name", opts.name);
|
|
48
59
|
if (opts.port) parts.push("--port", String(opts.port));
|
|
49
60
|
return parts.map(quote).join(" ");
|
package/src/cli/git.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// Derive a normalized repo identity ("gh/<owner>/<repo>") + branch from a git
|
|
4
|
+
// working tree, so a `codehost dev` daemon can be addressed by GitHub-shaped
|
|
5
|
+
// deep links. Best-effort: returns undefined fields off git / off GitHub.
|
|
6
|
+
|
|
7
|
+
export interface RepoIdentity {
|
|
8
|
+
/** Normalized identity, e.g. "gh/snomiao/codehost". */
|
|
9
|
+
repo?: string;
|
|
10
|
+
/** Current branch, e.g. "main". */
|
|
11
|
+
branch?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** True if `dir` is inside a git working tree (has a resolvable .git). */
|
|
15
|
+
export function isGitRepo(dir: string): boolean {
|
|
16
|
+
const r = git(dir, ["rev-parse", "--is-inside-work-tree"]);
|
|
17
|
+
return r.trim() === "true";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function repoIdentity(dir: string): RepoIdentity {
|
|
21
|
+
if (!isGitRepo(dir)) return {};
|
|
22
|
+
const remote =
|
|
23
|
+
git(dir, ["remote", "get-url", "origin"]) || git(dir, ["config", "--get", "remote.origin.url"]);
|
|
24
|
+
const branch =
|
|
25
|
+
git(dir, ["rev-parse", "--abbrev-ref", "HEAD"]) || git(dir, ["symbolic-ref", "--short", "HEAD"]);
|
|
26
|
+
return {
|
|
27
|
+
repo: parseGitHubRemote(remote),
|
|
28
|
+
branch: branch && branch !== "HEAD" ? branch : undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a GitHub remote URL into "gh/<owner>/<repo>". Handles:
|
|
34
|
+
* https://github.com/owner/repo(.git)
|
|
35
|
+
* git@github.com:owner/repo(.git)
|
|
36
|
+
* ssh://git@github.com/owner/repo(.git)
|
|
37
|
+
* Returns undefined for non-GitHub or unparseable remotes.
|
|
38
|
+
*/
|
|
39
|
+
export function parseGitHubRemote(url: string): string | undefined {
|
|
40
|
+
const u = url.trim();
|
|
41
|
+
if (!u) return undefined;
|
|
42
|
+
const m = u.match(/github\.com[/:]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
|
|
43
|
+
if (!m) return undefined;
|
|
44
|
+
const owner = m[1];
|
|
45
|
+
const repo = m[2];
|
|
46
|
+
if (!owner || !repo) return undefined;
|
|
47
|
+
return `gh/${owner}/${repo}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function git(dir: string, args: string[]): string {
|
|
51
|
+
const r = spawnSync("git", ["-C", dir, ...args], { encoding: "utf8" });
|
|
52
|
+
return r.status === 0 ? (r.stdout ?? "").trim() : "";
|
|
53
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import yargs from "yargs";
|
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
import { setupCommand } from "./commands/setup";
|
|
5
5
|
import { serveCommand } from "./commands/serve";
|
|
6
|
+
import { devCommand } from "./commands/dev";
|
|
7
|
+
import { exposeCommand } from "./commands/expose";
|
|
6
8
|
import { listCommand } from "./commands/list";
|
|
7
9
|
import { stopCommand } from "./commands/stop";
|
|
8
10
|
import { updateCommand } from "./commands/update";
|
|
@@ -12,6 +14,8 @@ yargs(hideBin(process.argv))
|
|
|
12
14
|
.usage("$0 <command> [options]")
|
|
13
15
|
.command(setupCommand)
|
|
14
16
|
.command(serveCommand)
|
|
17
|
+
.command(devCommand)
|
|
18
|
+
.command(exposeCommand)
|
|
15
19
|
.command(listCommand)
|
|
16
20
|
.command(stopCommand)
|
|
17
21
|
.command(updateCommand)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** Public codehost page that brokers the WebRTC handshake. */
|
|
4
|
+
export const PAGE_URL = "https://codehost.dev";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the auto-connect URL for a room token. The token rides in the URL
|
|
8
|
+
* *fragment* (`#t=<token>`) on purpose: the page is a static asset, so the
|
|
9
|
+
* fragment never leaves the browser — it isn't sent to Cloudflare, nor written
|
|
10
|
+
* to access logs or `Referer` headers. The page reads it, fills the token, and
|
|
11
|
+
* auto-connects when a single server is live.
|
|
12
|
+
*/
|
|
13
|
+
export function connectUrl(token: string, page: string = PAGE_URL): string {
|
|
14
|
+
return `${page}/#t=${encodeURIComponent(token)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Open `url` in the default browser. Best-effort, cross-platform, detached. */
|
|
18
|
+
export function openBrowser(url: string): void {
|
|
19
|
+
try {
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
// `start` is a cmd builtin; the empty "" is the (ignored) window title so
|
|
22
|
+
// a quoted URL isn't mistaken for one.
|
|
23
|
+
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, windowsHide: true }).unref();
|
|
24
|
+
} else if (process.platform === "darwin") {
|
|
25
|
+
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
26
|
+
} else {
|
|
27
|
+
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// best-effort; the URL is always printed too.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Print the connect URL and, in an interactive terminal, open it in the
|
|
36
|
+
* browser. The TTY guard keeps the oxmgr-spawned daemon (which re-runs
|
|
37
|
+
* `serve`/`dev` in the foreground with no TTY) from popping a tab on every
|
|
38
|
+
* restart — it just logs the URL.
|
|
39
|
+
*/
|
|
40
|
+
export function announceConnect(token: string, page: string = PAGE_URL): void {
|
|
41
|
+
const url = connectUrl(token, page);
|
|
42
|
+
console.log(`[codehost] connect: ${url}`);
|
|
43
|
+
if (process.stdout.isTTY) {
|
|
44
|
+
console.log("[codehost] opening your browser…");
|
|
45
|
+
openBrowser(url);
|
|
46
|
+
}
|
|
47
|
+
}
|