@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.
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +3 -3
- package/README.md +213 -18
- package/dist/build-manifest.d.ts +2 -3
- package/dist/build-manifest.js +75 -170
- package/dist/build-manifest.test.js +113 -88
- package/dist/cli-manifest.json +1253 -1105
- package/dist/cli.js +14 -14
- package/dist/clis/antigravity/serve.js +2 -2
- package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
- package/dist/clis/sinafinance/rolling-news.js +40 -0
- package/dist/clis/sinafinance/stock.d.ts +8 -0
- package/dist/clis/sinafinance/stock.js +117 -0
- package/dist/commanderAdapter.js +26 -3
- package/dist/daemon.js +19 -7
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +49 -11
- package/dist/external-clis.yaml +16 -0
- package/dist/external.js +3 -3
- package/dist/main.js +2 -1
- package/dist/serialization.js +6 -1
- package/dist/serialization.test.d.ts +1 -0
- package/dist/serialization.test.js +23 -0
- package/dist/tui.js +2 -1
- package/docs/adapters/browser/sinafinance.md +56 -6
- package/extension/dist/background.js +12 -6
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +21 -6
- package/extension/src/protocol.ts +2 -1
- package/package.json +1 -1
- package/src/build-manifest.test.ts +117 -88
- package/src/build-manifest.ts +81 -180
- package/src/cli.ts +14 -14
- package/src/clis/antigravity/serve.ts +2 -2
- package/src/clis/sinafinance/rolling-news.ts +42 -0
- package/src/clis/sinafinance/stock.ts +127 -0
- package/src/commanderAdapter.ts +25 -2
- package/src/daemon.ts +21 -8
- package/src/errors.ts +71 -10
- package/src/external-clis.yaml +16 -0
- package/src/external.ts +3 -3
- package/src/main.ts +2 -1
- package/src/serialization.test.ts +26 -0
- package/src/serialization.ts +6 -1
- package/src/tui.ts +2 -1
package/src/commanderAdapter.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
307
|
+
process.exit(EXIT_CODES.SERVICE_UNAVAIL);
|
|
295
308
|
}
|
|
296
309
|
console.error('[daemon] Server error:', err.message);
|
|
297
|
-
process.exit(
|
|
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(
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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(
|
|
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) {
|
|
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(
|
|
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(
|
|
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 {
|
package/src/external-clis.yaml
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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(
|
|
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
|
+
});
|
package/src/serialization.ts
CHANGED
|
@@ -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
|
|
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(
|
|
165
|
+
process.exit(EXIT_CODES.INTERRUPTED);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
|