@sumeru/cli 0.1.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/LICENSE +18 -0
- package/dist/.build-fingerprint +1 -0
- package/dist/build-adapters.d.ts +18 -0
- package/dist/build-adapters.d.ts.map +1 -0
- package/dist/build-adapters.js +48 -0
- package/dist/build-adapters.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +251 -0
- package/dist/cli.js.map +1 -0
- package/dist/pid-file.d.ts +51 -0
- package/dist/pid-file.d.ts.map +1 -0
- package/dist/pid-file.js +143 -0
- package/dist/pid-file.js.map +1 -0
- package/dist/port-check.d.ts +54 -0
- package/dist/port-check.d.ts.map +1 -0
- package/dist/port-check.js +160 -0
- package/dist/port-check.js.map +1 -0
- package/package.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shazhou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1c87be7f6e3fc7a33efca7c66f512072454c46ebb199496e30241ae6134d2d0c
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Adapter } from "@sumeru/core";
|
|
2
|
+
import type { GatewayConfig } from "@sumeru/server";
|
|
3
|
+
/**
|
|
4
|
+
* A factory function that constructs an `Adapter` from an opaque options
|
|
5
|
+
* blob. The blob's keys are validated by the adapter, not by the CLI.
|
|
6
|
+
*/
|
|
7
|
+
export type AdapterFactory = (opts: Record<string, unknown>) => Adapter;
|
|
8
|
+
/** Registry of adapter factories keyed by adapter name. */
|
|
9
|
+
export type AdapterFactoryMap = Record<string, AdapterFactory>;
|
|
10
|
+
/** Default factories — wires in the built-in adapter packages. */
|
|
11
|
+
export declare const DEFAULT_ADAPTER_FACTORIES: AdapterFactoryMap;
|
|
12
|
+
/**
|
|
13
|
+
* Walk a parsed `gateways` map and produce the adapter registry the server
|
|
14
|
+
* needs. Per-gateway `config` blobs are forwarded verbatim; gateways whose
|
|
15
|
+
* `adapter` field does not match a known factory are silently omitted.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildAdapters(gateways: Record<string, GatewayConfig>, factories?: AdapterFactoryMap): Record<string, Adapter>;
|
|
18
|
+
//# sourceMappingURL=build-adapters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-adapters.d.ts","sourceRoot":"","sources":["../src/build-adapters.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;AAExE,2DAA2D;AAC3D,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE/D,kEAAkE;AAClE,eAAO,MAAM,yBAAyB,EAAE,iBAKvC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EACvC,SAAS,GAAE,iBAA6C,GACtD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the adapter registry for a Sumeru server from parsed gateway config.
|
|
3
|
+
*
|
|
4
|
+
* Walks the `gateways` map and dispatches on `gw.adapter` to the matching
|
|
5
|
+
* built-in factory (hermes, claude-code). Each gateway's `gw.config` blob is
|
|
6
|
+
* forwarded verbatim to the factory; absent / `null` blobs become `{}`.
|
|
7
|
+
*
|
|
8
|
+
* Unknown adapter names are silently skipped — the gateway then surfaces as
|
|
9
|
+
* `status: "unavailable"` via `GET /gateways`. The CLI MUST NOT crash on a
|
|
10
|
+
* gateway whose adapter package is not bundled.
|
|
11
|
+
*
|
|
12
|
+
* The factory map is injectable for tests; production code uses
|
|
13
|
+
* {@link DEFAULT_ADAPTER_FACTORIES} which wires in the real built-in packages.
|
|
14
|
+
*
|
|
15
|
+
* See `specs/cli-pass-gateway-config.md` (issue #32).
|
|
16
|
+
*/
|
|
17
|
+
import { createClaudeCodeAdapter } from "@sumeru/adapter-claude-code";
|
|
18
|
+
import { createCodexAdapter } from "@sumeru/adapter-codex";
|
|
19
|
+
import { createCursorAgentAdapter } from "@sumeru/adapter-cursor-agent";
|
|
20
|
+
import { createHermesAdapter } from "@sumeru/adapter-hermes";
|
|
21
|
+
/** Default factories — wires in the built-in adapter packages. */
|
|
22
|
+
export const DEFAULT_ADAPTER_FACTORIES = {
|
|
23
|
+
hermes: (opts) => createHermesAdapter(opts),
|
|
24
|
+
"claude-code": (opts) => createClaudeCodeAdapter(opts),
|
|
25
|
+
codex: (opts) => createCodexAdapter(opts),
|
|
26
|
+
"cursor-agent": (opts) => createCursorAgentAdapter(opts),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Walk a parsed `gateways` map and produce the adapter registry the server
|
|
30
|
+
* needs. Per-gateway `config` blobs are forwarded verbatim; gateways whose
|
|
31
|
+
* `adapter` field does not match a known factory are silently omitted.
|
|
32
|
+
*/
|
|
33
|
+
export function buildAdapters(gateways, factories = DEFAULT_ADAPTER_FACTORIES) {
|
|
34
|
+
const adapters = {};
|
|
35
|
+
for (const [name, gw] of Object.entries(gateways)) {
|
|
36
|
+
const factory = factories[gw.adapter];
|
|
37
|
+
if (factory === undefined) {
|
|
38
|
+
// Adapter package not bundled by this CLI build — leave the
|
|
39
|
+
// gateway's adapter slot empty so the registry reports
|
|
40
|
+
// `status: "unavailable"`.
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const opts = gw.config ?? {};
|
|
44
|
+
adapters[name] = factory(opts);
|
|
45
|
+
}
|
|
46
|
+
return adapters;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=build-adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-adapters.js","sourceRoot":"","sources":["../src/build-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAa7D,kEAAkE;AAClE,MAAM,CAAC,MAAM,yBAAyB,GAAsB;IAC3D,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC;IACtD,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC;IACzC,cAAc,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC;CACxD,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC5B,QAAuC,EACvC,YAA+B,yBAAyB;IAExD,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnD,MAAM,OAAO,GAAG,SAAS,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,4DAA4D;YAC5D,uDAAuD;YACvD,2BAA2B;YAC3B,SAAS;QACV,CAAC;QACD,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC;QAC7B,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,QAAQ,CAAC;AACjB,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadConfig, startServer, } from "@sumeru/server";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { buildAdapters } from "./build-adapters.js";
|
|
8
|
+
import { isProcessAlive, readPidFile, removePidFile, resolvePidFilePath, writePidFile, } from "./pid-file.js";
|
|
9
|
+
import { formatPortInUse, killHolder, lookupPortHolder } from "./port-check.js";
|
|
10
|
+
function findVersion() {
|
|
11
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
for (let i = 0; i < 5; i++) {
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
15
|
+
if (pkg.name === "@sumeru/cli")
|
|
16
|
+
return pkg.version ?? "0.0.0";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* keep walking */
|
|
20
|
+
}
|
|
21
|
+
dir = dirname(dir);
|
|
22
|
+
}
|
|
23
|
+
return "0.0.0";
|
|
24
|
+
}
|
|
25
|
+
const program = new Command();
|
|
26
|
+
program
|
|
27
|
+
.name("sumeru")
|
|
28
|
+
.description("Agent house — HTTP service for multi-agent management")
|
|
29
|
+
.version(findVersion());
|
|
30
|
+
program
|
|
31
|
+
.command("run")
|
|
32
|
+
.description("[planned] Run a scene with a specified adapter and model")
|
|
33
|
+
.requiredOption("-s, --scene <path>", "Path to scene directory or YAML")
|
|
34
|
+
.requiredOption("-r, --runner <type>", "Adapter type (hermes, claude-code)")
|
|
35
|
+
.requiredOption("-m, --model <model>", "Model identifier")
|
|
36
|
+
.option("-t, --timeout <seconds>", "Timeout in seconds", "300")
|
|
37
|
+
.option("--network", "Allow network access", true)
|
|
38
|
+
.option("--no-network", "Disable network access")
|
|
39
|
+
.option("-i, --image <image>", "Docker image")
|
|
40
|
+
.option("-o, --output <path>", "Output path for recording")
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
console.log("sumeru run — not yet implemented");
|
|
43
|
+
console.log("Options:", JSON.stringify(opts, null, 2));
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.command("list")
|
|
47
|
+
.description("[planned] List available scenes")
|
|
48
|
+
.option("-d, --dir <path>", "Scenes directory", "scenes")
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
console.log("sumeru list — not yet implemented");
|
|
51
|
+
console.log("Directory:", opts.dir);
|
|
52
|
+
});
|
|
53
|
+
program
|
|
54
|
+
.command("start")
|
|
55
|
+
.description("Start the Sumeru HTTP server")
|
|
56
|
+
.option("-p, --port <number>", "TCP port to bind (0 = ephemeral)", "7900")
|
|
57
|
+
.option("-h, --host <host>", "Bind address", "127.0.0.1")
|
|
58
|
+
.option("-c, --config <path>", "Path to sumeru.yaml configuration file")
|
|
59
|
+
.option("--ocas-dir <path>", "Directory for the ocas content-addressed store (default: $SUMERU_OCAS_DIR or ~/.sumeru/ocas)")
|
|
60
|
+
.option("--force", "Kill any process holding the chosen port before binding (sends SIGTERM, then SIGKILL after 2s)")
|
|
61
|
+
.action(async (opts) => {
|
|
62
|
+
const port = Number.parseInt(opts.port, 10);
|
|
63
|
+
if (Number.isNaN(port) || port < 0) {
|
|
64
|
+
console.error(`Invalid --port value: ${opts.port}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const host = String(opts.host);
|
|
68
|
+
const force = Boolean(opts.force);
|
|
69
|
+
const ocasDir = typeof opts.ocasDir === "string" && opts.ocasDir.length > 0
|
|
70
|
+
? opts.ocasDir
|
|
71
|
+
: null;
|
|
72
|
+
// Load config (if any) BEFORE binding a port — we want to fail loudly
|
|
73
|
+
// on bad config without leaving a half-started listener around.
|
|
74
|
+
let name = "sumeru";
|
|
75
|
+
let gateways = {};
|
|
76
|
+
let workspaceRoot = null;
|
|
77
|
+
if (typeof opts.config === "string" && opts.config.length > 0) {
|
|
78
|
+
let cfg;
|
|
79
|
+
try {
|
|
80
|
+
cfg = await loadConfig(opts.config);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
console.error(`Failed to load config from ${opts.config}: ${msg}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
name = cfg.name;
|
|
88
|
+
gateways = cfg.gateways;
|
|
89
|
+
workspaceRoot = cfg.workspaceRoot;
|
|
90
|
+
}
|
|
91
|
+
// --- PID file lifecycle (issue #33) ---
|
|
92
|
+
const pidFilePath = resolvePidFilePath();
|
|
93
|
+
const existingPid = readPidFile(pidFilePath);
|
|
94
|
+
if (existingPid !== null) {
|
|
95
|
+
if (isProcessAlive(existingPid)) {
|
|
96
|
+
if (force) {
|
|
97
|
+
try {
|
|
98
|
+
await killHolder(existingPid, port, host);
|
|
99
|
+
console.error(`[sumeru] killed pid ${existingPid} from stale pid file`);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
console.error(`Failed to kill pid ${existingPid}: ${msg}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(`Another sumeru appears to be running (pid ${existingPid}, recorded in ${pidFilePath}).\n Stop it first, or run \`sumeru start … --force\` to terminate it.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.error(`[sumeru] removing stale pid file (pid ${existingPid} not running)`);
|
|
114
|
+
// fall through; writePidFile below will overwrite.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
writePidFile(pidFilePath, process.pid);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122
|
+
console.error(`[sumeru] could not write pid file ${pidFilePath}: ${msg}`);
|
|
123
|
+
// Best-effort — continue startup.
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const server = await startServerWithRetry({
|
|
127
|
+
port,
|
|
128
|
+
host,
|
|
129
|
+
name,
|
|
130
|
+
version: findVersion(),
|
|
131
|
+
gateways,
|
|
132
|
+
workspaceRoot,
|
|
133
|
+
ocasDir,
|
|
134
|
+
force,
|
|
135
|
+
});
|
|
136
|
+
console.log(`Listening on http://${server.host}:${server.port}`);
|
|
137
|
+
let shuttingDown = false;
|
|
138
|
+
const shutdown = async (signal) => {
|
|
139
|
+
if (shuttingDown) {
|
|
140
|
+
// Second signal — escape hatch for a hung shutdown.
|
|
141
|
+
const code = signal === "SIGINT" ? 130 : 143;
|
|
142
|
+
process.exit(code);
|
|
143
|
+
}
|
|
144
|
+
shuttingDown = true;
|
|
145
|
+
console.error(`[sumeru] shutting down (${signal})...`);
|
|
146
|
+
try {
|
|
147
|
+
await server.stop();
|
|
148
|
+
try {
|
|
149
|
+
removePidFile(pidFilePath);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
console.error(`[sumeru] could not remove pid file: ${msg}`);
|
|
154
|
+
}
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
console.error(`[sumeru] failed to stop server: ${msg}`);
|
|
160
|
+
try {
|
|
161
|
+
removePidFile(pidFilePath);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* ignore on the failure path */
|
|
165
|
+
}
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
process.on("SIGINT", () => {
|
|
170
|
+
void shutdown("SIGINT");
|
|
171
|
+
});
|
|
172
|
+
process.on("SIGTERM", () => {
|
|
173
|
+
void shutdown("SIGTERM");
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
try {
|
|
178
|
+
removePidFile(pidFilePath);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
/* best effort on the error path */
|
|
182
|
+
}
|
|
183
|
+
const code = err instanceof Error && "code" in err
|
|
184
|
+
? err.code
|
|
185
|
+
: null;
|
|
186
|
+
if (code === "EADDRINUSE") {
|
|
187
|
+
const holder = await lookupPortHolder(host, port);
|
|
188
|
+
console.error(formatPortInUse({ host, port, holder }));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
192
|
+
console.error(`Failed to start server: ${msg}`);
|
|
193
|
+
}
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
async function startServerWithRetry(args) {
|
|
198
|
+
try {
|
|
199
|
+
return await startServer({
|
|
200
|
+
port: args.port,
|
|
201
|
+
host: args.host,
|
|
202
|
+
name: args.name,
|
|
203
|
+
version: args.version,
|
|
204
|
+
gateways: args.gateways,
|
|
205
|
+
workspaceRoot: args.workspaceRoot,
|
|
206
|
+
adapters: buildAdapters(args.gateways),
|
|
207
|
+
sseHeartbeatMs: null,
|
|
208
|
+
sseBufferSize: null,
|
|
209
|
+
sseRetentionMs: null,
|
|
210
|
+
ocasDir: args.ocasDir,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
const code = err instanceof Error && "code" in err
|
|
215
|
+
? err.code
|
|
216
|
+
: null;
|
|
217
|
+
if (code !== "EADDRINUSE" || !args.force)
|
|
218
|
+
throw err;
|
|
219
|
+
const holder = await lookupPortHolder(args.host, args.port);
|
|
220
|
+
if (holder === null) {
|
|
221
|
+
// Cannot identify holder — propagate the original error so the
|
|
222
|
+
// generic diagnostic kicks in.
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
await killHolder(holder.pid, args.port, args.host);
|
|
227
|
+
}
|
|
228
|
+
catch (killErr) {
|
|
229
|
+
const msg = killErr instanceof Error ? killErr.message : String(killErr);
|
|
230
|
+
console.error(`Failed to kill pid ${holder.pid}: ${msg}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
console.error(`[sumeru] killed pid ${holder.pid} holding port ${args.port}`);
|
|
234
|
+
// Retry the bind once.
|
|
235
|
+
return startServer({
|
|
236
|
+
port: args.port,
|
|
237
|
+
host: args.host,
|
|
238
|
+
name: args.name,
|
|
239
|
+
version: args.version,
|
|
240
|
+
gateways: args.gateways,
|
|
241
|
+
workspaceRoot: args.workspaceRoot,
|
|
242
|
+
adapters: buildAdapters(args.gateways),
|
|
243
|
+
sseHeartbeatMs: null,
|
|
244
|
+
sseBufferSize: null,
|
|
245
|
+
sseRetentionMs: null,
|
|
246
|
+
ocasDir: args.ocasDir,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
program.parse();
|
|
251
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID file management for `sumeru start` (issue #33).
|
|
3
|
+
*
|
|
4
|
+
* Best-effort pid file at `~/.sumeru/sumeru.pid` (or `$SUMERU_PID_FILE`).
|
|
5
|
+
*
|
|
6
|
+
* - `writePidFile(path, pid)` — creates parent dir with 0o700 if missing,
|
|
7
|
+
* writes `<pid>\n` with mode 0o600. Throws on filesystem errors so the
|
|
8
|
+
* caller can choose to degrade.
|
|
9
|
+
* - `readPidFile(path)` — returns the parsed pid, `null` if the file is
|
|
10
|
+
* missing OR malformed (operators may have hand-edited it; we won't crash).
|
|
11
|
+
* - `removePidFile(path)` — silently succeeds if the file is already gone.
|
|
12
|
+
* - `isProcessAlive(pid)` — uses `process.kill(pid, 0)` (no signal sent) to
|
|
13
|
+
* probe liveness. ESRCH → dead. EPERM → live but foreign (still treated
|
|
14
|
+
* as live for safety — we don't want to overwrite someone else's pid file).
|
|
15
|
+
*
|
|
16
|
+
* See specs/cli-pid-file.md.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the configured pid file path. Honors `SUMERU_PID_FILE` for tests;
|
|
20
|
+
* defaults to `~/.sumeru/sumeru.pid`. `~/` is expanded against `os.homedir()`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolvePidFilePath(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Write `<pid>\n` to `path` with mode 0o600. Creates the parent dir
|
|
25
|
+
* (0o700) if missing. Throws on EACCES / EROFS / ENOSPC etc. — the caller
|
|
26
|
+
* is responsible for catching and degrading to a warning.
|
|
27
|
+
*/
|
|
28
|
+
export declare function writePidFile(path: string, pid: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Read and parse the pid file. Returns `null` if the file is missing or
|
|
31
|
+
* the contents are not a positive integer.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readPidFile(path: string): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Remove the pid file. No-op if already absent. Other I/O errors propagate
|
|
36
|
+
* so the caller can decide whether to log.
|
|
37
|
+
*/
|
|
38
|
+
export declare function removePidFile(path: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Probe whether `pid` is running. Sends signal 0 via `process.kill`, which
|
|
41
|
+
* performs the permission/existence check without actually delivering a
|
|
42
|
+
* signal.
|
|
43
|
+
*
|
|
44
|
+
* - Success → process exists and we can signal it → `true`.
|
|
45
|
+
* - `ESRCH` → no such process → `false`.
|
|
46
|
+
* - `EPERM` → process exists but belongs to another user → `true` (treat
|
|
47
|
+
* as live; we don't want to overwrite a foreign pid file).
|
|
48
|
+
* - Any other error → `false` (defensive; surfaces as "stale" to the caller).
|
|
49
|
+
*/
|
|
50
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
51
|
+
//# sourceMappingURL=pid-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pid-file.d.ts","sourceRoot":"","sources":["../src/pid-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAaH;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAgB5D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqBvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CA2BhD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAanD"}
|
package/dist/pid-file.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID file management for `sumeru start` (issue #33).
|
|
3
|
+
*
|
|
4
|
+
* Best-effort pid file at `~/.sumeru/sumeru.pid` (or `$SUMERU_PID_FILE`).
|
|
5
|
+
*
|
|
6
|
+
* - `writePidFile(path, pid)` — creates parent dir with 0o700 if missing,
|
|
7
|
+
* writes `<pid>\n` with mode 0o600. Throws on filesystem errors so the
|
|
8
|
+
* caller can choose to degrade.
|
|
9
|
+
* - `readPidFile(path)` — returns the parsed pid, `null` if the file is
|
|
10
|
+
* missing OR malformed (operators may have hand-edited it; we won't crash).
|
|
11
|
+
* - `removePidFile(path)` — silently succeeds if the file is already gone.
|
|
12
|
+
* - `isProcessAlive(pid)` — uses `process.kill(pid, 0)` (no signal sent) to
|
|
13
|
+
* probe liveness. ESRCH → dead. EPERM → live but foreign (still treated
|
|
14
|
+
* as live for safety — we don't want to overwrite someone else's pid file).
|
|
15
|
+
*
|
|
16
|
+
* See specs/cli-pid-file.md.
|
|
17
|
+
*/
|
|
18
|
+
import { chmodSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the configured pid file path. Honors `SUMERU_PID_FILE` for tests;
|
|
23
|
+
* defaults to `~/.sumeru/sumeru.pid`. `~/` is expanded against `os.homedir()`.
|
|
24
|
+
*/
|
|
25
|
+
export function resolvePidFilePath() {
|
|
26
|
+
const env = process.env.SUMERU_PID_FILE;
|
|
27
|
+
const raw = typeof env === "string" && env.length > 0
|
|
28
|
+
? env
|
|
29
|
+
: join(homedir(), ".sumeru", "sumeru.pid");
|
|
30
|
+
const expanded = raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
|
|
31
|
+
return resolve(expanded);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write `<pid>\n` to `path` with mode 0o600. Creates the parent dir
|
|
35
|
+
* (0o700) if missing. Throws on EACCES / EROFS / ENOSPC etc. — the caller
|
|
36
|
+
* is responsible for catching and degrading to a warning.
|
|
37
|
+
*/
|
|
38
|
+
export function writePidFile(path, pid) {
|
|
39
|
+
const dir = dirname(path);
|
|
40
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
41
|
+
// `recursive: true` does not chmod an already-existing dir; nudge it.
|
|
42
|
+
try {
|
|
43
|
+
chmodSync(dir, 0o700);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* best effort — operator may not own the dir */
|
|
47
|
+
}
|
|
48
|
+
writeFileSync(path, `${pid}\n`, { mode: 0o600 });
|
|
49
|
+
// `writeFileSync` honors mode only on create; force it for overwrite.
|
|
50
|
+
try {
|
|
51
|
+
chmodSync(path, 0o600);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* best effort */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read and parse the pid file. Returns `null` if the file is missing or
|
|
59
|
+
* the contents are not a positive integer.
|
|
60
|
+
*/
|
|
61
|
+
export function readPidFile(path) {
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
raw = readFileSync(path, "utf-8");
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err !== null &&
|
|
68
|
+
typeof err === "object" &&
|
|
69
|
+
"code" in err &&
|
|
70
|
+
err.code === "ENOENT") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = raw.trim();
|
|
76
|
+
if (trimmed.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
if (!/^\d+$/.test(trimmed))
|
|
79
|
+
return null;
|
|
80
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
81
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
82
|
+
return null;
|
|
83
|
+
return pid;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Remove the pid file. No-op if already absent. Other I/O errors propagate
|
|
87
|
+
* so the caller can decide whether to log.
|
|
88
|
+
*/
|
|
89
|
+
export function removePidFile(path) {
|
|
90
|
+
try {
|
|
91
|
+
statSync(path);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err !== null &&
|
|
95
|
+
typeof err === "object" &&
|
|
96
|
+
"code" in err &&
|
|
97
|
+
err.code === "ENOENT") {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(path);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err !== null &&
|
|
107
|
+
typeof err === "object" &&
|
|
108
|
+
"code" in err &&
|
|
109
|
+
err.code === "ENOENT") {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Probe whether `pid` is running. Sends signal 0 via `process.kill`, which
|
|
117
|
+
* performs the permission/existence check without actually delivering a
|
|
118
|
+
* signal.
|
|
119
|
+
*
|
|
120
|
+
* - Success → process exists and we can signal it → `true`.
|
|
121
|
+
* - `ESRCH` → no such process → `false`.
|
|
122
|
+
* - `EPERM` → process exists but belongs to another user → `true` (treat
|
|
123
|
+
* as live; we don't want to overwrite a foreign pid file).
|
|
124
|
+
* - Any other error → `false` (defensive; surfaces as "stale" to the caller).
|
|
125
|
+
*/
|
|
126
|
+
export function isProcessAlive(pid) {
|
|
127
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
128
|
+
return false;
|
|
129
|
+
try {
|
|
130
|
+
process.kill(pid, 0);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err === null || typeof err !== "object" || !("code" in err)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const code = err.code;
|
|
138
|
+
if (code === "EPERM")
|
|
139
|
+
return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=pid-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pid-file.js","sourceRoot":"","sources":["../src/pid-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port conflict detection + force-kill helpers for `sumeru start` (issue #33).
|
|
3
|
+
*
|
|
4
|
+
* - `lookupPortHolder(host, port)` shells out to `lsof -i :<port>
|
|
5
|
+
* -sTCP:LISTEN -t -P -n` to identify the process bound to the port.
|
|
6
|
+
* Returns `null` if lsof is missing OR no holder is found OR the helper
|
|
7
|
+
* fails for any reason — the caller should fall back to a generic
|
|
8
|
+
* diagnostic.
|
|
9
|
+
* - `formatPortInUse({ host, port, holder })` produces the operator-facing
|
|
10
|
+
* error block.
|
|
11
|
+
* - `killHolder(pid, port, host)` sends SIGTERM, waits up to 2s for the port
|
|
12
|
+
* to free, then SIGKILLs. Throws if the kill itself errors (e.g. EPERM).
|
|
13
|
+
*
|
|
14
|
+
* See specs/cli-startup-port-check.md.
|
|
15
|
+
*/
|
|
16
|
+
export type PortHolder = {
|
|
17
|
+
pid: number;
|
|
18
|
+
command: string;
|
|
19
|
+
};
|
|
20
|
+
export type FormatPortInUseOptions = {
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
holder: PortHolder | null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the process listening on `host:port` via lsof. Returns null if:
|
|
27
|
+
* - lsof is not on PATH (ENOENT spawn error),
|
|
28
|
+
* - lsof exits non-zero (no holder found),
|
|
29
|
+
* - the holder pid cannot be parsed,
|
|
30
|
+
* - any other error — we never throw to the caller.
|
|
31
|
+
*/
|
|
32
|
+
export declare function lookupPortHolder(_host: string, port: number): Promise<PortHolder | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Render the diagnostic the CLI prints when `EADDRINUSE` is caught.
|
|
35
|
+
*
|
|
36
|
+
* - When the holder is identified (lsof present, pid resolved), emit the
|
|
37
|
+
* multi-line block with `Held by pid …` and a `--force` hint.
|
|
38
|
+
* - When the holder is unknown (lsof missing OR could not identify), fall
|
|
39
|
+
* back to the legacy single-line message — no `--force` hint, since
|
|
40
|
+
* without a target pid `--force` cannot do anything anyway.
|
|
41
|
+
*
|
|
42
|
+
* See specs/cli-startup-port-check.md.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatPortInUse(opts: FormatPortInUseOptions): string;
|
|
45
|
+
/**
|
|
46
|
+
* Send SIGTERM to `pid`. Wait up to `gracefulMs` for the port to free; if
|
|
47
|
+
* still bound, send SIGKILL. Resolves once the port is free or after
|
|
48
|
+
* `gracefulMs + killWaitMs` total.
|
|
49
|
+
*
|
|
50
|
+
* Throws if `process.kill` itself fails (e.g. EPERM or ESRCH at SIGTERM
|
|
51
|
+
* time — both are operator-actionable conditions).
|
|
52
|
+
*/
|
|
53
|
+
export declare function killHolder(pid: number, port: number, host: string, gracefulMs?: number): Promise<void>;
|
|
54
|
+
//# sourceMappingURL=port-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-check.d.ts","sourceRoot":"","sources":["../src/port-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,MAAM,MAAM,UAAU,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAK5B;AA2DD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAUpE;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,SAAQ,GAChB,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port conflict detection + force-kill helpers for `sumeru start` (issue #33).
|
|
3
|
+
*
|
|
4
|
+
* - `lookupPortHolder(host, port)` shells out to `lsof -i :<port>
|
|
5
|
+
* -sTCP:LISTEN -t -P -n` to identify the process bound to the port.
|
|
6
|
+
* Returns `null` if lsof is missing OR no holder is found OR the helper
|
|
7
|
+
* fails for any reason — the caller should fall back to a generic
|
|
8
|
+
* diagnostic.
|
|
9
|
+
* - `formatPortInUse({ host, port, holder })` produces the operator-facing
|
|
10
|
+
* error block.
|
|
11
|
+
* - `killHolder(pid, port, host)` sends SIGTERM, waits up to 2s for the port
|
|
12
|
+
* to free, then SIGKILLs. Throws if the kill itself errors (e.g. EPERM).
|
|
13
|
+
*
|
|
14
|
+
* See specs/cli-startup-port-check.md.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { createConnection } from "node:net";
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the process listening on `host:port` via lsof. Returns null if:
|
|
20
|
+
* - lsof is not on PATH (ENOENT spawn error),
|
|
21
|
+
* - lsof exits non-zero (no holder found),
|
|
22
|
+
* - the holder pid cannot be parsed,
|
|
23
|
+
* - any other error — we never throw to the caller.
|
|
24
|
+
*/
|
|
25
|
+
export async function lookupPortHolder(_host, port) {
|
|
26
|
+
const pid = await runLsofForPid(port);
|
|
27
|
+
if (pid === null)
|
|
28
|
+
return null;
|
|
29
|
+
const command = await readCommandForPid(pid);
|
|
30
|
+
return { pid, command };
|
|
31
|
+
}
|
|
32
|
+
function runLsofForPid(port) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
let child;
|
|
35
|
+
try {
|
|
36
|
+
child = spawn("lsof", ["-i", `:${port}`, "-sTCP:LISTEN", "-t", "-P", "-n"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
resolve(null);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let out = "";
|
|
43
|
+
child.stdout?.on("data", (chunk) => {
|
|
44
|
+
out += chunk.toString("utf-8");
|
|
45
|
+
});
|
|
46
|
+
child.on("error", () => resolve(null));
|
|
47
|
+
child.on("close", () => {
|
|
48
|
+
const first = out.split("\n").find((line) => /^\d+$/.test(line.trim()));
|
|
49
|
+
if (first === undefined) {
|
|
50
|
+
resolve(null);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const pid = Number.parseInt(first.trim(), 10);
|
|
54
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
55
|
+
resolve(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
resolve(pid);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function readCommandForPid(pid) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let child;
|
|
65
|
+
try {
|
|
66
|
+
child = spawn("ps", ["-p", String(pid), "-o", "comm="], {
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
resolve("unknown");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let out = "";
|
|
75
|
+
child.stdout?.on("data", (chunk) => {
|
|
76
|
+
out += chunk.toString("utf-8");
|
|
77
|
+
});
|
|
78
|
+
child.on("error", () => resolve("unknown"));
|
|
79
|
+
child.on("close", () => {
|
|
80
|
+
const trimmed = out.trim();
|
|
81
|
+
resolve(trimmed.length > 0 ? trimmed : "unknown");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Render the diagnostic the CLI prints when `EADDRINUSE` is caught.
|
|
87
|
+
*
|
|
88
|
+
* - When the holder is identified (lsof present, pid resolved), emit the
|
|
89
|
+
* multi-line block with `Held by pid …` and a `--force` hint.
|
|
90
|
+
* - When the holder is unknown (lsof missing OR could not identify), fall
|
|
91
|
+
* back to the legacy single-line message — no `--force` hint, since
|
|
92
|
+
* without a target pid `--force` cannot do anything anyway.
|
|
93
|
+
*
|
|
94
|
+
* See specs/cli-startup-port-check.md.
|
|
95
|
+
*/
|
|
96
|
+
export function formatPortInUse(opts) {
|
|
97
|
+
const { host, port, holder } = opts;
|
|
98
|
+
if (holder === null) {
|
|
99
|
+
return `Port ${port} is already in use on ${host}. Choose a different --port or stop the conflicting process.`;
|
|
100
|
+
}
|
|
101
|
+
return [
|
|
102
|
+
`Port ${port} is already in use on ${host}.`,
|
|
103
|
+
` Held by pid ${holder.pid} (${holder.command})`,
|
|
104
|
+
` Run \`sumeru start --port ${port} --force\` to terminate it, or pick a different --port.`,
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Send SIGTERM to `pid`. Wait up to `gracefulMs` for the port to free; if
|
|
109
|
+
* still bound, send SIGKILL. Resolves once the port is free or after
|
|
110
|
+
* `gracefulMs + killWaitMs` total.
|
|
111
|
+
*
|
|
112
|
+
* Throws if `process.kill` itself fails (e.g. EPERM or ESRCH at SIGTERM
|
|
113
|
+
* time — both are operator-actionable conditions).
|
|
114
|
+
*/
|
|
115
|
+
export async function killHolder(pid, port, host, gracefulMs = 2_000) {
|
|
116
|
+
process.kill(pid, "SIGTERM");
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
while (Date.now() - start < gracefulMs) {
|
|
119
|
+
if (!(await isPortBound(host, port)))
|
|
120
|
+
return;
|
|
121
|
+
await sleep(100);
|
|
122
|
+
}
|
|
123
|
+
// Still bound — escalate.
|
|
124
|
+
try {
|
|
125
|
+
process.kill(pid, "SIGKILL");
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err !== null &&
|
|
129
|
+
typeof err === "object" &&
|
|
130
|
+
"code" in err &&
|
|
131
|
+
err.code === "ESRCH") {
|
|
132
|
+
// Already gone between SIGTERM and SIGKILL. That's fine.
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
// Best-effort wait for the kernel to release the socket.
|
|
138
|
+
const killStart = Date.now();
|
|
139
|
+
while (Date.now() - killStart < 1_000) {
|
|
140
|
+
if (!(await isPortBound(host, port)))
|
|
141
|
+
return;
|
|
142
|
+
await sleep(50);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isPortBound(host, port) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const socket = createConnection({ host, port });
|
|
148
|
+
const done = (bound) => {
|
|
149
|
+
socket.removeAllListeners();
|
|
150
|
+
socket.destroy();
|
|
151
|
+
resolve(bound);
|
|
152
|
+
};
|
|
153
|
+
socket.once("connect", () => done(true));
|
|
154
|
+
socket.once("error", () => done(false));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function sleep(ms) {
|
|
158
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=port-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-check.js","sourceRoot":"","sources":["../src/port-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAa5C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,KAAa,EACb,IAAY;IAEZ,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAC7C,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AACzB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IAClC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,IAAI,KAA+B,CAAC;QACpC,IAAI,CAAC;YACJ,KAAK,GAAG,KAAK,CACZ,MAAM,EACN,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EACpD,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CACvC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,OAAO;QACR,CAAC;QACD,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAC1C,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACvC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACtB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;YACxE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO;YACR,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;gBACvC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO;YACR,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACrC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,IAAI,KAA+B,CAAC;QACpC,IAAI,CAAC;YACJ,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;gBACvD,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;aACnC,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,CAAC,SAAS,CAAC,CAAC;YACnB,OAAO;QACR,CAAC;QACD,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAC1C,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QAC5C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACtB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;YAC3B,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,IAA4B;IAC3D,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACpC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,QAAQ,IAAI,yBAAyB,IAAI,8DAA8D,CAAC;IAChH,CAAC;IACD,OAAO;QACN,QAAQ,IAAI,yBAAyB,IAAI,GAAG;QAC5C,iBAAiB,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,OAAO,GAAG;QACjD,+BAA+B,IAAI,yDAAyD;KAC5F,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,GAAW,EACX,IAAY,EACZ,IAAY,EACZ,UAAU,GAAG,KAAK;IAElB,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO;QAC7C,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IACD,0BAA0B;IAC1B,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IACC,GAAG,KAAK,IAAI;YACZ,OAAO,GAAG,KAAK,QAAQ;YACvB,MAAM,IAAI,GAAG;YACZ,GAAyB,CAAC,IAAI,KAAK,OAAO,EAC1C,CAAC;YACF,yDAAyD;YACzD,OAAO;QACR,CAAC;QACD,MAAM,GAAG,CAAC;IACX,CAAC;IACD,yDAAyD;IACzD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,KAAK,EAAE,CAAC;QACvC,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO;QAC7C,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;AACF,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,IAAY;IAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,MAAM,GAAG,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,CAAC,KAAc,EAAQ,EAAE;YACrC,MAAM,CAAC,kBAAkB,EAAE,CAAC;YAC5B,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACxB,OAAO,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;AAClD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sumeru/cli",
|
|
3
|
+
"description": "CLI for Sumeru agent house",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sumeru": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^14.0.0",
|
|
14
|
+
"yaml": "^2.7.0",
|
|
15
|
+
"@sumeru/adapter-claude-code": "^0.1.0",
|
|
16
|
+
"@sumeru/adapter-codex": "^0.1.0",
|
|
17
|
+
"@sumeru/adapter-cursor-agent": "^0.1.0",
|
|
18
|
+
"@sumeru/core": "^0.1.0",
|
|
19
|
+
"@sumeru/server": "^0.1.0",
|
|
20
|
+
"@sumeru/adapter-hermes": "^0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test:ci": "npx vitest run"
|
|
25
|
+
}
|
|
26
|
+
}
|