@waibiwaibig/all-api 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.
Files changed (3) hide show
  1. package/README.md +16 -8
  2. package/package.json +1 -1
  3. package/src/cli.mjs +118 -5
package/README.md CHANGED
@@ -12,25 +12,25 @@ Implemented surface:
12
12
 
13
13
  ## Use
14
14
 
15
- From GitHub:
15
+ From npm:
16
16
 
17
17
  ```sh
18
- git clone https://github.com/waibiwaibig/all-api.git
19
- cd all-api
20
- npm install
21
- npm link
18
+ npm install -g @waibiwaibig/all-api
22
19
  all-api setup
23
20
  ```
24
21
 
25
- From npm:
22
+ From source:
26
23
 
27
24
  ```sh
28
- npm install -g @waibiwaibig/all-api
25
+ git clone https://github.com/waibiwaibig/all-api.git
26
+ cd all-api
27
+ npm install
28
+ npm link
29
29
  all-api setup
30
30
  ```
31
31
 
32
32
  `setup` asks for the workspace directory, creates an API key, starts the server,
33
- and prints:
33
+ in the background, prints the connection details, and exits:
34
34
 
35
35
  ```text
36
36
  Base URL:
@@ -62,6 +62,12 @@ Create another key:
62
62
  all-api key create --models claude-code
63
63
  ```
64
64
 
65
+ Stop the background server:
66
+
67
+ ```sh
68
+ all-api stop
69
+ ```
70
+
65
71
  ## Notes
66
72
 
67
73
  - No runtime dependencies.
@@ -71,3 +77,5 @@ all-api key create --models claude-code
71
77
  - Codex runs with `--sandbox read-only`.
72
78
  - Claude runs with `--permission-mode plan`.
73
79
  - Binds to `127.0.0.1` by default.
80
+ - `/v1` is the OpenAI API version prefix. Use `http://127.0.0.1:4011/v1`
81
+ for OpenAI-compatible clients; root paths like `/chat/completions` also work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waibiwaibig/all-api",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Tiny OpenAI-compatible gateway for local coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import readline from "node:readline/promises";
@@ -21,6 +21,7 @@ Usage:
21
21
  all-api init [--config FILE] [--workspace DIR]
22
22
  all-api setup [--config FILE] [--workspace DIR] [--host HOST] [--port PORT] [--yes]
23
23
  all-api up [--config FILE] [--host HOST] [--port PORT]
24
+ all-api stop [--config FILE]
24
25
  all-api detect
25
26
  all-api key create [--config FILE] [--models MODEL,MODEL]
26
27
 
@@ -28,6 +29,7 @@ Examples:
28
29
  all-api init --workspace /path/to/repo
29
30
  all-api setup
30
31
  all-api up
32
+ all-api stop
31
33
  all-api key create --models codex-local,claude-code
32
34
  `);
33
35
  }
@@ -255,7 +257,7 @@ async function cmdSetup(args) {
255
257
  if (!/^n/i.test(keep)) {
256
258
  config.host = argValue(args, "--host", config.host ?? "127.0.0.1");
257
259
  config.port = Number(argValue(args, "--port", config.port ?? DEFAULT_PORT));
258
- startServer(config);
260
+ if (!(await ensureDaemon(configPath, config))) return;
259
261
  printEndpoint(config, null);
260
262
  return;
261
263
  }
@@ -284,10 +286,97 @@ async function cmdSetup(args) {
284
286
  console.log(`Created ${configPath}`);
285
287
  }
286
288
 
287
- startServer(config);
289
+ if (!(await ensureDaemon(configPath, config))) return;
288
290
  printEndpoint(config, generatedKey);
289
291
  }
290
292
 
293
+ async function ensureDaemon(configPath, config) {
294
+ if (await isServerHealthy(config)) return true;
295
+
296
+ const pidPath = pidFileForConfig(configPath);
297
+ const oldPid = readPid(pidPath);
298
+ if (oldPid && isProcessRunning(oldPid)) {
299
+ console.error(`A process is already recorded for this config: ${oldPid}`);
300
+ console.error(`If it is stale, remove ${pidPath}`);
301
+ process.exitCode = 1;
302
+ return false;
303
+ }
304
+
305
+ ensureDir(dirname(pidPath));
306
+ const child = spawn(process.execPath, [
307
+ realpathSync(fileURLToPath(import.meta.url)),
308
+ "up",
309
+ "--config",
310
+ configPath,
311
+ "--host",
312
+ config.host,
313
+ "--port",
314
+ String(config.port),
315
+ ], {
316
+ detached: true,
317
+ stdio: "ignore",
318
+ });
319
+ child.unref();
320
+ writeFileSync(pidPath, `${child.pid}\n`, { mode: 0o600 });
321
+
322
+ const ready = await waitForHealth(config, 5000);
323
+ if (!ready) {
324
+ console.error("Server did not become ready within 5 seconds.");
325
+ console.error(`PID file: ${pidPath}`);
326
+ process.exitCode = 1;
327
+ return false;
328
+ }
329
+ return true;
330
+ }
331
+
332
+ async function waitForHealth(config, timeoutMs) {
333
+ const started = Date.now();
334
+ while (Date.now() - started < timeoutMs) {
335
+ if (await isServerHealthy(config)) return true;
336
+ await sleep(100);
337
+ }
338
+ return false;
339
+ }
340
+
341
+ function sleep(ms) {
342
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
343
+ }
344
+
345
+ function isServerHealthy(config) {
346
+ return new Promise((resolveHealth) => {
347
+ const host = config.host === "0.0.0.0" ? "127.0.0.1" : config.host;
348
+ const req = http.request(`http://${host}:${config.port}/health`, { method: "GET", timeout: 500 }, (res) => {
349
+ res.resume();
350
+ resolveHealth(res.statusCode === 200);
351
+ });
352
+ req.on("error", () => resolveHealth(false));
353
+ req.on("timeout", () => {
354
+ req.destroy();
355
+ resolveHealth(false);
356
+ });
357
+ req.end();
358
+ });
359
+ }
360
+
361
+ function pidFileForConfig(configPath) {
362
+ return join(dirname(configPath), `server-${sha256(resolve(configPath)).slice(0, 12)}.pid`);
363
+ }
364
+
365
+ function readPid(pidPath) {
366
+ if (!existsSync(pidPath)) return null;
367
+ const pid = Number(readFileSync(pidPath, "utf8").trim());
368
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
369
+ }
370
+
371
+ function isProcessRunning(pid) {
372
+ try {
373
+ process.kill(pid, 0);
374
+ return true;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+
291
380
  async function ask(question, defaultValue) {
292
381
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
293
382
  try {
@@ -341,6 +430,28 @@ async function cmdUp(args) {
341
430
  printEndpoint(config, generatedKey);
342
431
  }
343
432
 
433
+ async function cmdStop(args) {
434
+ const configPath = resolve(argValue(args, "--config", DEFAULT_CONFIG));
435
+ const pidPath = pidFileForConfig(configPath);
436
+ const pid = readPid(pidPath);
437
+ if (!pid) {
438
+ console.log("No all-api server PID found.");
439
+ return;
440
+ }
441
+
442
+ try {
443
+ process.kill(-pid, "SIGTERM");
444
+ } catch {
445
+ try {
446
+ process.kill(pid, "SIGTERM");
447
+ } catch {
448
+ // Already stopped.
449
+ }
450
+ }
451
+ rmSync(pidPath, { force: true });
452
+ console.log(`Stopped all-api server ${pid}.`);
453
+ }
454
+
344
455
  function printEndpoint(config, key) {
345
456
  const hostForPrint = config.host === "0.0.0.0" ? "localhost" : config.host;
346
457
  console.log("");
@@ -390,7 +501,7 @@ async function route(req, res, config) {
390
501
  return;
391
502
  }
392
503
 
393
- if (req.method === "GET" && url.pathname === "/v1/models") {
504
+ if (req.method === "GET" && (url.pathname === "/v1/models" || url.pathname === "/models")) {
394
505
  sendJson(res, 200, {
395
506
  object: "list",
396
507
  data: allowedModels(config, auth.key).map((model) => ({
@@ -403,7 +514,7 @@ async function route(req, res, config) {
403
514
  return;
404
515
  }
405
516
 
406
- if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
517
+ if (req.method === "POST" && (url.pathname === "/v1/chat/completions" || url.pathname === "/chat/completions")) {
407
518
  const body = await readJsonBody(req);
408
519
  const model = config.models.find((m) => m.enabled && m.id === body.model);
409
520
  if (!model) {
@@ -653,6 +764,8 @@ async function main(args = process.argv.slice(2)) {
653
764
  await cmdSetup(args.slice(1));
654
765
  } else if (command === "up") {
655
766
  await cmdUp(args.slice(1));
767
+ } else if (command === "stop") {
768
+ await cmdStop(args.slice(1));
656
769
  } else if (command === "detect") {
657
770
  await cmdDetect();
658
771
  } else if (command === "key" && args[1] === "create") {