@jackwener/opencli 1.5.4 → 1.5.5

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.
@@ -9282,6 +9282,61 @@
9282
9282
  "type": "ts",
9283
9283
  "modulePath": "sinafinance/news.js"
9284
9284
  },
9285
+ {
9286
+ "site": "sinafinance",
9287
+ "name": "rolling-news",
9288
+ "description": "新浪财经滚动新闻",
9289
+ "domain": "finance.sina.com.cn/roll",
9290
+ "strategy": "cookie",
9291
+ "browser": true,
9292
+ "args": [],
9293
+ "columns": [
9294
+ "column",
9295
+ "title",
9296
+ "date",
9297
+ "url"
9298
+ ],
9299
+ "type": "ts",
9300
+ "modulePath": "sinafinance/rolling-news.js"
9301
+ },
9302
+ {
9303
+ "site": "sinafinance",
9304
+ "name": "stock",
9305
+ "description": "新浪财经行情(A股/港股/美股)",
9306
+ "domain": "suggest3.sinajs.cn,hq.sinajs.cn",
9307
+ "strategy": "public",
9308
+ "browser": false,
9309
+ "args": [
9310
+ {
9311
+ "name": "key",
9312
+ "type": "string",
9313
+ "required": true,
9314
+ "positional": true,
9315
+ "help": "Stock name or code (e.g. 贵州茅台, 腾讯控股, AAPL)"
9316
+ },
9317
+ {
9318
+ "name": "market",
9319
+ "type": "string",
9320
+ "default": "auto",
9321
+ "required": false,
9322
+ "help": "Market: cn, hk, us, auto (default: auto searches cn → hk → us)"
9323
+ }
9324
+ ],
9325
+ "columns": [
9326
+ "Symbol",
9327
+ "Name",
9328
+ "Price",
9329
+ "Change",
9330
+ "ChangePercent",
9331
+ "Open",
9332
+ "High",
9333
+ "Low",
9334
+ "Volume",
9335
+ "MarketCap"
9336
+ ],
9337
+ "type": "ts",
9338
+ "modulePath": "sinafinance/stock.js"
9339
+ },
9285
9340
  {
9286
9341
  "site": "smzdm",
9287
9342
  "name": "search",
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { PKG_VERSION } from './version.js';
14
14
  import { printCompletionScript } from './completion.js';
15
15
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
16
16
  import { registerAllCommands } from './commanderAdapter.js';
17
- import { getErrorMessage } from './errors.js';
17
+ import { EXIT_CODES, getErrorMessage } from './errors.js';
18
18
  export function runCli(BUILTIN_CLIS, USER_CLIS) {
19
19
  const program = new Command();
20
20
  // enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
@@ -108,7 +108,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
108
108
  const { verifyClis, renderVerifyReport } = await import('./verify.js');
109
109
  const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
110
110
  console.log(renderVerifyReport(r));
111
- process.exitCode = r.ok ? 0 : 1;
111
+ process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
112
112
  });
113
113
  // ── Built-in: explore / synthesize / generate / cascade ───────────────────
114
114
  program
@@ -164,7 +164,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
164
164
  workspace,
165
165
  });
166
166
  console.log(renderGenerateSummary(r));
167
- process.exitCode = r.ok ? 0 : 1;
167
+ process.exitCode = r.ok ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR;
168
168
  });
169
169
  // ── Built-in: record ─────────────────────────────────────────────────────
170
170
  program
@@ -186,7 +186,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
186
186
  timeoutMs: parseInt(opts.timeout, 10),
187
187
  });
188
188
  console.log(renderRecordSummary(result));
189
- process.exitCode = result.candidateCount > 0 ? 0 : 1;
189
+ process.exitCode = result.candidateCount > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.EMPTY_RESULT;
190
190
  });
191
191
  program
192
192
  .command('cascade')
@@ -251,7 +251,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
251
251
  }
252
252
  catch (err) {
253
253
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
254
- process.exitCode = 1;
254
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
255
255
  }
256
256
  });
257
257
  pluginCmd
@@ -266,7 +266,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
266
266
  }
267
267
  catch (err) {
268
268
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
269
- process.exitCode = 1;
269
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
270
270
  }
271
271
  });
272
272
  pluginCmd
@@ -277,12 +277,12 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
277
277
  .action(async (name, opts) => {
278
278
  if (!name && !opts.all) {
279
279
  console.error(chalk.red('Error: Please specify a plugin name or use the --all flag.'));
280
- process.exitCode = 1;
280
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
281
281
  return;
282
282
  }
283
283
  if (name && opts.all) {
284
284
  console.error(chalk.red('Error: Cannot specify both a plugin name and --all.'));
285
- process.exitCode = 1;
285
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
286
286
  return;
287
287
  }
288
288
  const { updatePlugin, updateAllPlugins } = await import('./plugin.js');
@@ -309,7 +309,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
309
309
  console.log();
310
310
  if (hasErrors) {
311
311
  console.error(chalk.red('Completed with some errors.'));
312
- process.exitCode = 1;
312
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
313
313
  }
314
314
  else {
315
315
  console.log(chalk.green('✅ All plugins updated successfully.'));
@@ -323,7 +323,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
323
323
  }
324
324
  catch (err) {
325
325
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
326
- process.exitCode = 1;
326
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
327
327
  }
328
328
  });
329
329
  pluginCmd
@@ -408,7 +408,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
408
408
  }
409
409
  catch (err) {
410
410
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
411
- process.exitCode = 1;
411
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
412
412
  }
413
413
  });
414
414
  // ── External CLIs ─────────────────────────────────────────────────────────
@@ -421,7 +421,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
421
421
  const ext = externalClis.find(e => e.name === name);
422
422
  if (!ext) {
423
423
  console.error(chalk.red(`External CLI '${name}' not found in registry.`));
424
- process.exitCode = 1;
424
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
425
425
  return;
426
426
  }
427
427
  installExternalCli(ext);
@@ -446,7 +446,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
446
446
  }
447
447
  catch (err) {
448
448
  console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
449
- process.exitCode = 1;
449
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
450
450
  }
451
451
  }
452
452
  for (const ext of externalClis) {
@@ -485,7 +485,7 @@ export function runCli(BUILTIN_CLIS, USER_CLIS) {
485
485
  console.error(chalk.dim(` Tip: '${binary}' exists on your PATH. Use 'opencli register ${binary}' to add it as an external CLI.`));
486
486
  }
487
487
  program.outputHelp();
488
- process.exitCode = 1;
488
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
489
489
  });
490
490
  program.parse();
491
491
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { createServer } from 'node:http';
13
13
  import { CDPBridge } from '../../browser/cdp.js';
14
- import { getErrorMessage } from '../../errors.js';
14
+ import { EXIT_CODES, getErrorMessage } from '../../errors.js';
15
15
  // ─── Helpers ─────────────────────────────────────────────────────────
16
16
  function generateMsgId() {
17
17
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -506,7 +506,7 @@ export async function startServe(opts = {}) {
506
506
  console.error('\n[serve] Shutting down...');
507
507
  cdp?.close().catch(() => { });
508
508
  server.close();
509
- process.exit(0);
509
+ process.exit(EXIT_CODES.SUCCESS);
510
510
  };
511
511
  process.on('SIGTERM', shutdown);
512
512
  process.on('SIGINT', shutdown);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Sinafinance rolling news feed
3
+ */
4
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Sinafinance rolling news feed
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ cli({
6
+ site: 'sinafinance',
7
+ name: 'rolling-news',
8
+ description: '新浪财经滚动新闻',
9
+ domain: 'finance.sina.com.cn/roll',
10
+ strategy: Strategy.COOKIE,
11
+ args: [],
12
+ columns: ['column', 'title', 'date', 'url'],
13
+ func: async (page, _args) => {
14
+ await page.goto(`https://finance.sina.com.cn/roll/#pageid=384&lid=2519`);
15
+ await page.wait({ selector: '.d_list_txt li', timeout: 10000 });
16
+ const payload = await page.evaluate(`
17
+ (() => {
18
+ const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
19
+ const results = [];
20
+ document.querySelectorAll('.d_list_txt li').forEach(el => {
21
+ const titleEl = el.querySelector('.c_tit a');
22
+ const columnEl = el.querySelector('.c_chl');
23
+ const dateEl = el.querySelector('.c_time');
24
+ const url = titleEl?.getAttribute('href') || '';
25
+ if (!url) return;
26
+ results.push({
27
+ title: cleanText(titleEl?.textContent || ''),
28
+ column: cleanText(columnEl?.textContent || ''),
29
+ date: cleanText(dateEl?.textContent || ''),
30
+ url: url,
31
+ });
32
+ });
33
+ return results;
34
+ })()
35
+ `);
36
+ if (!Array.isArray(payload))
37
+ return [];
38
+ return payload;
39
+ },
40
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sinafinance stock quote — A股 / 港股 / 美股
3
+ *
4
+ * Uses two public Sina APIs (no browser required):
5
+ * suggest3.sinajs.cn — symbol search
6
+ * hq.sinajs.cn — real-time quote
7
+ */
8
+ export {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Sinafinance stock quote — A股 / 港股 / 美股
3
+ *
4
+ * Uses two public Sina APIs (no browser required):
5
+ * suggest3.sinajs.cn — symbol search
6
+ * hq.sinajs.cn — real-time quote
7
+ */
8
+ import { cli, Strategy } from '../../registry.js';
9
+ import { CliError } from '../../errors.js';
10
+ const MARKET_CN = '11';
11
+ const MARKET_HK = '31';
12
+ const MARKET_US = '41';
13
+ async function fetchGBK(url) {
14
+ const res = await fetch(url, { headers: { Referer: 'https://finance.sina.com.cn' } });
15
+ if (!res.ok)
16
+ throw new CliError('FETCH_ERROR', `Sina API HTTP ${res.status}`, 'Check your network');
17
+ const buf = await res.arrayBuffer();
18
+ return new TextDecoder('gbk').decode(buf);
19
+ }
20
+ function parseSuggest(raw, markets) {
21
+ const m = raw.match(/suggestvalue="(.*)"/s);
22
+ if (!m)
23
+ return [];
24
+ return m[1].split(';').filter(Boolean).map(s => {
25
+ const p = s.split(',');
26
+ return { name: p[4] || p[0] || '', market: p[1] || '', symbol: p[3] || '' };
27
+ }).filter(e => markets.includes(e.market));
28
+ }
29
+ function hqSymbol(e) {
30
+ if (e.market === MARKET_HK)
31
+ return `hk${e.symbol}`;
32
+ if (e.market === MARKET_US)
33
+ return `gb_${e.symbol}`;
34
+ return e.symbol; // A股: already "sh600519" / "sz300XXX"
35
+ }
36
+ function parseHq(raw, sym) {
37
+ const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
38
+ const m = raw.match(new RegExp(`hq_str_${escaped}="([^"]*)"`));
39
+ return m ? m[1].split(',') : [];
40
+ }
41
+ function fmtMktCap(val) {
42
+ const n = parseFloat(val);
43
+ if (!n)
44
+ return '';
45
+ if (n >= 1e12)
46
+ return (n / 1e12).toFixed(2) + 'T';
47
+ if (n >= 1e9)
48
+ return (n / 1e9).toFixed(2) + 'B';
49
+ if (n >= 1e6)
50
+ return (n / 1e6).toFixed(2) + 'M';
51
+ return String(n);
52
+ }
53
+ cli({
54
+ site: 'sinafinance',
55
+ name: 'stock',
56
+ description: '新浪财经行情(A股/港股/美股)',
57
+ domain: 'suggest3.sinajs.cn,hq.sinajs.cn',
58
+ strategy: Strategy.PUBLIC,
59
+ browser: false,
60
+ args: [
61
+ { name: 'key', type: 'string', required: true, positional: true, help: 'Stock name or code (e.g. 贵州茅台, 腾讯控股, AAPL)' },
62
+ { name: 'market', type: 'string', default: 'auto', help: 'Market: cn, hk, us, auto (default: auto searches cn → hk → us)' },
63
+ ],
64
+ columns: ['Symbol', 'Name', 'Price', 'Change', 'ChangePercent', 'Open', 'High', 'Low', 'Volume', 'MarketCap'],
65
+ func: async (_page, args) => {
66
+ const key = String(args.key);
67
+ const market = String(args.market);
68
+ const marketMap = {
69
+ cn: [MARKET_CN], hk: [MARKET_HK], us: [MARKET_US],
70
+ auto: [MARKET_CN, MARKET_HK, MARKET_US],
71
+ };
72
+ const targetMarkets = marketMap[market];
73
+ if (!targetMarkets) {
74
+ throw new CliError('INPUT_ERROR', `Invalid market: "${market}"`, 'Expected cn, hk, us, or auto');
75
+ }
76
+ // 1. Search symbol — only request the markets we care about
77
+ const suggestRaw = await fetchGBK(`https://suggest3.sinajs.cn/suggest/type=${targetMarkets.join(',')}&key=${encodeURIComponent(key)}`);
78
+ const entries = parseSuggest(suggestRaw, targetMarkets);
79
+ if (!entries.length) {
80
+ throw new CliError('NOT_FOUND', `No stock found for "${key}"`, 'Try a different name, code, or --market');
81
+ }
82
+ // Pick best match: score by name similarity, tiebreak by market priority
83
+ const needle = key.toLowerCase();
84
+ const score = (e) => {
85
+ const n = e.name.toLowerCase();
86
+ if (n === needle)
87
+ return 1;
88
+ if (n.includes(needle))
89
+ return needle.length / n.length;
90
+ return 0;
91
+ };
92
+ const best = entries.sort((a, b) => {
93
+ const d = score(b) - score(a);
94
+ return d !== 0 ? d : targetMarkets.indexOf(a.market) - targetMarkets.indexOf(b.market);
95
+ })[0];
96
+ // 2. Fetch quote
97
+ const sym = hqSymbol(best);
98
+ const hqRaw = await fetchGBK(`https://hq.sinajs.cn/list=${sym}`);
99
+ const f = parseHq(hqRaw, sym);
100
+ if (f.length < 2 || !f[0]) {
101
+ throw new CliError('NOT_FOUND', `No quote data for "${key}"`, 'Market may be closed or data unavailable');
102
+ }
103
+ if (best.market === MARKET_CN) {
104
+ const price = parseFloat(f[3]);
105
+ const prev = parseFloat(f[2]);
106
+ const chg = (price - prev).toFixed(2);
107
+ const chgPct = ((price - prev) / prev * 100).toFixed(2) + '%';
108
+ return [{ Symbol: sym.toUpperCase(), Name: f[0], Price: f[3], Change: chg, ChangePercent: chgPct, Open: f[1], High: f[4], Low: f[5], Volume: f[8], MarketCap: '' }];
109
+ }
110
+ if (best.market === MARKET_HK) {
111
+ // [2]=price [4]=high [5]=low [6]=open [7]=change [8]=change% [11]=volume
112
+ return [{ Symbol: best.symbol, Name: f[1], Price: f[2], Change: f[7], ChangePercent: f[8] + '%', Open: f[6], High: f[4], Low: f[5], Volume: f[11], MarketCap: '' }];
113
+ }
114
+ // MARKET_US: [1]=price [2]=change% [4]=change [6]=open [7]=today_low [8]=52wH [9]=52wL [10]=volume [12]=mktcap
115
+ return [{ Symbol: best.symbol.toUpperCase(), Name: f[0], Price: f[1], Change: f[4], ChangePercent: f[2] + '%', Open: f[6], High: f[8], Low: f[9], Volume: f[10], MarketCap: fmtMktCap(f[12]) }];
116
+ },
117
+ });
@@ -14,7 +14,7 @@ import { fullName, getRegistry } from './registry.js';
14
14
  import { formatRegistryHelpText } from './serialization.js';
15
15
  import { render as renderOutput } from './output.js';
16
16
  import { executeCommand } from './execution.js';
17
- import { CliError, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
17
+ import { CliError, EXIT_CODES, ERROR_ICONS, getErrorMessage, BrowserConnectError, AuthRequiredError, TimeoutError, SelectorError, EmptyResultError, ArgumentError, AdapterLoadError, CommandExecutionError, } from './errors.js';
18
18
  import { checkDaemonStatus } from './browser/discover.js';
19
19
  export function normalizeArgValue(argType, value, name) {
20
20
  if (argType !== 'bool')
@@ -28,7 +28,7 @@ export function normalizeArgValue(argType, value, name) {
28
28
  return true;
29
29
  if (normalized === 'false')
30
30
  return false;
31
- throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
31
+ throw new ArgumentError(`"${name}" must be either "true" or "false".`);
32
32
  }
33
33
  /**
34
34
  * Register a single CliCommand as a Commander subcommand.
@@ -106,10 +106,33 @@ export function registerCommandToProgram(siteCmd, cmd) {
106
106
  }
107
107
  catch (err) {
108
108
  await renderError(err, fullName(cmd), optionsRecord.verbose === true);
109
- process.exitCode = 1;
109
+ process.exitCode = resolveExitCode(err);
110
110
  }
111
111
  });
112
112
  }
113
+ // ── Exit code resolution ─────────────────────────────────────────────────────
114
+ /**
115
+ * Map any thrown value to a Unix process exit code.
116
+ *
117
+ * - CliError subclasses carry their own exitCode (set in errors.ts).
118
+ * - Generic Error objects are classified by message pattern so that
119
+ * un-typed auth / not-found errors from adapters still produce
120
+ * meaningful exit codes for shell scripts.
121
+ */
122
+ function resolveExitCode(err) {
123
+ if (err instanceof CliError)
124
+ return err.exitCode;
125
+ // Pattern-based fallback for untyped errors thrown by third-party adapters.
126
+ const msg = getErrorMessage(err);
127
+ const kind = classifyGenericError(msg);
128
+ if (kind === 'auth')
129
+ return EXIT_CODES.NOPERM;
130
+ if (kind === 'not-found')
131
+ return EXIT_CODES.EMPTY_RESULT;
132
+ if (kind === 'http')
133
+ return EXIT_CODES.GENERIC_ERROR; // HTTP 4xx/5xx → generic; renderer shows details
134
+ return EXIT_CODES.GENERIC_ERROR;
135
+ }
113
136
  // ── Error rendering ──────────────────────────────────────────────────────────
114
137
  const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
115
138
  /** Pattern-based classifier for untyped errors thrown by adapters. */
package/dist/daemon.js CHANGED
@@ -21,6 +21,7 @@
21
21
  import { createServer } from 'node:http';
22
22
  import { WebSocketServer, WebSocket } from 'ws';
23
23
  import { DEFAULT_DAEMON_PORT } from './constants.js';
24
+ import { EXIT_CODES } from './errors.js';
24
25
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
25
26
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
26
27
  // ─── State ───────────────────────────────────────────────────────────
@@ -41,7 +42,7 @@ function resetIdleTimer() {
41
42
  clearTimeout(idleTimer);
42
43
  idleTimer = setTimeout(() => {
43
44
  console.error('[daemon] Idle timeout, shutting down');
44
- process.exit(0);
45
+ process.exit(EXIT_CODES.SUCCESS);
45
46
  }, IDLE_TIMEOUT);
46
47
  }
47
48
  // ─── HTTP Server ─────────────────────────────────────────────────────
@@ -269,10 +270,10 @@ httpServer.listen(PORT, '127.0.0.1', () => {
269
270
  httpServer.on('error', (err) => {
270
271
  if (err.code === 'EADDRINUSE') {
271
272
  console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
272
- process.exit(1);
273
+ process.exit(EXIT_CODES.SERVICE_UNAVAIL);
273
274
  }
274
275
  console.error('[daemon] Server error:', err.message);
275
- process.exit(1);
276
+ process.exit(EXIT_CODES.GENERIC_ERROR);
276
277
  });
277
278
  // Graceful shutdown
278
279
  function shutdown() {
@@ -285,7 +286,7 @@ function shutdown() {
285
286
  if (extensionWs)
286
287
  extensionWs.close();
287
288
  httpServer.close();
288
- process.exit(0);
289
+ process.exit(EXIT_CODES.SUCCESS);
289
290
  }
290
291
  process.on('SIGTERM', shutdown);
291
292
  process.on('SIGINT', shutdown);
package/dist/errors.d.ts CHANGED
@@ -4,13 +4,41 @@
4
4
  * All errors thrown by the framework should extend CliError so that
5
5
  * the top-level handler in commanderAdapter.ts can render consistent,
6
6
  * helpful output with emoji-coded severity and actionable hints.
7
+ *
8
+ * ## Exit codes
9
+ *
10
+ * opencli follows Unix conventions (sysexits.h) for process exit codes:
11
+ *
12
+ * 0 Success
13
+ * 1 Generic / unexpected error
14
+ * 2 Argument / usage error (ArgumentError)
15
+ * 66 No input / empty result (EmptyResultError)
16
+ * 69 Service unavailable (BrowserConnectError, AdapterLoadError)
17
+ * 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
18
+ * 77 Permission denied / auth needed (AuthRequiredError)
19
+ * 78 Configuration error (ConfigError)
20
+ * 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
7
21
  */
22
+ export declare const EXIT_CODES: {
23
+ readonly SUCCESS: 0;
24
+ readonly GENERIC_ERROR: 1;
25
+ readonly USAGE_ERROR: 2;
26
+ readonly EMPTY_RESULT: 66;
27
+ readonly SERVICE_UNAVAIL: 69;
28
+ readonly TEMPFAIL: 75;
29
+ readonly NOPERM: 77;
30
+ readonly CONFIG_ERROR: 78;
31
+ readonly INTERRUPTED: 130;
32
+ };
33
+ export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
8
34
  export declare class CliError extends Error {
9
35
  /** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'AUTH_REQUIRED') */
10
36
  readonly code: string;
11
37
  /** Human-readable hint on how to fix the problem */
12
38
  readonly hint?: string;
13
- constructor(code: string, message: string, hint?: string);
39
+ /** Unix process exit code defaults to 1 (generic error) */
40
+ readonly exitCode: ExitCode;
41
+ constructor(code: string, message: string, hint?: string, exitCode?: ExitCode);
14
42
  }
15
43
  export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
16
44
  export declare class BrowserConnectError extends CliError {
package/dist/errors.js CHANGED
@@ -4,61 +4,99 @@
4
4
  * All errors thrown by the framework should extend CliError so that
5
5
  * the top-level handler in commanderAdapter.ts can render consistent,
6
6
  * helpful output with emoji-coded severity and actionable hints.
7
+ *
8
+ * ## Exit codes
9
+ *
10
+ * opencli follows Unix conventions (sysexits.h) for process exit codes:
11
+ *
12
+ * 0 Success
13
+ * 1 Generic / unexpected error
14
+ * 2 Argument / usage error (ArgumentError)
15
+ * 66 No input / empty result (EmptyResultError)
16
+ * 69 Service unavailable (BrowserConnectError, AdapterLoadError)
17
+ * 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
18
+ * 77 Permission denied / auth needed (AuthRequiredError)
19
+ * 78 Configuration error (ConfigError)
20
+ * 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
7
21
  */
22
+ // ── Exit code table ──────────────────────────────────────────────────────────
23
+ export const EXIT_CODES = {
24
+ SUCCESS: 0,
25
+ GENERIC_ERROR: 1,
26
+ USAGE_ERROR: 2, // Bad arguments / command misuse
27
+ EMPTY_RESULT: 66, // No data / not found (EX_NOINPUT)
28
+ SERVICE_UNAVAIL: 69, // Daemon / browser unavailable (EX_UNAVAILABLE)
29
+ TEMPFAIL: 75, // Timeout — try again later (EX_TEMPFAIL)
30
+ NOPERM: 77, // Auth required / permission (EX_NOPERM)
31
+ CONFIG_ERROR: 78, // Missing / invalid config (EX_CONFIG)
32
+ INTERRUPTED: 130, // Ctrl-C / SIGINT
33
+ };
34
+ // ── Base class ───────────────────────────────────────────────────────────────
8
35
  export class CliError extends Error {
9
36
  /** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'AUTH_REQUIRED') */
10
37
  code;
11
38
  /** Human-readable hint on how to fix the problem */
12
39
  hint;
13
- constructor(code, message, hint) {
40
+ /** Unix process exit code defaults to 1 (generic error) */
41
+ exitCode;
42
+ constructor(code, message, hint, exitCode = EXIT_CODES.GENERIC_ERROR) {
14
43
  super(message);
15
44
  this.name = new.target.name;
16
45
  this.code = code;
17
46
  this.hint = hint;
47
+ this.exitCode = exitCode;
18
48
  }
19
49
  }
20
50
  export class BrowserConnectError extends CliError {
21
51
  kind;
22
52
  constructor(message, hint, kind = 'unknown') {
23
- super('BROWSER_CONNECT', message, hint);
53
+ super('BROWSER_CONNECT', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
24
54
  this.kind = kind;
25
55
  }
26
56
  }
27
57
  export class AdapterLoadError extends CliError {
28
- constructor(message, hint) { super('ADAPTER_LOAD', message, hint); }
58
+ constructor(message, hint) {
59
+ super('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
60
+ }
29
61
  }
30
62
  export class CommandExecutionError extends CliError {
31
- constructor(message, hint) { super('COMMAND_EXEC', message, hint); }
63
+ constructor(message, hint) {
64
+ super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
65
+ }
32
66
  }
33
67
  export class ConfigError extends CliError {
34
- constructor(message, hint) { super('CONFIG', message, hint); }
68
+ constructor(message, hint) {
69
+ super('CONFIG', message, hint, EXIT_CODES.CONFIG_ERROR);
70
+ }
35
71
  }
36
72
  export class AuthRequiredError extends CliError {
37
73
  domain;
38
74
  constructor(domain, message) {
39
- super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`);
75
+ super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`, EXIT_CODES.NOPERM);
40
76
  this.domain = domain;
41
77
  }
42
78
  }
43
79
  export class TimeoutError extends CliError {
44
80
  constructor(label, seconds, hint) {
45
- super('TIMEOUT', `${label} timed out after ${seconds}s`, hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var');
81
+ super('TIMEOUT', `${label} timed out after ${seconds}s`, hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var', EXIT_CODES.TEMPFAIL);
46
82
  }
47
83
  }
48
84
  export class ArgumentError extends CliError {
49
- constructor(message, hint) { super('ARGUMENT', message, hint); }
85
+ constructor(message, hint) {
86
+ super('ARGUMENT', message, hint, EXIT_CODES.USAGE_ERROR);
87
+ }
50
88
  }
51
89
  export class EmptyResultError extends CliError {
52
90
  constructor(command, hint) {
53
- super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in');
91
+ super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in', EXIT_CODES.EMPTY_RESULT);
54
92
  }
55
93
  }
56
94
  export class SelectorError extends CliError {
57
95
  constructor(selector, hint) {
58
- super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.');
96
+ super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.', EXIT_CODES.GENERIC_ERROR);
59
97
  }
60
98
  }
61
- // ── Utilities ───────────────────────────────────────────────────────────
99
+ // ── Utilities ───────────────────────────────────────────────────────────────
62
100
  /** Extract a human-readable message from an unknown caught value. */
63
101
  export function getErrorMessage(error) {
64
102
  return error instanceof Error ? error.message : String(error);
package/dist/external.js CHANGED
@@ -6,7 +6,7 @@ import { spawnSync, execFileSync } from 'node:child_process';
6
6
  import yaml from 'js-yaml';
7
7
  import chalk from 'chalk';
8
8
  import { log } from './logger.js';
9
- import { getErrorMessage } from './errors.js';
9
+ import { EXIT_CODES, getErrorMessage } from './errors.js';
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  function getUserRegistryPath() {
12
12
  const home = os.homedir();
@@ -154,7 +154,7 @@ export function executeExternalCli(name, args, preloaded) {
154
154
  // 2. Try to auto install
155
155
  const success = installExternalCli(cli);
156
156
  if (!success) {
157
- process.exitCode = 1;
157
+ process.exitCode = EXIT_CODES.SERVICE_UNAVAIL;
158
158
  return;
159
159
  }
160
160
  }
@@ -162,7 +162,7 @@ export function executeExternalCli(name, args, preloaded) {
162
162
  const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
163
163
  if (result.error) {
164
164
  console.error(chalk.red(`Failed to execute '${cli.binary}': ${result.error.message}`));
165
- process.exitCode = 1;
165
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
166
166
  return;
167
167
  }
168
168
  if (result.status !== null) {
package/dist/main.js CHANGED
@@ -21,6 +21,7 @@ import { runCli } from './cli.js';
21
21
  import { emitHook } from './hooks.js';
22
22
  import { installNodeNetwork } from './node-network.js';
23
23
  import { registerUpdateNoticeOnExit, checkForUpdateBackground } from './update-check.js';
24
+ import { EXIT_CODES } from './errors.js';
24
25
  installNodeNetwork();
25
26
  const __filename = fileURLToPath(import.meta.url);
26
27
  const __dirname = path.dirname(__filename);
@@ -53,7 +54,7 @@ if (getCompIdx !== -1) {
53
54
  cursor = words.length;
54
55
  const candidates = getCompletions(words, cursor);
55
56
  process.stdout.write(candidates.join('\n') + '\n');
56
- process.exit(0);
57
+ process.exit(EXIT_CODES.SUCCESS);
57
58
  }
58
59
  await emitHook('onStartup', { command: '__startup__', args: {} });
59
60
  runCli(BUILTIN_CLIS, USER_CLIS);