Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1c5f889
feat: 推特新增回复图片能力支持本地路径和网络路径
YoungCan-Wang Apr 4, 2026
14c9d27
Merge pull request #1 from YoungCan-Wang/feat/twitter-reply-image
YoungCan-Wang Apr 4, 2026
ac38ec2
fix(twitter/reply): fix image upload fallback, restore execCommand, a…
jackwener Apr 4, 2026
73b0ef8
fix(twitter/reply): add local image size check and base64 fallback wa…
jackwener Apr 4, 2026
20a9678
Merge branch 'jackwener:main' into main
YoungCan-Wang Apr 4, 2026
3d76d6f
Merge branch 'jackwener:main' into main
YoungCan-Wang Apr 5, 2026
2165d04
feat: 解决实际痛点(重型 SPA 超时),改动集中且向后兼容
Apr 5, 2026
23de071
feat: ERROR_ICONS 里已经注册了 NETWORK、API_ERROR、PAGE_CHANGED 三个图标,但没有对应的 E…
Apr 5, 2026
300efbd
feat: 接口承诺返回 any(调用者可直接访问属性),实现却声明 unknown(需要类型断言)。对齐后消除了这个隐式分歧。 共享 e…
Apr 5, 2026
0d1da98
ci: re-trigger E2E
Apr 5, 2026
8ced30d
Merge branch 'jackwener:main' into main
YoungCan-Wang Apr 5, 2026
900d458
Merge branch 'jackwener:main' into main
YoungCan-Wang Apr 6, 2026
84da3d9
Revert "feat: 接口承诺返回 any(调用者可直接访问属性),实现却声明 unknown(需要类型断言)。对齐后消除了这个隐式…
Apr 6, 2026
b409474
feat: 增加raw mode,解决cli网页时agent会猜变量名的问题
Apr 7, 2026
b1e0461
Merge branch 'main' of https://github.com/YoungCan-Wang/opencli
Apr 7, 2026
d277bce
Merge branch 'feat/raw-output'
Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export interface ManifestEntry {
/** Relative path to the original source file from clis/ dir (for YAML: 'site/cmd.yaml') */
sourceFile?: string;
/** Pre-navigation control — see CliCommand.navigateBefore */
navigateBefore?: boolean | string;
navigateBefore?: boolean | string | { url: string; waitUntil?: 'load' | 'none'; settleMs?: number };
}

import { type YamlCliDefinition, parseYamlArgs } from './yaml-schema.js';
Expand Down
10 changes: 9 additions & 1 deletion src/commanderAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
}
subCmd
.option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
.option('-v, --verbose', 'Debug output', false);
.option('-v, --verbose', 'Debug output', false)
.option('--raw', 'Output raw unformatted result as JSON (for adapter development)', false);

subCmd.addHelpText('after', formatRegistryHelpText(cmd));

Expand All @@ -97,6 +98,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
cmd.validateArgs?.(kwargs);

const verbose = optionsRecord.verbose === true;
const raw = optionsRecord.raw === true;
let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
const formatExplicit = subCmd.getOptionValueSource('format') === 'cli';
if (verbose) process.env.OPENCLI_VERBOSE = '1';
Expand All @@ -111,6 +113,12 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
return;
}

// --raw: output raw unformatted result as JSON, skip all formatting
if (raw) {
console.log(JSON.stringify(result, null, 2));
return;
}

const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
if (!formatExplicit && format === 'table' && resolved.defaultFormat) {
format = resolved.defaultFormat;
Expand Down
43 changes: 40 additions & 3 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
* opencli follows Unix conventions (sysexits.h) for process exit codes:
*
* 0 Success
* 1 Generic / unexpected error
* 1 Generic / unexpected error (ApiError, SelectorError)
* 2 Argument / usage error (ArgumentError)
* 66 No input / empty result (EmptyResultError)
* 66 No input / empty result (EmptyResultError, PageChangedError)
* 69 Service unavailable (BrowserConnectError, AdapterLoadError)
* 75 Temporary failure, retry later (TimeoutError) EX_TEMPFAIL
* 75 Temporary failure, retry later (TimeoutError, NetworkError) EX_TEMPFAIL
* 77 Permission denied / auth needed (AuthRequiredError)
* 78 Configuration error (ConfigError)
* 130 Interrupted by Ctrl-C (set by tui.ts SIGINT handler)
Expand Down Expand Up @@ -137,6 +137,43 @@ export class SelectorError extends CliError {
}
}

export class NetworkError extends CliError {
readonly statusCode?: number;
constructor(message: string, statusCode?: number, hint?: string) {
super(
'NETWORK',
message,
hint ?? 'Check your network connection, or try again later',
EXIT_CODES.TEMPFAIL,
);
this.statusCode = statusCode;
}
}

export class ApiError extends CliError {
readonly apiCode?: number | string;
constructor(label: string, apiCode?: number | string, apiMessage?: string, hint?: string) {
super(
'API_ERROR',
`${label}: API error${apiCode !== undefined ? ` code=${apiCode}` : ''}${apiMessage ? ` — ${apiMessage}` : ''}`,
hint ?? 'The API returned an unexpected error. The endpoint may have changed.',
EXIT_CODES.GENERIC_ERROR,
);
this.apiCode = apiCode;
}
}

export class PageChangedError extends CliError {
constructor(command: string, hint?: string) {
super(
'PAGE_CHANGED',
`${command}: page structure has changed`,
hint ?? 'The website may have been updated. Please report this issue so the adapter can be fixed.',
EXIT_CODES.EMPTY_RESULT,
);
}
}

// ── Utilities ───────────────────────────────────────────────────────────────

/** Extract a human-readable message from an unknown caught value. */
Expand Down
31 changes: 23 additions & 8 deletions src/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
*/

import { type CliCommand, type InternalCliCommand, type Arg, type CommandArgs, Strategy, getRegistry, fullName } from './registry.js';
import { type CliCommand, type InternalCliCommand, type PreNavOptions, type Arg, type CommandArgs, Strategy, getRegistry, fullName } from './registry.js';
import type { IPage } from './types.js';
import { pathToFileURL } from 'node:url';
import { executePipeline } from './pipeline/index.js';
Expand Down Expand Up @@ -109,12 +109,24 @@ async function runCommand(
);
}

function resolvePreNav(cmd: CliCommand): string | null {
interface ResolvedPreNav {
url: string;
waitUntil?: 'load' | 'none';
settleMs?: number;
}

function resolvePreNav(cmd: CliCommand): ResolvedPreNav | null {
if (cmd.navigateBefore === false) return null;
if (typeof cmd.navigateBefore === 'string') return cmd.navigateBefore;

if (typeof cmd.navigateBefore === 'object' && cmd.navigateBefore !== null && 'url' in cmd.navigateBefore) {
const opts = cmd.navigateBefore as PreNavOptions;
return { url: opts.url, waitUntil: opts.waitUntil, settleMs: opts.settleMs };
}

if (typeof cmd.navigateBefore === 'string') return { url: cmd.navigateBefore };

if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
return `https://${cmd.domain}`;
return { url: `https://${cmd.domain}` };
}
return null;
}
Expand Down Expand Up @@ -193,17 +205,20 @@ export async function executeCommand(
ensureRequiredEnv(cmd);
const BrowserFactory = getBrowserFactory(cmd.site);
result = await browserSession(BrowserFactory, async (page) => {
const preNavUrl = resolvePreNav(cmd);
if (preNavUrl) {
const preNav = resolvePreNav(cmd);
if (preNav) {
// Navigate directly — the extension's handleNavigate already has a fast-path
// that skips navigation if the tab is already at the target URL.
// This avoids an extra exec round-trip (getCurrentUrl) on first command and
// lets the extension create the automation window with the target URL directly
// instead of about:blank.
const gotoOpts = (preNav.waitUntil || preNav.settleMs)
? { waitUntil: preNav.waitUntil, settleMs: preNav.settleMs }
: undefined;
try {
await page.goto(preNavUrl);
await page.goto(preNav.url, gotoOpts);
} catch (err) {
if (debug) log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
if (debug) log.debug(`[pre-nav] Failed to navigate to ${preNav.url}: ${err instanceof Error ? err.message : err}`);
}
}
try {
Expand Down
5 changes: 5 additions & 0 deletions src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ function resolveColumns(rows: Record<string, unknown>[], opts: RenderOptions): s
}

export function render(data: unknown, opts: RenderOptions = {}): void {
// RAW mode: bypass all formatting, output raw JSON (for adapter development)
if (process.env.RAW === '1' || process.env.OPENCLI_RAW === '1') {
console.log(JSON.stringify(data, null, 2));
return;
}
let fmt = opts.fmt ?? 'table';
// Non-TTY auto-downgrade only when format was NOT explicitly passed by user.
// Priority: explicit -f (any value) > OUTPUT env var > TTY auto-detect > table
Expand Down
3 changes: 3 additions & 0 deletions src/pipeline/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ function debugStepResult(op: string, data: unknown): void {
log.stepResult('(no data)');
} else if (Array.isArray(data)) {
log.stepResult(`${data.length} items`);
if (data.length > 0) {
log.verbose(` [Sample] ${JSON.stringify(data[0]).slice(0, 300)}`);
}
} else if (typeof data === 'object') {
const keys = Object.keys(data).slice(0, 5);
log.stepResult(`dict (${keys.join(', ')}${Object.keys(data).length > 5 ? '...' : ''})`);
Expand Down
19 changes: 18 additions & 1 deletion src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ export interface RequiredEnv {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- kwargs from CLI parsing are inherently untyped
export type CommandArgs = Record<string, any>;

/**
* Options for navigateBefore when fine-grained control is needed.
*
* Useful for heavy SPAs where the default `waitUntil: 'load'` + 1s settle
* causes timeouts, or when a cookie-seeding navigation to a different
* subdomain is required before loading the actual target page.
*/
export interface PreNavOptions {
/** Primary URL to navigate to. */
url: string;
/** Override waitUntil behavior ('load' or 'none'). Default: 'load'. */
waitUntil?: 'load' | 'none';
/** Milliseconds to wait for DOM stability after load. Default: 1000. */
settleMs?: number;
}

export interface CliCommand {
site: string;
name: string;
Expand Down Expand Up @@ -62,8 +78,9 @@ export interface CliCommand {
* - `undefined` / `true`: navigate to `https://${domain}` (default)
* - `false`: skip — adapter handles its own navigation (e.g. boss common.ts)
* - `string`: navigate to this specific URL instead of the domain root
* - `object`: navigate with explicit options (waitUntil, settleMs)
*/
navigateBefore?: boolean | string;
navigateBefore?: boolean | string | PreNavOptions;
/** Override the default CLI output format when the user does not pass -f/--format. */
defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
}
Expand Down
Loading