@jackwener/opencli 1.5.3 → 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.
Files changed (46) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +3 -3
  2. package/README.md +213 -18
  3. package/dist/build-manifest.d.ts +2 -3
  4. package/dist/build-manifest.js +75 -170
  5. package/dist/build-manifest.test.js +113 -88
  6. package/dist/cli-manifest.json +1253 -1105
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  10. package/dist/clis/sinafinance/rolling-news.js +40 -0
  11. package/dist/clis/sinafinance/stock.d.ts +8 -0
  12. package/dist/clis/sinafinance/stock.js +117 -0
  13. package/dist/commanderAdapter.js +26 -3
  14. package/dist/daemon.js +19 -7
  15. package/dist/errors.d.ts +29 -1
  16. package/dist/errors.js +49 -11
  17. package/dist/external-clis.yaml +16 -0
  18. package/dist/external.js +3 -3
  19. package/dist/main.js +2 -1
  20. package/dist/serialization.js +6 -1
  21. package/dist/serialization.test.d.ts +1 -0
  22. package/dist/serialization.test.js +23 -0
  23. package/dist/tui.js +2 -1
  24. package/docs/adapters/browser/sinafinance.md +56 -6
  25. package/extension/dist/background.js +12 -6
  26. package/extension/manifest.json +2 -2
  27. package/extension/package-lock.json +2 -2
  28. package/extension/package.json +1 -1
  29. package/extension/src/background.ts +21 -6
  30. package/extension/src/protocol.ts +2 -1
  31. package/package.json +1 -1
  32. package/src/build-manifest.test.ts +117 -88
  33. package/src/build-manifest.ts +81 -180
  34. package/src/cli.ts +14 -14
  35. package/src/clis/antigravity/serve.ts +2 -2
  36. package/src/clis/sinafinance/rolling-news.ts +42 -0
  37. package/src/clis/sinafinance/stock.ts +127 -0
  38. package/src/commanderAdapter.ts +25 -2
  39. package/src/daemon.ts +21 -8
  40. package/src/errors.ts +71 -10
  41. package/src/external-clis.yaml +16 -0
  42. package/src/external.ts +3 -3
  43. package/src/main.ts +2 -1
  44. package/src/serialization.test.ts +26 -0
  45. package/src/serialization.ts +6 -1
  46. package/src/tui.ts +2 -1
@@ -18,6 +18,7 @@ import { render as renderOutput } from './output.js';
18
18
  import { executeCommand } from './execution.js';
19
19
  import {
20
20
  CliError,
21
+ EXIT_CODES,
21
22
  ERROR_ICONS,
22
23
  getErrorMessage,
23
24
  BrowserConnectError,
@@ -40,7 +41,7 @@ export function normalizeArgValue(argType: string | undefined, value: unknown, n
40
41
  if (normalized === 'true') return true;
41
42
  if (normalized === 'false') return false;
42
43
 
43
- throw new CliError('ARGUMENT', `"${name}" must be either "true" or "false".`);
44
+ throw new ArgumentError(`"${name}" must be either "true" or "false".`);
44
45
  }
45
46
 
46
47
  /**
@@ -117,11 +118,33 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
117
118
  });
118
119
  } catch (err) {
119
120
  await renderError(err, fullName(cmd), optionsRecord.verbose === true);
120
- process.exitCode = 1;
121
+ process.exitCode = resolveExitCode(err);
121
122
  }
122
123
  });
123
124
  }
124
125
 
126
+ // ── Exit code resolution ─────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Map any thrown value to a Unix process exit code.
130
+ *
131
+ * - CliError subclasses carry their own exitCode (set in errors.ts).
132
+ * - Generic Error objects are classified by message pattern so that
133
+ * un-typed auth / not-found errors from adapters still produce
134
+ * meaningful exit codes for shell scripts.
135
+ */
136
+ function resolveExitCode(err: unknown): number {
137
+ if (err instanceof CliError) return err.exitCode;
138
+
139
+ // Pattern-based fallback for untyped errors thrown by third-party adapters.
140
+ const msg = getErrorMessage(err);
141
+ const kind = classifyGenericError(msg);
142
+ if (kind === 'auth') return EXIT_CODES.NOPERM;
143
+ if (kind === 'not-found') return EXIT_CODES.EMPTY_RESULT;
144
+ if (kind === 'http') return EXIT_CODES.GENERIC_ERROR; // HTTP 4xx/5xx → generic; renderer shows details
145
+ return EXIT_CODES.GENERIC_ERROR;
146
+ }
147
+
125
148
  // ── Error rendering ──────────────────────────────────────────────────────────
126
149
 
127
150
  const ISSUES_URL = 'https://github.com/jackwener/opencli/issues';
package/src/daemon.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
23
23
  import { WebSocketServer, WebSocket, type RawData } from 'ws';
24
24
  import { DEFAULT_DAEMON_PORT } from './constants.js';
25
+ import { EXIT_CODES } from './errors.js';
25
26
 
26
27
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
27
28
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
@@ -53,7 +54,7 @@ function resetIdleTimer(): void {
53
54
  if (idleTimer) clearTimeout(idleTimer);
54
55
  idleTimer = setTimeout(() => {
55
56
  console.error('[daemon] Idle timeout, shutting down');
56
- process.exit(0);
57
+ process.exit(EXIT_CODES.SUCCESS);
57
58
  }, IDLE_TIMEOUT);
58
59
  }
59
60
 
@@ -102,7 +103,22 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
102
103
  return;
103
104
  }
104
105
 
105
- // Require custom header on all HTTP requests. Browsers cannot attach
106
+ const url = req.url ?? '/';
107
+ const pathname = url.split('?')[0];
108
+
109
+ // Health-check endpoint — no X-OpenCLI header required.
110
+ // Used by the extension to silently probe daemon reachability before
111
+ // attempting a WebSocket connection (avoids uncatchable ERR_CONNECTION_REFUSED).
112
+ // Security note: this endpoint is reachable by any client that passes the
113
+ // origin check above (chrome-extension:// or no Origin header, e.g. curl).
114
+ // Timing side-channels can reveal daemon presence to local processes, which
115
+ // is an accepted risk given the daemon is loopback-only and short-lived.
116
+ if (req.method === 'GET' && pathname === '/ping') {
117
+ jsonResponse(res, 200, { ok: true });
118
+ return;
119
+ }
120
+
121
+ // Require custom header on all other HTTP requests. Browsers cannot attach
106
122
  // custom headers in "simple" requests, and our preflight returns no
107
123
  // Access-Control-Allow-Headers, so scripted fetch() from web pages is
108
124
  // blocked even if Origin check is somehow bypassed.
@@ -111,9 +127,6 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
111
127
  return;
112
128
  }
113
129
 
114
- const url = req.url ?? '/';
115
- const pathname = url.split('?')[0];
116
-
117
130
  if (req.method === 'GET' && pathname === '/status') {
118
131
  jsonResponse(res, 200, {
119
132
  ok: true,
@@ -291,10 +304,10 @@ httpServer.listen(PORT, '127.0.0.1', () => {
291
304
  httpServer.on('error', (err: NodeJS.ErrnoException) => {
292
305
  if (err.code === 'EADDRINUSE') {
293
306
  console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
294
- process.exit(1);
307
+ process.exit(EXIT_CODES.SERVICE_UNAVAIL);
295
308
  }
296
309
  console.error('[daemon] Server error:', err.message);
297
- process.exit(1);
310
+ process.exit(EXIT_CODES.GENERIC_ERROR);
298
311
  });
299
312
 
300
313
  // Graceful shutdown
@@ -307,7 +320,7 @@ function shutdown(): void {
307
320
  pending.clear();
308
321
  if (extensionWs) extensionWs.close();
309
322
  httpServer.close();
310
- process.exit(0);
323
+ process.exit(EXIT_CODES.SUCCESS);
311
324
  }
312
325
 
313
326
  process.on('SIGTERM', shutdown);
package/src/errors.ts CHANGED
@@ -4,48 +4,96 @@
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
  */
8
22
 
23
+ // ── Exit code table ──────────────────────────────────────────────────────────
24
+
25
+ export const EXIT_CODES = {
26
+ SUCCESS: 0,
27
+ GENERIC_ERROR: 1,
28
+ USAGE_ERROR: 2, // Bad arguments / command misuse
29
+ EMPTY_RESULT: 66, // No data / not found (EX_NOINPUT)
30
+ SERVICE_UNAVAIL:69, // Daemon / browser unavailable (EX_UNAVAILABLE)
31
+ TEMPFAIL: 75, // Timeout — try again later (EX_TEMPFAIL)
32
+ NOPERM: 77, // Auth required / permission (EX_NOPERM)
33
+ CONFIG_ERROR: 78, // Missing / invalid config (EX_CONFIG)
34
+ INTERRUPTED: 130, // Ctrl-C / SIGINT
35
+ } as const;
36
+
37
+ export type ExitCode = typeof EXIT_CODES[keyof typeof EXIT_CODES];
38
+
39
+ // ── Base class ───────────────────────────────────────────────────────────────
40
+
9
41
  export class CliError extends Error {
10
42
  /** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'AUTH_REQUIRED') */
11
43
  readonly code: string;
12
44
  /** Human-readable hint on how to fix the problem */
13
45
  readonly hint?: string;
46
+ /** Unix process exit code — defaults to 1 (generic error) */
47
+ readonly exitCode: ExitCode;
14
48
 
15
- constructor(code: string, message: string, hint?: string) {
49
+ constructor(code: string, message: string, hint?: string, exitCode: ExitCode = EXIT_CODES.GENERIC_ERROR) {
16
50
  super(message);
17
51
  this.name = new.target.name;
18
52
  this.code = code;
19
53
  this.hint = hint;
54
+ this.exitCode = exitCode;
20
55
  }
21
56
  }
22
57
 
58
+ // ── Typed subclasses ─────────────────────────────────────────────────────────
59
+
23
60
  export type BrowserConnectKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
24
61
 
25
62
  export class BrowserConnectError extends CliError {
26
63
  readonly kind: BrowserConnectKind;
27
64
  constructor(message: string, hint?: string, kind: BrowserConnectKind = 'unknown') {
28
- super('BROWSER_CONNECT', message, hint);
65
+ super('BROWSER_CONNECT', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
29
66
  this.kind = kind;
30
67
  }
31
68
  }
32
69
 
33
70
  export class AdapterLoadError extends CliError {
34
- constructor(message: string, hint?: string) { super('ADAPTER_LOAD', message, hint); }
71
+ constructor(message: string, hint?: string) {
72
+ super('ADAPTER_LOAD', message, hint, EXIT_CODES.SERVICE_UNAVAIL);
73
+ }
35
74
  }
36
75
 
37
76
  export class CommandExecutionError extends CliError {
38
- constructor(message: string, hint?: string) { super('COMMAND_EXEC', message, hint); }
77
+ constructor(message: string, hint?: string) {
78
+ super('COMMAND_EXEC', message, hint, EXIT_CODES.GENERIC_ERROR);
79
+ }
39
80
  }
40
81
 
41
82
  export class ConfigError extends CliError {
42
- constructor(message: string, hint?: string) { super('CONFIG', message, hint); }
83
+ constructor(message: string, hint?: string) {
84
+ super('CONFIG', message, hint, EXIT_CODES.CONFIG_ERROR);
85
+ }
43
86
  }
44
87
 
45
88
  export class AuthRequiredError extends CliError {
46
89
  readonly domain: string;
47
90
  constructor(domain: string, message?: string) {
48
- super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`);
91
+ super(
92
+ 'AUTH_REQUIRED',
93
+ message ?? `Not logged in to ${domain}`,
94
+ `Please open Chrome and log in to https://${domain}`,
95
+ EXIT_CODES.NOPERM,
96
+ );
49
97
  this.domain = domain;
50
98
  }
51
99
  }
@@ -56,27 +104,40 @@ export class TimeoutError extends CliError {
56
104
  'TIMEOUT',
57
105
  `${label} timed out after ${seconds}s`,
58
106
  hint ?? 'Try again, or increase timeout with OPENCLI_BROWSER_COMMAND_TIMEOUT env var',
107
+ EXIT_CODES.TEMPFAIL,
59
108
  );
60
109
  }
61
110
  }
62
111
 
63
112
  export class ArgumentError extends CliError {
64
- constructor(message: string, hint?: string) { super('ARGUMENT', message, hint); }
113
+ constructor(message: string, hint?: string) {
114
+ super('ARGUMENT', message, hint, EXIT_CODES.USAGE_ERROR);
115
+ }
65
116
  }
66
117
 
67
118
  export class EmptyResultError extends CliError {
68
119
  constructor(command: string, hint?: string) {
69
- super('EMPTY_RESULT', `${command} returned no data`, hint ?? 'The page structure may have changed, or you may need to log in');
120
+ super(
121
+ 'EMPTY_RESULT',
122
+ `${command} returned no data`,
123
+ hint ?? 'The page structure may have changed, or you may need to log in',
124
+ EXIT_CODES.EMPTY_RESULT,
125
+ );
70
126
  }
71
127
  }
72
128
 
73
129
  export class SelectorError extends CliError {
74
130
  constructor(selector: string, hint?: string) {
75
- super('SELECTOR', `Could not find element: ${selector}`, hint ?? 'The page UI may have changed. Please report this issue.');
131
+ super(
132
+ 'SELECTOR',
133
+ `Could not find element: ${selector}`,
134
+ hint ?? 'The page UI may have changed. Please report this issue.',
135
+ EXIT_CODES.GENERIC_ERROR,
136
+ );
76
137
  }
77
138
  }
78
139
 
79
- // ── Utilities ───────────────────────────────────────────────────────────
140
+ // ── Utilities ───────────────────────────────────────────────────────────────
80
141
 
81
142
  /** Extract a human-readable message from an unknown caught value. */
82
143
  export function getErrorMessage(error: unknown): string {
@@ -21,3 +21,19 @@
21
21
  tags: [docker, containers, devops]
22
22
  install:
23
23
  mac: "brew install --cask docker"
24
+
25
+ - name: lark-cli
26
+ binary: lark-cli
27
+ description: "Lark/Feishu CLI — messages, documents, spreadsheets, calendar, tasks and 200+ commands for AI agents"
28
+ homepage: "https://github.com/larksuite/cli"
29
+ tags: [lark, feishu, collaboration, productivity, ai-agent]
30
+ install:
31
+ default: "npm install -g @larksuite/cli"
32
+
33
+ - name: vercel
34
+ binary: vercel
35
+ description: "Vercel CLI — deploy projects, manage domains, env vars, logs and serverless functions"
36
+ homepage: "https://vercel.com/docs/cli"
37
+ tags: [vercel, deployment, serverless, frontend, devops]
38
+ install:
39
+ default: "npm install -g vercel"
package/src/external.ts 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
 
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
 
@@ -180,7 +180,7 @@ export function executeExternalCli(name: string, args: string[], preloaded?: Ext
180
180
  // 2. Try to auto install
181
181
  const success = installExternalCli(cli);
182
182
  if (!success) {
183
- process.exitCode = 1;
183
+ process.exitCode = EXIT_CODES.SERVICE_UNAVAIL;
184
184
  return;
185
185
  }
186
186
  }
@@ -189,7 +189,7 @@ export function executeExternalCli(name: string, args: string[], preloaded?: Ext
189
189
  const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
190
190
  if (result.error) {
191
191
  console.error(chalk.red(`Failed to execute '${cli.binary}': ${result.error.message}`));
192
- process.exitCode = 1;
192
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
193
193
  return;
194
194
  }
195
195
 
package/src/main.ts CHANGED
@@ -22,6 +22,7 @@ import { runCli } from './cli.js';
22
22
  import { emitHook } from './hooks.js';
23
23
  import { installNodeNetwork } from './node-network.js';
24
24
  import { registerUpdateNoticeOnExit, checkForUpdateBackground } from './update-check.js';
25
+ import { EXIT_CODES } from './errors.js';
25
26
 
26
27
  installNodeNetwork();
27
28
 
@@ -57,7 +58,7 @@ if (getCompIdx !== -1) {
57
58
  if (cursor === undefined) cursor = words.length;
58
59
  const candidates = getCompletions(words, cursor);
59
60
  process.stdout.write(candidates.join('\n') + '\n');
60
- process.exit(0);
61
+ process.exit(EXIT_CODES.SUCCESS);
61
62
  }
62
63
 
63
64
  await emitHook('onStartup', { command: '__startup__', args: {} });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { CliCommand } from './registry.js';
3
+ import { Strategy } from './registry.js';
4
+ import { formatRegistryHelpText } from './serialization.js';
5
+
6
+ describe('formatRegistryHelpText', () => {
7
+ it('summarizes long choices lists so help text stays readable', () => {
8
+ const cmd: CliCommand = {
9
+ site: 'demo',
10
+ name: 'dynamic',
11
+ description: 'Demo command',
12
+ strategy: Strategy.PUBLIC,
13
+ browser: false,
14
+ args: [
15
+ {
16
+ name: 'field',
17
+ help: 'Field to use',
18
+ choices: ['all-fields', 'topic', 'title', 'author', 'publication-titles', 'year-published', 'doi'],
19
+ },
20
+ ],
21
+ columns: ['field'],
22
+ };
23
+
24
+ expect(formatRegistryHelpText(cmd)).toContain('--field: all-fields, topic, title, author, ... (+3 more)');
25
+ });
26
+ });
@@ -62,6 +62,11 @@ export function formatArgSummary(args: Arg[]): string {
62
62
  .join(' ');
63
63
  }
64
64
 
65
+ function summarizeChoices(choices: string[]): string {
66
+ if (choices.length <= 4) return choices.join(', ');
67
+ return `${choices.slice(0, 4).join(', ')}, ... (+${choices.length - 4} more)`;
68
+ }
69
+
65
70
  /** Generate the --help appendix showing registry metadata not exposed by Commander. */
66
71
  export function formatRegistryHelpText(cmd: CliCommand): string {
67
72
  const lines: string[] = [];
@@ -69,7 +74,7 @@ export function formatRegistryHelpText(cmd: CliCommand): string {
69
74
  for (const a of choicesArgs) {
70
75
  const prefix = a.positional ? `<${a.name}>` : `--${a.name}`;
71
76
  const def = a.default != null ? ` (default: ${a.default})` : '';
72
- lines.push(` ${prefix}: ${a.choices!.join(', ')}${def}`);
77
+ lines.push(` ${prefix}: ${summarizeChoices(a.choices!)}${def}`);
73
78
  }
74
79
  const meta: string[] = [];
75
80
  meta.push(`Strategy: ${strategyLabel(cmd)}`);
package/src/tui.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Uses raw stdin mode + ANSI escape codes for interactive prompts.
5
5
  */
6
6
  import chalk from 'chalk';
7
+ import { EXIT_CODES } from './errors.js';
7
8
 
8
9
  export interface CheckboxItem {
9
10
  label: string;
@@ -161,7 +162,7 @@ export async function checkboxPrompt(
161
162
  // Ctrl+C — exit process
162
163
  if (key === '\x03') {
163
164
  cleanup();
164
- process.exit(130);
165
+ process.exit(EXIT_CODES.INTERRUPTED);
165
166
  }
166
167
  }
167
168