codehost 0.5.1 → 0.7.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/package.json +3 -2
- package/src/cli/commands/list.ts +18 -3
- package/src/cli/commands/stop.ts +9 -1
- package/src/cli/commands/supervise.ts +24 -0
- package/src/cli/daemonize.ts +29 -11
- package/src/cli/fallback-daemon.ts +139 -0
- package/src/cli/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [0.7.0](https://github.com/snomiao/codehost/compare/v0.6.0...v0.7.0) (2026-06-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add 'ch' bin alias for codehost CLI ([b66983d](https://github.com/snomiao/codehost/commit/b66983dd4d56073a9b1e8257eaf858440249968a))
|
|
7
|
+
|
|
8
|
+
# [0.6.0](https://github.com/snomiao/codehost/compare/v0.5.1...v0.6.0) (2026-06-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* detached fallback daemon when oxmgr is unavailable ([3aeddad](https://github.com/snomiao/codehost/commit/3aeddad595a2fc65f8a65050a5cc6171f41bca8f))
|
|
14
|
+
|
|
1
15
|
## [0.5.1](https://github.com/snomiao/codehost/compare/v0.5.0...v0.5.1) (2026-06-08)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"homepage": "https://codehost.dev",
|
|
10
10
|
"bugs": "https://github.com/snomiao/codehost/issues",
|
|
11
11
|
"bin": {
|
|
12
|
-
"codehost": "./src/cli/index.ts"
|
|
12
|
+
"codehost": "./src/cli/index.ts",
|
|
13
|
+
"ch": "./src/cli/index.ts"
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"dev": "concurrently \"bun run dev:server\" \"vite --port 5173\"",
|
package/src/cli/commands/list.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs";
|
|
2
|
-
import { listDaemons } from "../oxmgr";
|
|
2
|
+
import { hasOxmgr, listDaemons } from "../oxmgr";
|
|
3
|
+
import { listFallbackDaemons } from "../fallback-daemon";
|
|
3
4
|
|
|
4
5
|
export const listCommand: CommandModule = {
|
|
5
6
|
command: "list",
|
|
6
7
|
aliases: ["ls"],
|
|
7
|
-
describe: "List codehost servers
|
|
8
|
+
describe: "List codehost servers (oxmgr-managed and detached)",
|
|
8
9
|
handler: async () => {
|
|
9
|
-
|
|
10
|
+
const detached = listFallbackDaemons();
|
|
11
|
+
if (detached.length) {
|
|
12
|
+
console.log("Detached daemons (no oxmgr):");
|
|
13
|
+
for (const d of detached) {
|
|
14
|
+
console.log(` ${d.name} pid ${d.pid} ${d.cwd} (log: ${d.log})`);
|
|
15
|
+
}
|
|
16
|
+
console.log("");
|
|
17
|
+
}
|
|
18
|
+
// Only hit oxmgr if it's actually runnable — `hasOxmgr` doesn't self-heal,
|
|
19
|
+
// so a broken install won't re-download its binary on every `list`.
|
|
20
|
+
if (await hasOxmgr()) {
|
|
21
|
+
process.exit(await listDaemons());
|
|
22
|
+
}
|
|
23
|
+
if (!detached.length) console.log("No codehost daemons running.");
|
|
24
|
+
process.exit(0);
|
|
10
25
|
},
|
|
11
26
|
};
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs";
|
|
2
|
-
import { stopDaemon } from "../oxmgr";
|
|
2
|
+
import { daemonName, stopDaemon } from "../oxmgr";
|
|
3
|
+
import { stopFallbackDaemon } from "../fallback-daemon";
|
|
3
4
|
|
|
4
5
|
interface StopArgs {
|
|
5
6
|
name: string;
|
|
@@ -15,6 +16,13 @@ export const stopCommand: CommandModule<{}, StopArgs> = {
|
|
|
15
16
|
demandOption: true,
|
|
16
17
|
}) as any,
|
|
17
18
|
handler: async (argv) => {
|
|
19
|
+
// Detached daemons are tracked locally; check those first so we don't poke
|
|
20
|
+
// (and possibly re-download) oxmgr when it isn't even managing this one.
|
|
21
|
+
const full = argv.name.startsWith("codehost-") ? argv.name : daemonName(argv.name);
|
|
22
|
+
if (stopFallbackDaemon(full) || stopFallbackDaemon(argv.name)) {
|
|
23
|
+
console.log(`[codehost] stopped ${full}`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
18
26
|
process.exit(await stopDaemon(argv.name));
|
|
19
27
|
},
|
|
20
28
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
import { runSupervisor } from "../fallback-daemon";
|
|
3
|
+
|
|
4
|
+
interface SuperviseArgs {
|
|
5
|
+
name: string;
|
|
6
|
+
argv: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Hidden internal command: the supervisor process behind a detached fallback
|
|
10
|
+
// daemon (see fallback-daemon.ts). Not meant to be run by hand — `serve -d` /
|
|
11
|
+
// `setup` spawn it when oxmgr isn't available.
|
|
12
|
+
export const superviseCommand: CommandModule<{}, SuperviseArgs> = {
|
|
13
|
+
command: "__supervise",
|
|
14
|
+
describe: false, // hidden from help
|
|
15
|
+
builder: (y) =>
|
|
16
|
+
y
|
|
17
|
+
.option("name", { type: "string", demandOption: true })
|
|
18
|
+
.option("argv", { type: "string", demandOption: true, describe: "JSON-encoded serve argv" }) as any,
|
|
19
|
+
handler: async (a) => {
|
|
20
|
+
const argv = JSON.parse(a.argv) as string[];
|
|
21
|
+
const code = await runSupervisor(a.name, argv);
|
|
22
|
+
process.exit(code);
|
|
23
|
+
},
|
|
24
|
+
};
|
package/src/cli/daemonize.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { daemonName, startDaemon } from "./oxmgr";
|
|
2
2
|
import { selfUpdate } from "./self-update";
|
|
3
|
+
import { startFallbackDaemon } from "./fallback-daemon";
|
|
3
4
|
|
|
4
5
|
export interface ServeDaemonOptions {
|
|
5
6
|
/** Subcommand to re-launch under oxmgr. */
|
|
@@ -28,8 +29,11 @@ export interface ServeDaemonResult {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* Launch a foreground `codehost serve` (without -d)
|
|
32
|
-
*
|
|
32
|
+
* Launch a foreground `codehost serve` (without -d) so it survives the shell and
|
|
33
|
+
* restarts on failure. Prefers oxmgr (which also adds login auto-start); when
|
|
34
|
+
* oxmgr can't run here (e.g. broken native binary on Windows) it falls back to a
|
|
35
|
+
* detached, self-restarting child instead of failing. `CODEHOST_NO_OXMGR=1`
|
|
36
|
+
* forces the fallback. Shared by `serve -d` and `setup`.
|
|
33
37
|
*/
|
|
34
38
|
export async function launchServeDaemon(opts: ServeDaemonOptions): Promise<ServeDaemonResult> {
|
|
35
39
|
// Upgrade the global install (if that's how we're running) before spawning, so
|
|
@@ -39,25 +43,39 @@ export async function launchServeDaemon(opts: ServeDaemonOptions): Promise<Serve
|
|
|
39
43
|
|
|
40
44
|
const label = opts.name ?? opts.dir.split("/").pop() ?? opts.host;
|
|
41
45
|
const name = daemonName(label);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const argv = buildForegroundArgv(opts);
|
|
47
|
+
|
|
48
|
+
if (process.env.CODEHOST_NO_OXMGR !== "1") {
|
|
49
|
+
console.log(`[codehost] starting daemon "${name}" via oxmgr`);
|
|
50
|
+
// startDaemon attempts to self-heal oxmgr once; false means it's unusable here.
|
|
51
|
+
const ok = await startDaemon({ name, command: argv.map(quote).join(" "), cwd: opts.dir });
|
|
52
|
+
if (ok) {
|
|
53
|
+
console.log(`[codehost] daemon started. View: codehost list · Stop: codehost stop ${name}`);
|
|
54
|
+
return { ok: true, name };
|
|
55
|
+
}
|
|
56
|
+
console.warn("[codehost] oxmgr unavailable — falling back to a detached daemon (no login auto-start).");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ok = startFallbackDaemon({ name, argv, cwd: opts.dir });
|
|
45
60
|
if (ok) {
|
|
46
|
-
console.log(`[codehost] daemon started. View: codehost list · Stop: codehost stop ${name}`);
|
|
61
|
+
console.log(`[codehost] detached daemon "${name}" started. View: codehost list · Stop: codehost stop ${name}`);
|
|
62
|
+
} else {
|
|
63
|
+
console.error("[codehost] failed to start a detached daemon.");
|
|
47
64
|
}
|
|
48
65
|
return { ok, name };
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
/**
|
|
52
|
-
* Reconstruct the exact foreground `serve` invocation (without -d)
|
|
53
|
-
*
|
|
54
|
-
* for `bunx codehost` and local `bun src/cli/index.ts`.
|
|
69
|
+
* Reconstruct the exact foreground `serve` invocation (without -d) as an argv
|
|
70
|
+
* array. Uses the same runtime + entry script that launched us, so it works both
|
|
71
|
+
* for `bunx codehost` and local `bun src/cli/index.ts`. oxmgr takes a shell
|
|
72
|
+
* string (we quote+join); the fallback spawns the argv directly.
|
|
55
73
|
*/
|
|
56
|
-
function
|
|
74
|
+
function buildForegroundArgv(opts: ServeDaemonOptions): string[] {
|
|
57
75
|
const parts = [process.execPath, process.argv[1], opts.command ?? "serve", opts.arg ?? opts.dir, "-t", opts.token, "--signal", opts.signal];
|
|
58
76
|
if (opts.name) parts.push("--name", opts.name);
|
|
59
77
|
if (opts.port) parts.push("--port", String(opts.port));
|
|
60
|
-
return parts
|
|
78
|
+
return parts;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
function quote(s: string): string {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn, type Subprocess } from "bun";
|
|
2
|
+
import { mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// A minimal, non-oxmgr daemon manager: a detached, self-restarting child that
|
|
7
|
+
// survives the shell. Used as a fallback when oxmgr's native binary can't run
|
|
8
|
+
// here (e.g. broken on a Windows box) so `-d`/`setup` still leave a running
|
|
9
|
+
// server instead of failing or re-download-looping. Tracked in a JSON registry
|
|
10
|
+
// so `codehost list`/`stop` can see and manage these alongside oxmgr daemons.
|
|
11
|
+
//
|
|
12
|
+
// Tradeoff vs oxmgr: no login auto-start (that's oxmgr's per-OS service bit). It
|
|
13
|
+
// does survive the launching shell and restarts the server on crash.
|
|
14
|
+
|
|
15
|
+
const ROOT = join(homedir(), ".codehost");
|
|
16
|
+
const REGISTRY = join(ROOT, "daemons.json");
|
|
17
|
+
const LOG_DIR = join(ROOT, "logs");
|
|
18
|
+
|
|
19
|
+
export interface FallbackDaemon {
|
|
20
|
+
name: string;
|
|
21
|
+
/** Supervisor process pid. */
|
|
22
|
+
pid: number;
|
|
23
|
+
cwd: string;
|
|
24
|
+
/** The foreground serve argv the supervisor (re)spawns. */
|
|
25
|
+
argv: string[];
|
|
26
|
+
log: string;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readRegistry(): FallbackDaemon[] {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(REGISTRY, "utf8")) as FallbackDaemon[];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeRegistry(list: FallbackDaemon[]): void {
|
|
39
|
+
mkdirSync(dirname(REGISTRY), { recursive: true });
|
|
40
|
+
writeFileSync(REGISTRY, JSON.stringify(list, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True if a pid is currently alive (signal 0 probe). */
|
|
44
|
+
export function isAlive(pid: number): boolean {
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start (replacing any same-named instance) a detached, self-restarting daemon
|
|
55
|
+
* that runs `argv` from `cwd`, with output appended to a per-daemon log. Returns
|
|
56
|
+
* false if the supervisor couldn't be spawned.
|
|
57
|
+
*/
|
|
58
|
+
export function startFallbackDaemon(opts: { name: string; argv: string[]; cwd: string }): boolean {
|
|
59
|
+
stopFallbackDaemon(opts.name); // replace any previous instance with this name
|
|
60
|
+
|
|
61
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
62
|
+
const log = join(LOG_DIR, `${opts.name}.log`);
|
|
63
|
+
const fd = openSync(log, "a");
|
|
64
|
+
const proc = spawn(
|
|
65
|
+
[process.execPath, process.argv[1], "__supervise", "--name", opts.name, "--argv", JSON.stringify(opts.argv)],
|
|
66
|
+
{ cwd: opts.cwd, stdin: "ignore", stdout: fd, stderr: fd },
|
|
67
|
+
);
|
|
68
|
+
// Detach so the launching process (setup / serve -d) can exit while this keeps
|
|
69
|
+
// running as an orphan.
|
|
70
|
+
proc.unref();
|
|
71
|
+
if (!proc.pid) return false;
|
|
72
|
+
|
|
73
|
+
const list = readRegistry().filter((d) => d.name !== opts.name);
|
|
74
|
+
list.push({ name: opts.name, pid: proc.pid, cwd: opts.cwd, argv: opts.argv, log, startedAt: Date.now() });
|
|
75
|
+
writeRegistry(list);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Live detached daemons (dead registry entries are pruned as a side effect). */
|
|
80
|
+
export function listFallbackDaemons(): FallbackDaemon[] {
|
|
81
|
+
const list = readRegistry();
|
|
82
|
+
const alive = list.filter((d) => isAlive(d.pid));
|
|
83
|
+
if (alive.length !== list.length) writeRegistry(alive);
|
|
84
|
+
return alive;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Stop and deregister a detached daemon by name. Returns false if not found. */
|
|
88
|
+
export function stopFallbackDaemon(name: string): boolean {
|
|
89
|
+
const list = readRegistry();
|
|
90
|
+
const hit = list.find((d) => d.name === name);
|
|
91
|
+
if (!hit) return false;
|
|
92
|
+
try {
|
|
93
|
+
process.kill(hit.pid); // SIGTERM -> supervisor kills its child, then exits
|
|
94
|
+
} catch {
|
|
95
|
+
// already gone
|
|
96
|
+
}
|
|
97
|
+
writeRegistry(list.filter((d) => d.name !== name));
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Supervisor body (run via the hidden `__supervise` command). Runs the serve
|
|
103
|
+
* argv, restarting it on a non-zero exit with capped exponential backoff; stops
|
|
104
|
+
* when the child exits cleanly or when this process receives SIGTERM/SIGINT
|
|
105
|
+
* (killing the child first). Output goes to the inherited log fd.
|
|
106
|
+
*/
|
|
107
|
+
export async function runSupervisor(name: string, argv: string[]): Promise<number> {
|
|
108
|
+
let child: Subprocess | null = null;
|
|
109
|
+
let stopping = false;
|
|
110
|
+
const onSignal = () => {
|
|
111
|
+
stopping = true;
|
|
112
|
+
try {
|
|
113
|
+
child?.kill();
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
process.on("SIGTERM", onSignal);
|
|
119
|
+
process.on("SIGINT", onSignal);
|
|
120
|
+
|
|
121
|
+
let attempt = 0;
|
|
122
|
+
while (!stopping) {
|
|
123
|
+
console.log(`[codehost:${name}] starting: ${argv.join(" ")}`);
|
|
124
|
+
child = spawn(argv, { cwd: process.cwd(), stdin: "ignore", stdout: "inherit", stderr: "inherit" });
|
|
125
|
+
const code = await child.exited;
|
|
126
|
+
if (stopping) break;
|
|
127
|
+
if (code === 0) {
|
|
128
|
+
console.log(`[codehost:${name}] server exited cleanly; supervisor stopping.`);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
attempt++;
|
|
132
|
+
const waitMs = Math.min(30_000, 1000 * 2 ** Math.min(attempt, 5));
|
|
133
|
+
console.error(
|
|
134
|
+
`[codehost:${name}] server exited with code ${code}; restarting in ${Math.round(waitMs / 1000)}s (attempt ${attempt}).`,
|
|
135
|
+
);
|
|
136
|
+
await Bun.sleep(waitMs);
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { exposeCommand } from "./commands/expose";
|
|
|
8
8
|
import { listCommand } from "./commands/list";
|
|
9
9
|
import { stopCommand } from "./commands/stop";
|
|
10
10
|
import { updateCommand } from "./commands/update";
|
|
11
|
+
import { superviseCommand } from "./commands/supervise";
|
|
11
12
|
|
|
12
13
|
yargs(hideBin(process.argv))
|
|
13
14
|
.scriptName("codehost")
|
|
@@ -19,6 +20,7 @@ yargs(hideBin(process.argv))
|
|
|
19
20
|
.command(listCommand)
|
|
20
21
|
.command(stopCommand)
|
|
21
22
|
.command(updateCommand)
|
|
23
|
+
.command(superviseCommand)
|
|
22
24
|
.demandCommand(1, "Specify a command, e.g. `codehost serve`")
|
|
23
25
|
.strict()
|
|
24
26
|
.help()
|