@leo000001/opencode-quota-sidebar 4.0.16 → 4.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.
package/dist/cli.d.ts CHANGED
@@ -12,6 +12,12 @@ type CliServerCommand = {
12
12
  shell?: boolean;
13
13
  };
14
14
  type SpawnedCliServerProcess = ReturnType<typeof spawn>;
15
+ type CliPortResolver = (preferredPort: number, attemptedPorts: ReadonlySet<number>) => Promise<number>;
16
+ type StartedCliServer = {
17
+ url: string;
18
+ pid?: number;
19
+ close: () => void;
20
+ };
15
21
  export declare function releaseCliServerProcess(proc: {
16
22
  stdin?: {
17
23
  destroy: () => void;
@@ -45,12 +51,24 @@ export declare function terminateCliServerProcess(proc: {
45
51
  export declare function extractCliServerUrl(output: string): string | undefined;
46
52
  export declare function parseCliArgs(argv: string[]): CliCommand;
47
53
  export declare function cliBaseUrl(): string;
48
- export declare function cliServerCommandCandidates(platform?: NodeJS.Platform): CliServerCommand[];
54
+ export declare function cliServerCommandCandidates(platform?: NodeJS.Platform, port?: number): CliServerCommand[];
55
+ export declare function isCliServerPortConflictFailure(failure: unknown): boolean;
56
+ export declare function registerCliShutdownCleanup(cleanup: () => void, options?: {
57
+ targetProcess?: NodeJS.Process;
58
+ killProcess?: typeof process.kill;
59
+ }): () => void;
60
+ export declare function reserveCliServerPort(preferredPort?: number, attemptedPorts?: ReadonlySet<number>): Promise<number>;
49
61
  export declare function closeCliServerProcess(proc: SpawnedCliServerProcess, platform?: NodeJS.Platform, killProcess?: typeof process.kill, spawnProcess?: typeof spawn): void;
50
- export declare function tryStartCliOpencodeServer(candidate: CliServerCommand, spawnProcess?: typeof spawn, closeProcess?: typeof closeCliServerProcess): Promise<{
51
- url: string;
52
- close: () => void;
53
- }>;
62
+ export declare function tryStartCliOpencodeServer(candidate: CliServerCommand, spawnProcess?: typeof spawn, closeProcess?: typeof closeCliServerProcess): Promise<StartedCliServer>;
63
+ export declare function spawnCliServerWatchdog(targetPid: number | undefined, ttlMs?: number, spawnProcess?: typeof spawn, nodeExecPath?: string): import("child_process").ChildProcess | undefined;
64
+ export declare function startCliOpencodeServer(options?: {
65
+ platform?: NodeJS.Platform;
66
+ spawnProcess?: typeof spawn;
67
+ spawnWatchdogProcess?: typeof spawn;
68
+ closeProcess?: typeof closeCliServerProcess;
69
+ reservePort?: CliPortResolver;
70
+ tempServerTtlMs?: number;
71
+ }): Promise<StartedCliServer>;
54
72
  export declare function runCli(argv: string[]): Promise<string>;
55
73
  export declare function cliExitCodeForError(message: string): 0 | 1;
56
74
  export declare function cliShouldRunMain(argv1?: string, modulePath?: string, resolvePath?: (filePath: string) => string): boolean;
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
+ import { createServer } from 'node:net';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import { createOpencodeClient } from '@opencode-ai/sdk/client';
7
8
  import { cliCurrentLabel, renderCliDashboard, renderCliHistoryDashboard, } from './cli_render.js';
@@ -38,8 +39,16 @@ function strictFilterHistoryProviders(history, allowedProviderIDs) {
38
39
  return filterHistoryProvidersForDisplay(history, allowedProviderIDs);
39
40
  }
40
41
  const DEFAULT_OPENCODE_BASE_URL = 'http://localhost:4096';
41
- const CLI_SERVER_TIMEOUT_MS = 10_000;
42
+ const DEFAULT_OPENCODE_HOST = '127.0.0.1';
43
+ const DEFAULT_OPENCODE_PORT = 4096;
44
+ const CLI_SERVER_TIMEOUT_MS = 60_000;
42
45
  const CLI_FORCE_EXIT_DELAY_MS = 100;
46
+ const CLI_SERVER_PORT_RETRY_LIMIT = 5;
47
+ const CLI_PORT_RESERVE_RETRY_LIMIT = 16;
48
+ const CLI_TEMP_SERVER_TTL_MS = 10 * 60_000;
49
+ const CLI_TEMP_SERVER_HINT = 'CLI note: if the temporary OpenCode server keeps failing, run `opencode serve` manually to inspect logs, or set `OPENCODE_BASE_URL`.';
50
+ const CLI_SHUTDOWN_HOOK_KEY = Symbol('quota-sidebar.cliShutdownHook');
51
+ const CLI_SHUTDOWN_CALLBACKS_KEY = Symbol('quota-sidebar.cliShutdownCallbacks');
43
52
  export function releaseCliServerProcess(proc) {
44
53
  // The CLI only needs the child pipes until the server prints its listen URL.
45
54
  // After that, unref/destroy them so the parent process can exit cleanly.
@@ -174,24 +183,158 @@ export function cliBaseUrl() {
174
183
  function isDefaultBaseUrl() {
175
184
  return !process.env.OPENCODE_BASE_URL?.trim();
176
185
  }
177
- export function cliServerCommandCandidates(platform = process.platform) {
178
- const directArgs = ['serve', '--hostname=127.0.0.1', '--port=4096'];
186
+ export function cliServerCommandCandidates(platform = process.platform, port = DEFAULT_OPENCODE_PORT) {
187
+ const directArgs = [
188
+ 'serve',
189
+ `--hostname=${DEFAULT_OPENCODE_HOST}`,
190
+ `--port=${port}`,
191
+ ];
192
+ const directCommand = `opencode serve --hostname=${DEFAULT_OPENCODE_HOST} --port=${port}`;
179
193
  if (platform === 'win32') {
180
194
  return [
181
195
  { command: 'opencode.cmd', args: directArgs },
182
196
  {
183
- command: 'opencode serve --hostname=127.0.0.1 --port=4096',
197
+ command: directCommand,
184
198
  args: [],
185
199
  shell: true,
186
200
  },
187
201
  {
188
202
  command: 'bash',
189
- args: ['-lc', 'opencode serve --hostname=127.0.0.1 --port=4096'],
203
+ args: ['-lc', directCommand],
190
204
  },
191
205
  ];
192
206
  }
193
207
  return [{ command: 'opencode', args: directArgs }];
194
208
  }
209
+ function cliServerFailureOutput(failure) {
210
+ if (typeof failure !== 'object' ||
211
+ failure === null ||
212
+ !('output' in failure)) {
213
+ return '';
214
+ }
215
+ const output = failure.output;
216
+ return typeof output === 'string' ? output : '';
217
+ }
218
+ function cliServerFailureError(failure) {
219
+ if (typeof failure === 'object' && failure !== null && 'error' in failure) {
220
+ const error = failure.error;
221
+ return error instanceof Error ? error : new Error(String(error));
222
+ }
223
+ return failure instanceof Error ? failure : new Error(String(failure));
224
+ }
225
+ function isRecoverableCliServerFailure(failure) {
226
+ return (typeof failure === 'object' &&
227
+ failure !== null &&
228
+ 'recoverable' in failure &&
229
+ failure.recoverable === true);
230
+ }
231
+ export function isCliServerPortConflictFailure(failure) {
232
+ const text = [
233
+ cliServerFailureError(failure).message,
234
+ cliServerFailureOutput(failure),
235
+ ]
236
+ .filter(Boolean)
237
+ .join('\n');
238
+ return /eaddrinuse|address already in use|port .*already in use|listen .*in use/i.test(text);
239
+ }
240
+ function describeCliError(error) {
241
+ return error instanceof Error ? error.message : String(error);
242
+ }
243
+ function formatCliTempServerFailure(prefix, error) {
244
+ return `${prefix}: ${describeCliError(error)}\n\n${CLI_TEMP_SERVER_HINT}`;
245
+ }
246
+ function runCliShutdownCallbacks(targetProcess) {
247
+ const callbacks = targetProcess[CLI_SHUTDOWN_CALLBACKS_KEY];
248
+ if (!callbacks)
249
+ return;
250
+ for (const callback of Array.from(callbacks)) {
251
+ try {
252
+ callback();
253
+ }
254
+ catch {
255
+ // Best-effort cleanup for CLI temp servers.
256
+ }
257
+ }
258
+ }
259
+ export function registerCliShutdownCleanup(cleanup, options) {
260
+ const targetProcess = options?.targetProcess ||
261
+ process;
262
+ const callbacks = targetProcess[CLI_SHUTDOWN_CALLBACKS_KEY] ||
263
+ (targetProcess[CLI_SHUTDOWN_CALLBACKS_KEY] = new Set());
264
+ callbacks.add(cleanup);
265
+ if (!targetProcess[CLI_SHUTDOWN_HOOK_KEY]) {
266
+ targetProcess[CLI_SHUTDOWN_HOOK_KEY] = true;
267
+ targetProcess.once('exit', () => {
268
+ runCliShutdownCallbacks(targetProcess);
269
+ });
270
+ targetProcess.once('uncaughtExceptionMonitor', () => {
271
+ runCliShutdownCallbacks(targetProcess);
272
+ });
273
+ for (const signal of ['SIGINT', 'SIGTERM']) {
274
+ targetProcess.once(signal, () => {
275
+ runCliShutdownCallbacks(targetProcess);
276
+ if (typeof targetProcess.pid === 'number' && targetProcess.pid > 0) {
277
+ ;
278
+ (options?.killProcess ?? process.kill)(targetProcess.pid, signal);
279
+ }
280
+ });
281
+ }
282
+ }
283
+ return () => {
284
+ callbacks.delete(cleanup);
285
+ };
286
+ }
287
+ function tryReserveCliServerPort(port) {
288
+ return new Promise((resolve, reject) => {
289
+ const server = createServer();
290
+ let settled = false;
291
+ const finish = (value, error) => {
292
+ if (settled)
293
+ return;
294
+ settled = true;
295
+ server.removeAllListeners();
296
+ if (error) {
297
+ reject(error);
298
+ return;
299
+ }
300
+ resolve(value);
301
+ };
302
+ server.once('error', (error) => {
303
+ const code = error.code;
304
+ if (code === 'EADDRINUSE' || code === 'EACCES') {
305
+ finish(undefined);
306
+ return;
307
+ }
308
+ finish(undefined, error);
309
+ });
310
+ server.listen({ host: DEFAULT_OPENCODE_HOST, port, exclusive: true }, () => {
311
+ const address = server.address();
312
+ const resolvedPort = typeof address === 'object' && address ? address.port : port;
313
+ server.close((error) => {
314
+ if (error) {
315
+ finish(undefined, error);
316
+ return;
317
+ }
318
+ finish(resolvedPort);
319
+ });
320
+ });
321
+ server.unref();
322
+ });
323
+ }
324
+ export async function reserveCliServerPort(preferredPort = DEFAULT_OPENCODE_PORT, attemptedPorts = new Set()) {
325
+ if (!attemptedPorts.has(preferredPort)) {
326
+ const preferred = await tryReserveCliServerPort(preferredPort);
327
+ if (preferred !== undefined)
328
+ return preferred;
329
+ }
330
+ for (let attempt = 0; attempt < CLI_PORT_RESERVE_RETRY_LIMIT; attempt++) {
331
+ const fallback = await tryReserveCliServerPort(0);
332
+ if (fallback === undefined || attemptedPorts.has(fallback))
333
+ continue;
334
+ return fallback;
335
+ }
336
+ throw new Error('Failed to reserve a local port for the temporary OpenCode server');
337
+ }
195
338
  function releaseCliServerPipes(proc, inspect, onError, onExit) {
196
339
  if (inspect) {
197
340
  proc.stdout?.removeListener('data', inspect);
@@ -307,35 +450,97 @@ export async function tryStartCliOpencodeServer(candidate, spawnProcess = spawn,
307
450
  });
308
451
  return {
309
452
  url,
453
+ pid: proc.pid,
310
454
  close: () => closeProcess(proc),
311
455
  };
312
456
  }
313
- async function startCliOpencodeServer() {
314
- const candidates = cliServerCommandCandidates();
457
+ export function spawnCliServerWatchdog(targetPid, ttlMs = CLI_TEMP_SERVER_TTL_MS, spawnProcess = spawn, nodeExecPath = process.execPath) {
458
+ if (typeof targetPid !== 'number' ||
459
+ targetPid <= 0 ||
460
+ !Number.isFinite(ttlMs) ||
461
+ ttlMs <= 0) {
462
+ return undefined;
463
+ }
464
+ const script = [
465
+ "const { spawn } = require('node:child_process')",
466
+ `const pid = ${JSON.stringify(targetPid)}`,
467
+ `const ttl = ${JSON.stringify(Math.floor(ttlMs))}`,
468
+ 'setTimeout(() => {',
469
+ " if (process.platform === 'win32') {",
470
+ " const killer = spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true })",
471
+ ' killer.unref?.()',
472
+ ' process.exit(0)',
473
+ ' }',
474
+ ' try {',
475
+ " process.kill(-pid, 'SIGTERM')",
476
+ ' } catch (error) {',
477
+ " if (!error || (typeof error === 'object' && error !== null && error.code !== 'ESRCH')) {",
478
+ " try { process.kill(pid, 'SIGTERM') } catch {}",
479
+ ' }',
480
+ ' }',
481
+ ' process.exit(0)',
482
+ '}, ttl)',
483
+ ].join('\n');
484
+ const proc = spawnProcess(nodeExecPath, ['-e', script], {
485
+ stdio: 'ignore',
486
+ detached: true,
487
+ windowsHide: true,
488
+ });
489
+ proc.unref();
490
+ return proc;
491
+ }
492
+ export async function startCliOpencodeServer(options) {
493
+ const platform = options?.platform ?? process.platform;
494
+ const spawnProcess = options?.spawnProcess ?? spawn;
495
+ const spawnWatchdogProcess = options?.spawnWatchdogProcess ?? spawn;
496
+ const closeProcess = options?.closeProcess ?? closeCliServerProcess;
497
+ const resolvePort = options?.reservePort ?? reserveCliServerPort;
498
+ const tempServerTtlMs = options?.tempServerTtlMs ?? CLI_TEMP_SERVER_TTL_MS;
315
499
  let lastError;
316
- for (const candidate of candidates) {
317
- try {
318
- return await tryStartCliOpencodeServer(candidate);
319
- }
320
- catch (failure) {
321
- lastError = failure;
322
- const recoverable = typeof failure === 'object' &&
323
- failure !== null &&
324
- 'recoverable' in failure &&
325
- failure.recoverable === true;
326
- if (!recoverable) {
327
- const error = typeof failure === 'object' && failure !== null && 'error' in failure
328
- ? failure.error
329
- : failure;
330
- throw error instanceof Error ? error : new Error(String(error));
500
+ const attemptedPorts = new Set();
501
+ for (let attempt = 0; attempt < CLI_SERVER_PORT_RETRY_LIMIT; attempt++) {
502
+ const port = await resolvePort(DEFAULT_OPENCODE_PORT, attemptedPorts);
503
+ attemptedPorts.add(port);
504
+ for (const candidate of cliServerCommandCandidates(platform, port)) {
505
+ try {
506
+ const server = await tryStartCliOpencodeServer(candidate, spawnProcess, closeProcess);
507
+ let watchdogProc;
508
+ try {
509
+ watchdogProc = spawnCliServerWatchdog(server.pid, tempServerTtlMs, spawnWatchdogProcess);
510
+ }
511
+ catch (error) {
512
+ server.close();
513
+ throw new Error(`Failed to start temporary OpenCode server watchdog: ${describeCliError(error)}`);
514
+ }
515
+ const stopWatchdog = () => {
516
+ if (!watchdogProc)
517
+ return;
518
+ const proc = watchdogProc;
519
+ watchdogProc = undefined;
520
+ closeProcess(proc);
521
+ };
522
+ return {
523
+ ...server,
524
+ close: () => {
525
+ stopWatchdog();
526
+ server.close();
527
+ },
528
+ };
529
+ }
530
+ catch (failure) {
531
+ lastError = failure;
532
+ if (isCliServerPortConflictFailure(failure))
533
+ break;
534
+ if (!isRecoverableCliServerFailure(failure)) {
535
+ throw cliServerFailureError(failure);
536
+ }
331
537
  }
332
538
  }
539
+ if (!isCliServerPortConflictFailure(lastError))
540
+ break;
333
541
  }
334
- const error = typeof lastError === 'object' && lastError !== null && 'error' in lastError
335
- ? lastError.error
336
- : lastError;
337
- throw error instanceof Error
338
- ? error
542
+ throw lastError
543
+ ? cliServerFailureError(lastError)
339
544
  : new Error('Failed to start OpenCode server');
340
545
  }
341
546
  async function resolvePathInfo(directory) {
@@ -360,7 +565,27 @@ async function resolvePathInfo(directory) {
360
565
  if (!isDefaultBaseUrl()) {
361
566
  throw new Error(`Failed to connect to OpenCode API at ${cliBaseUrl()}: ${error instanceof Error ? error.message : String(error)}`);
362
567
  }
363
- const server = await startCliOpencodeServer();
568
+ let server;
569
+ try {
570
+ server = await startCliOpencodeServer();
571
+ }
572
+ catch (startError) {
573
+ throw new Error(formatCliTempServerFailure('Failed to auto-start a temporary OpenCode server', startError));
574
+ }
575
+ let closed = false;
576
+ const unregisterTempServerCleanup = registerCliShutdownCleanup(() => {
577
+ if (closed)
578
+ return;
579
+ closed = true;
580
+ server.close();
581
+ });
582
+ const closeTempServer = () => {
583
+ if (closed)
584
+ return;
585
+ closed = true;
586
+ unregisterTempServerCleanup();
587
+ server.close();
588
+ };
364
589
  try {
365
590
  const client = createOpencodeClient({
366
591
  directory,
@@ -375,12 +600,12 @@ async function resolvePathInfo(directory) {
375
600
  client,
376
601
  worktree: data.worktree || directory,
377
602
  directory: data.directory || directory,
378
- close: () => server.close(),
603
+ close: closeTempServer,
379
604
  };
380
605
  }
381
606
  catch (innerError) {
382
- server.close();
383
- throw innerError;
607
+ closeTempServer();
608
+ throw new Error(formatCliTempServerFailure(`Failed to query the temporary OpenCode server at ${server.url}`, innerError));
384
609
  }
385
610
  }
386
611
  }
package/dist/tui.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
3
3
  declare const plugin: TuiPluginModule & {
4
4
  id: string;
5
5
  };