@askalf/dario 3.31.14 → 3.31.15

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/dist/cli.js CHANGED
@@ -12,34 +12,22 @@
12
12
  // ── Bun auto-relaunch ──
13
13
  // Bun's TLS fingerprint matches Claude Code's runtime (both use Bun/BoringSSL).
14
14
  // If Bun is installed and we're running on Node, relaunch under Bun for
15
- // network-level fingerprint fidelity.
16
- if (!('Bun' in globalThis) && !process.env.DARIO_NO_BUN) {
17
- try {
18
- const { execFileSync } = await import('node:child_process');
19
- // Check if bun exists
20
- execFileSync('bun', ['--version'], { stdio: 'ignore', timeout: 3000 });
21
- // Relaunch under bun
22
- const { spawn } = await import('node:child_process');
23
- const child = spawn('bun', ['run', ...process.argv.slice(1)], {
24
- stdio: 'inherit',
25
- env: { ...process.env, DARIO_NO_BUN: '1' },
26
- });
27
- child.on('exit', (code) => process.exit(code ?? 0));
28
- // Prevent this process from continuing
29
- await new Promise(() => { });
30
- }
31
- catch {
32
- // Bun not available, continue with Node
33
- }
34
- }
15
+ // network-level fingerprint fidelity. Moved below into the main-entry guard
16
+ // at the bottom of the file so importing this module (e.g. from tests that
17
+ // just want `parsePositiveIntEnv`) doesn't trigger a Bun relaunch or any
18
+ // other startup side effect.
35
19
  import { unlink } from 'node:fs/promises';
36
20
  import { join } from 'node:path';
37
21
  import { homedir } from 'node:os';
22
+ import { pathToFileURL } from 'node:url';
38
23
  import { startAutoOAuthFlow, startManualOAuthFlow, detectHeadlessEnvironment, getStatus, refreshTokens, loadCredentials } from './oauth.js';
39
24
  import { startProxy, sanitizeError } from './proxy.js';
40
25
  import { VALID_EFFORT_VALUES } from './cc-template.js';
41
26
  import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount, ensureLoginCredentialsInPool, MIGRATED_LOGIN_ALIAS } from './accounts.js';
42
27
  import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
28
+ // `args` / `command` at module scope — command handlers below close over
29
+ // `args` to read their own flags. Reading argv is harmless on import; only
30
+ // the handler dispatch at the bottom is gated behind the main-entry check.
43
31
  const args = process.argv.slice(2);
44
32
  const command = args[0] ?? 'proxy';
45
33
  async function login() {
@@ -1159,13 +1147,42 @@ const commands = {
1159
1147
  '--version': version,
1160
1148
  '-V': version,
1161
1149
  };
1162
- const handler = commands[command];
1163
- if (!handler) {
1164
- console.error(`Unknown command: ${command}`);
1165
- console.error('Run `dario help` for usage.');
1166
- process.exit(1);
1150
+ // Main-entry guard. Only run the Bun auto-relaunch and handler dispatch when
1151
+ // this module is the direct entry point — importing it (from tests or for
1152
+ // a library helper like `parsePositiveIntEnv`) must NOT start the proxy.
1153
+ // Before this guard, `import { parsePositiveIntEnv } from './cli.js'` would
1154
+ // fall through to `command = args[0] ?? 'proxy'` and fire `handler()`, which
1155
+ // tried to run `startProxy()` and failed the test with "Not authenticated".
1156
+ const isDirectEntry = typeof process.argv[1] === 'string' &&
1157
+ import.meta.url === pathToFileURL(process.argv[1]).href;
1158
+ if (isDirectEntry) {
1159
+ // Bun auto-relaunch for TLS fingerprint fidelity. Only meaningful when
1160
+ // dario is the direct entry — if we're imported, whoever imported us
1161
+ // already chose their runtime.
1162
+ if (!('Bun' in globalThis) && !process.env.DARIO_NO_BUN) {
1163
+ try {
1164
+ const { execFileSync, spawn } = await import('node:child_process');
1165
+ execFileSync('bun', ['--version'], { stdio: 'ignore', timeout: 3000 });
1166
+ const child = spawn('bun', ['run', ...process.argv.slice(1)], {
1167
+ stdio: 'inherit',
1168
+ env: { ...process.env, DARIO_NO_BUN: '1' },
1169
+ });
1170
+ child.on('exit', (code) => process.exit(code ?? 0));
1171
+ // Prevent this process from continuing
1172
+ await new Promise(() => { });
1173
+ }
1174
+ catch {
1175
+ // Bun not available, continue with Node
1176
+ }
1177
+ }
1178
+ const handler = commands[command];
1179
+ if (!handler) {
1180
+ console.error(`Unknown command: ${command}`);
1181
+ console.error('Run `dario help` for usage.');
1182
+ process.exit(1);
1183
+ }
1184
+ handler().catch(err => {
1185
+ console.error('Fatal error:', sanitizeError(err));
1186
+ process.exit(1);
1187
+ });
1167
1188
  }
1168
- handler().catch(err => {
1169
- console.error('Fatal error:', sanitizeError(err));
1170
- process.exit(1);
1171
- });
@@ -50,6 +50,16 @@ export interface RequestQueueOptions {
50
50
  maxConcurrent?: number;
51
51
  maxQueued?: number;
52
52
  queueTimeoutMs?: number;
53
+ /**
54
+ * Whether timeout timers are `unref`'d so they don't by themselves keep
55
+ * the Node event loop alive. Default `true` — appropriate for the proxy,
56
+ * where a leaked queue entry should never hang shutdown. Pass `false` in
57
+ * tests where the queue is the only pending work on the loop: an
58
+ * `unref`'d timer won't fire in that case (Node exits with "unsettled
59
+ * top-level await" before the 50ms timeout elapses), so the reject the
60
+ * test is waiting for never arrives.
61
+ */
62
+ unrefTimers?: boolean;
53
63
  }
54
64
  export declare const DEFAULT_MAX_CONCURRENT = 10;
55
65
  export declare const DEFAULT_MAX_QUEUED = 128;
@@ -58,6 +68,7 @@ export declare class RequestQueue {
58
68
  readonly maxConcurrent: number;
59
69
  readonly maxQueued: number;
60
70
  readonly queueTimeoutMs: number;
71
+ readonly unrefTimers: boolean;
61
72
  private active;
62
73
  private queue;
63
74
  constructor(opts?: RequestQueueOptions);
@@ -47,12 +47,14 @@ export class RequestQueue {
47
47
  maxConcurrent;
48
48
  maxQueued;
49
49
  queueTimeoutMs;
50
+ unrefTimers;
50
51
  active = 0;
51
52
  queue = [];
52
53
  constructor(opts = {}) {
53
54
  this.maxConcurrent = opts.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
54
55
  this.maxQueued = opts.maxQueued ?? DEFAULT_MAX_QUEUED;
55
56
  this.queueTimeoutMs = opts.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS;
57
+ this.unrefTimers = opts.unrefTimers ?? true;
56
58
  }
57
59
  /**
58
60
  * Acquire a concurrency slot. Resolves when admitted; throws
@@ -80,7 +82,9 @@ export class RequestQueue {
80
82
  }, this.queueTimeoutMs);
81
83
  // Keep the timer from pinning the event loop open on shutdown. A queued
82
84
  // request waiting for a slot shouldn't by itself keep the process alive.
83
- timeoutHandle.unref?.();
85
+ // Opt-out for tests — see `unrefTimers` comment in RequestQueueOptions.
86
+ if (this.unrefTimers)
87
+ timeoutHandle.unref?.();
84
88
  const entry = { resolve, reject, enqueuedAt, timeoutHandle };
85
89
  this.queue.push(entry);
86
90
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.31.14",
3
+ "version": "3.31.15",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {