@leo000001/opencode-quota-sidebar 4.0.5 → 4.0.11

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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
2
3
  import { type HistoryPeriod } from './period.js';
3
4
  type CliCommand = {
4
5
  period: HistoryPeriod;
@@ -10,9 +11,46 @@ type CliServerCommand = {
10
11
  args: string[];
11
12
  shell?: boolean;
12
13
  };
14
+ type SpawnedCliServerProcess = ReturnType<typeof spawn>;
15
+ export declare function releaseCliServerProcess(proc: {
16
+ stdin?: {
17
+ destroy: () => void;
18
+ } | null;
19
+ stdout?: {
20
+ destroy: () => void;
21
+ } | null;
22
+ stderr?: {
23
+ destroy: () => void;
24
+ } | null;
25
+ unref: () => void;
26
+ }): void;
27
+ export declare function terminateCliServerProcess(proc: {
28
+ pid?: number;
29
+ killed?: boolean;
30
+ kill: (signal?: NodeJS.Signals) => boolean;
31
+ stdin?: {
32
+ destroy: () => void;
33
+ } | null;
34
+ stdout?: {
35
+ destroy: () => void;
36
+ } | null;
37
+ stderr?: {
38
+ destroy: () => void;
39
+ } | null;
40
+ unref: () => void;
41
+ }, options?: {
42
+ platform?: NodeJS.Platform;
43
+ killProcess?: typeof process.kill;
44
+ }): void;
45
+ export declare function extractCliServerUrl(output: string): string | undefined;
13
46
  export declare function parseCliArgs(argv: string[]): CliCommand;
14
47
  export declare function cliBaseUrl(): string;
15
48
  export declare function cliServerCommandCandidates(platform?: NodeJS.Platform): CliServerCommand[];
49
+ 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
+ }>;
16
54
  export declare function runCli(argv: string[]): Promise<string>;
17
55
  export declare function cliExitCodeForError(message: string): 0 | 1;
18
56
  export declare function cliShouldRunMain(argv1?: string, modulePath?: string, resolvePath?: (filePath: string) => string): boolean;
package/dist/cli.js CHANGED
@@ -15,6 +15,42 @@ import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, listC
15
15
  import { createUsageService } from './usage_service.js';
16
16
  const DEFAULT_OPENCODE_BASE_URL = 'http://localhost:4096';
17
17
  const CLI_SERVER_TIMEOUT_MS = 10_000;
18
+ const CLI_FORCE_EXIT_DELAY_MS = 100;
19
+ export function releaseCliServerProcess(proc) {
20
+ // The CLI only needs the child pipes until the server prints its listen URL.
21
+ // After that, unref/destroy them so the parent process can exit cleanly.
22
+ proc.stdin?.destroy();
23
+ proc.stdout?.destroy();
24
+ proc.stderr?.destroy();
25
+ proc.unref();
26
+ }
27
+ export function terminateCliServerProcess(proc, options) {
28
+ releaseCliServerProcess(proc);
29
+ if (proc.killed)
30
+ return;
31
+ const platform = options?.platform ?? process.platform;
32
+ const killProcess = options?.killProcess ?? process.kill;
33
+ if (platform !== 'win32' && typeof proc.pid === 'number' && proc.pid > 0) {
34
+ try {
35
+ killProcess(-proc.pid, 'SIGTERM');
36
+ return;
37
+ }
38
+ catch {
39
+ // Fall back to the direct child pid when group termination is unavailable.
40
+ }
41
+ }
42
+ proc.kill('SIGTERM');
43
+ }
44
+ export function extractCliServerUrl(output) {
45
+ for (const line of output.split('\n')) {
46
+ if (!line.startsWith('opencode server listening'))
47
+ continue;
48
+ const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
49
+ if (match)
50
+ return match[1];
51
+ }
52
+ return undefined;
53
+ }
18
54
  const HELP_TEXT = `opencode-quota
19
55
 
20
56
  Usage:
@@ -132,12 +168,49 @@ export function cliServerCommandCandidates(platform = process.platform) {
132
168
  }
133
169
  return [{ command: 'opencode', args: directArgs }];
134
170
  }
135
- async function tryStartCliOpencodeServer(candidate) {
171
+ function releaseCliServerPipes(proc, inspect, onError, onExit) {
172
+ if (inspect) {
173
+ proc.stdout?.removeListener('data', inspect);
174
+ proc.stderr?.removeListener('data', inspect);
175
+ }
176
+ if (onError)
177
+ proc.removeListener('error', onError);
178
+ if (onExit)
179
+ proc.removeListener('exit', onExit);
180
+ proc.stdout?.unpipe();
181
+ proc.stderr?.unpipe();
182
+ proc.stdout?.destroy();
183
+ proc.stderr?.destroy();
184
+ }
185
+ export function closeCliServerProcess(proc, platform = process.platform, killProcess = process.kill, spawnProcess = spawn) {
186
+ const pid = proc.pid;
187
+ if (typeof pid !== 'number' || pid <= 0)
188
+ return;
189
+ if (platform === 'win32') {
190
+ const killer = spawnProcess('taskkill', ['/PID', String(pid), '/T', '/F'], {
191
+ stdio: 'ignore',
192
+ windowsHide: true,
193
+ });
194
+ killer.unref();
195
+ return;
196
+ }
197
+ try {
198
+ killProcess(-pid, 'SIGTERM');
199
+ }
200
+ catch (error) {
201
+ const code = error.code;
202
+ if (code !== 'ESRCH')
203
+ throw error;
204
+ }
205
+ }
206
+ export async function tryStartCliOpencodeServer(candidate, spawnProcess = spawn, closeProcess = closeCliServerProcess) {
136
207
  let proc;
137
208
  try {
138
- proc = spawn(candidate.command, candidate.args, {
209
+ proc = spawnProcess(candidate.command, candidate.args, {
139
210
  env: process.env,
211
+ stdio: ['pipe', 'pipe', 'pipe'],
140
212
  shell: candidate.shell ?? false,
213
+ detached: true,
141
214
  windowsHide: true,
142
215
  });
143
216
  }
@@ -150,52 +223,67 @@ async function tryStartCliOpencodeServer(candidate) {
150
223
  };
151
224
  }
152
225
  const url = await new Promise((resolve, reject) => {
226
+ let inspect;
227
+ let onError;
228
+ let onExit;
229
+ let output = '';
230
+ let settled = false;
153
231
  const id = setTimeout(() => {
232
+ if (settled)
233
+ return;
234
+ settled = true;
235
+ releaseCliServerPipes(proc, inspect, onError, onExit);
236
+ closeProcess(proc);
154
237
  reject(new Error(`Timeout waiting for OpenCode server to start after ${CLI_SERVER_TIMEOUT_MS}ms`));
155
238
  }, CLI_SERVER_TIMEOUT_MS);
156
- let output = '';
157
- let settled = false;
158
- const inspect = (chunk) => {
239
+ id.unref?.();
240
+ const fail = (failure) => {
241
+ if (settled)
242
+ return;
243
+ settled = true;
244
+ clearTimeout(id);
245
+ releaseCliServerPipes(proc, inspect, onError, onExit);
246
+ reject(failure);
247
+ };
248
+ inspect = (chunk) => {
159
249
  output += chunk.toString();
160
- const lines = output.split('\n');
161
- for (const line of lines) {
162
- if (!line.startsWith('opencode server listening'))
163
- continue;
164
- const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
165
- if (!match)
166
- continue;
250
+ const url = extractCliServerUrl(output);
251
+ if (url) {
167
252
  clearTimeout(id);
168
253
  settled = true;
169
- resolve(match[1]);
254
+ releaseCliServerPipes(proc, inspect, onError, onExit);
255
+ // The CLI only needs the startup line; after that the detached server
256
+ // must not keep the parent process alive.
257
+ proc.stdin?.destroy();
258
+ proc.unref();
259
+ resolve(url);
170
260
  return;
171
261
  }
172
262
  };
173
263
  proc.stdout?.on('data', inspect);
174
264
  proc.stderr?.on('data', inspect);
175
- proc.on('error', (error) => {
176
- clearTimeout(id);
265
+ onError = (error) => {
177
266
  const code = error.code;
178
- reject({
267
+ fail({
179
268
  error,
180
269
  output,
181
270
  recoverable: code === 'ENOENT' || code === 'EINVAL',
182
271
  });
183
- });
184
- proc.on('exit', (code) => {
185
- if (settled)
186
- return;
187
- clearTimeout(id);
272
+ };
273
+ onExit = (code) => {
188
274
  let message = `OpenCode server exited with code ${code}`;
189
275
  if (output.trim())
190
276
  message += `\n${output}`;
191
277
  const recoverable = /not recognized as an internal or external command/i.test(output) ||
192
278
  /command not found/i.test(output);
193
- reject({ error: new Error(message), output, recoverable });
194
- });
279
+ fail({ error: new Error(message), output, recoverable });
280
+ };
281
+ proc.on('error', onError);
282
+ proc.on('exit', onExit);
195
283
  });
196
284
  return {
197
285
  url,
198
- close: () => proc.kill(),
286
+ close: () => closeProcess(proc),
199
287
  };
200
288
  }
201
289
  async function startCliOpencodeServer() {
@@ -249,23 +337,40 @@ async function resolvePathInfo(directory) {
249
337
  throw new Error(`Failed to connect to OpenCode API at ${cliBaseUrl()}: ${error instanceof Error ? error.message : String(error)}`);
250
338
  }
251
339
  const server = await startCliOpencodeServer();
252
- const client = createOpencodeClient({
253
- directory,
254
- baseUrl: server.url,
255
- });
256
- const response = await client.path.get({
257
- query: { directory },
258
- throwOnError: true,
259
- });
260
- const data = response.data;
261
- return {
262
- client,
263
- worktree: data.worktree || directory,
264
- directory: data.directory || directory,
265
- close: () => server.close(),
266
- };
340
+ try {
341
+ const client = createOpencodeClient({
342
+ directory,
343
+ baseUrl: server.url,
344
+ });
345
+ const response = await client.path.get({
346
+ query: { directory },
347
+ throwOnError: true,
348
+ });
349
+ const data = response.data;
350
+ return {
351
+ client,
352
+ worktree: data.worktree || directory,
353
+ directory: data.directory || directory,
354
+ close: () => server.close(),
355
+ };
356
+ }
357
+ catch (innerError) {
358
+ server.close();
359
+ throw innerError;
360
+ }
267
361
  }
268
362
  }
363
+ function writeCliLine(stream, value) {
364
+ return new Promise((resolve, reject) => {
365
+ stream.write(`${value}\n`, (error) => {
366
+ if (error) {
367
+ reject(error);
368
+ return;
369
+ }
370
+ resolve();
371
+ });
372
+ });
373
+ }
269
374
  export async function runCli(argv) {
270
375
  const command = parseCliArgs(argv);
271
376
  const cwd = process.cwd();
@@ -292,6 +397,7 @@ export async function runCli(argv) {
292
397
  statePath,
293
398
  client: client,
294
399
  directory,
400
+ worktree,
295
401
  persistence: {
296
402
  markDirty: () => { },
297
403
  scheduleSave: () => { },
@@ -350,16 +456,21 @@ export function cliShouldRunMain(argv1 = process.argv[1], modulePath = fileURLTo
350
456
  return resolvePath(modulePath) === resolvePath(argv1);
351
457
  }
352
458
  async function main() {
459
+ let exitCode = 0;
353
460
  try {
354
461
  const output = await runCli(process.argv.slice(2));
355
- process.stdout.write(`${output}\n`);
462
+ await writeCliLine(process.stdout, output);
356
463
  }
357
464
  catch (error) {
358
465
  const message = error instanceof Error ? error.message : String(error);
359
- const exitCode = cliExitCodeForError(message);
466
+ exitCode = cliExitCodeForError(message);
360
467
  const stream = exitCode === 0 ? process.stdout : process.stderr;
361
- stream.write(`${message}\n`);
468
+ await writeCliLine(stream, message);
469
+ }
470
+ finally {
362
471
  process.exitCode = exitCode;
472
+ const forceExit = setTimeout(() => process.exit(exitCode), CLI_FORCE_EXIT_DELAY_MS);
473
+ forceExit.unref?.();
363
474
  }
364
475
  }
365
476
  if (cliShouldRunMain()) {
@@ -1,6 +1,6 @@
1
- import type { QuotaSnapshot } from './types.js';
2
- import { type UsageSummary } from './usage.js';
3
- import type { HistoryUsageResult } from './usage_service.js';
1
+ import type { QuotaSnapshot } from "./types.js";
2
+ import { type UsageSummary } from "./usage.js";
3
+ import type { HistoryUsageResult } from "./usage_service.js";
4
4
  export declare function renderCliDashboard(input: {
5
5
  label: string;
6
6
  usage: UsageSummary;
@@ -14,4 +14,4 @@ export declare function renderCliHistoryDashboard(input: {
14
14
  width?: number;
15
15
  showCost?: boolean;
16
16
  }): string;
17
- export declare function cliCurrentLabel(period: 'day' | 'week' | 'month'): "Today" | "This Week" | "This Month";
17
+ export declare function cliCurrentLabel(period: "day" | "week" | "month"): "Today" | "This Week" | "This Month";