@jackwener/opencli 0.7.5 → 0.7.8
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 +249 -0
- package/.agents/workflows/cross-project-adapter-migration.md +54 -0
- package/dist/browser/discover.d.ts +8 -0
- package/dist/browser/discover.js +83 -0
- package/dist/browser/errors.d.ts +21 -0
- package/dist/browser/errors.js +54 -0
- package/dist/browser/index.d.ts +22 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/mcp.d.ts +33 -0
- package/dist/browser/mcp.js +304 -0
- package/dist/browser/page.d.ts +41 -0
- package/dist/browser/page.js +142 -0
- package/dist/browser/tabs.d.ts +13 -0
- package/dist/browser/tabs.js +70 -0
- package/dist/browser.test.js +1 -1
- package/dist/completion.d.ts +21 -0
- package/dist/completion.js +116 -0
- package/dist/doctor.js +7 -7
- package/dist/engine.js +6 -4
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +42 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.js +47 -0
- package/dist/main.js +37 -2
- package/dist/pipeline/executor.js +8 -8
- package/dist/pipeline/steps/browser.d.ts +7 -7
- package/dist/pipeline/steps/intercept.d.ts +1 -1
- package/dist/pipeline/steps/tap.d.ts +1 -1
- package/dist/setup.js +1 -1
- package/package.json +4 -3
- package/scripts/clean-yaml.cjs +19 -0
- package/scripts/copy-yaml.cjs +21 -0
- package/scripts/postinstall.js +200 -0
- package/src/bilibili.ts +1 -1
- package/src/browser/discover.ts +90 -0
- package/src/browser/errors.ts +89 -0
- package/src/browser/index.ts +26 -0
- package/src/browser/mcp.ts +305 -0
- package/src/browser/page.ts +152 -0
- package/src/browser/tabs.ts +76 -0
- package/src/browser.test.ts +1 -1
- package/src/completion.ts +129 -0
- package/src/doctor.ts +13 -1
- package/src/engine.ts +9 -4
- package/src/errors.ts +48 -0
- package/src/logger.ts +57 -0
- package/src/main.ts +39 -3
- package/src/pipeline/executor.ts +8 -7
- package/src/pipeline/steps/browser.ts +18 -18
- package/src/pipeline/steps/intercept.ts +8 -8
- package/src/pipeline/steps/tap.ts +2 -2
- package/src/setup.ts +1 -1
- package/tsconfig.json +1 -2
- package/dist/browser.d.ts +0 -105
- package/dist/browser.js +0 -644
- package/src/browser.ts +0 -698
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell tab-completion support for opencli.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Shell script generators for bash, zsh, and fish
|
|
6
|
+
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
|
+
*/
|
|
8
|
+
import { getRegistry } from './registry.js';
|
|
9
|
+
import { CliError } from './errors.js';
|
|
10
|
+
// ── Dynamic completion logic ───────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Built-in (non-dynamic) top-level commands.
|
|
13
|
+
*/
|
|
14
|
+
const BUILTIN_COMMANDS = [
|
|
15
|
+
'list',
|
|
16
|
+
'validate',
|
|
17
|
+
'verify',
|
|
18
|
+
'explore',
|
|
19
|
+
'probe', // alias for explore
|
|
20
|
+
'synthesize',
|
|
21
|
+
'generate',
|
|
22
|
+
'cascade',
|
|
23
|
+
'doctor',
|
|
24
|
+
'setup',
|
|
25
|
+
'completion',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Return completion candidates given the current command-line words and cursor index.
|
|
29
|
+
*
|
|
30
|
+
* @param words - The argv after 'opencli' (words[0] is the first arg, e.g. site name)
|
|
31
|
+
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
32
|
+
*/
|
|
33
|
+
export function getCompletions(words, cursor) {
|
|
34
|
+
// cursor === 1 → completing the first argument (site name or built-in command)
|
|
35
|
+
if (cursor <= 1) {
|
|
36
|
+
const sites = new Set();
|
|
37
|
+
for (const [, cmd] of getRegistry()) {
|
|
38
|
+
sites.add(cmd.site);
|
|
39
|
+
}
|
|
40
|
+
return [...BUILTIN_COMMANDS, ...sites].sort();
|
|
41
|
+
}
|
|
42
|
+
const site = words[0];
|
|
43
|
+
// If the first word is a built-in command, no further completion
|
|
44
|
+
if (BUILTIN_COMMANDS.includes(site)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
// cursor === 2 → completing the sub-command name under a site
|
|
48
|
+
if (cursor === 2) {
|
|
49
|
+
const subcommands = [];
|
|
50
|
+
for (const [, cmd] of getRegistry()) {
|
|
51
|
+
if (cmd.site === site) {
|
|
52
|
+
subcommands.push(cmd.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return subcommands.sort();
|
|
56
|
+
}
|
|
57
|
+
// cursor >= 3 → no further completion
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
// ── Shell script generators ────────────────────────────────────────────────
|
|
61
|
+
export function bashCompletionScript() {
|
|
62
|
+
return `# Bash completion for opencli
|
|
63
|
+
# Add to ~/.bashrc: eval "$(opencli completion bash)"
|
|
64
|
+
_opencli_completions() {
|
|
65
|
+
local cur words cword
|
|
66
|
+
_get_comp_words_by_ref -n : cur words cword
|
|
67
|
+
|
|
68
|
+
local completions
|
|
69
|
+
completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)
|
|
70
|
+
|
|
71
|
+
COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
|
|
72
|
+
__ltrim_colon_completions "$cur"
|
|
73
|
+
}
|
|
74
|
+
complete -F _opencli_completions opencli
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
export function zshCompletionScript() {
|
|
78
|
+
return `# Zsh completion for opencli
|
|
79
|
+
# Add to ~/.zshrc: eval "$(opencli completion zsh)"
|
|
80
|
+
_opencli() {
|
|
81
|
+
local -a completions
|
|
82
|
+
local cword=$((CURRENT - 1))
|
|
83
|
+
completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"})
|
|
84
|
+
compadd -a completions
|
|
85
|
+
}
|
|
86
|
+
compdef _opencli opencli
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
export function fishCompletionScript() {
|
|
90
|
+
return `# Fish completion for opencli
|
|
91
|
+
# Add to ~/.config/fish/config.fish: opencli completion fish | source
|
|
92
|
+
complete -c opencli -f -a '(
|
|
93
|
+
set -l tokens (commandline -cop)
|
|
94
|
+
set -l cursor (count (commandline -cop))
|
|
95
|
+
opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null
|
|
96
|
+
)'
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Print the completion script for the requested shell.
|
|
101
|
+
*/
|
|
102
|
+
export function printCompletionScript(shell) {
|
|
103
|
+
switch (shell) {
|
|
104
|
+
case 'bash':
|
|
105
|
+
process.stdout.write(bashCompletionScript());
|
|
106
|
+
break;
|
|
107
|
+
case 'zsh':
|
|
108
|
+
process.stdout.write(zshCompletionScript());
|
|
109
|
+
break;
|
|
110
|
+
case 'fish':
|
|
111
|
+
process.stdout.write(fishCompletionScript());
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
throw new CliError('UNSUPPORTED_SHELL', `Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { PlaywrightMCP, getTokenFingerprint } from './browser.js';
|
|
7
|
+
import { PlaywrightMCP, getTokenFingerprint } from './browser/index.js';
|
|
8
8
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
9
9
|
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
10
10
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
@@ -266,14 +266,14 @@ export function discoverExtensionToken() {
|
|
|
266
266
|
const platform = os.platform();
|
|
267
267
|
const bases = [];
|
|
268
268
|
if (platform === 'darwin') {
|
|
269
|
-
bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
269
|
+
bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Dev'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
270
270
|
}
|
|
271
271
|
else if (platform === 'linux') {
|
|
272
|
-
bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
|
|
272
|
+
bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'google-chrome-unstable'), path.join(home, '.config', 'google-chrome-beta'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
|
|
273
273
|
}
|
|
274
274
|
else if (platform === 'win32') {
|
|
275
275
|
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
276
|
-
bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
|
|
276
|
+
bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Google', 'Chrome Dev', 'User Data'), path.join(appData, 'Google', 'Chrome Beta', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
|
|
277
277
|
}
|
|
278
278
|
const profiles = enumerateProfiles(bases);
|
|
279
279
|
const tokenRe = /([A-Za-z0-9_-]{40,50})/;
|
|
@@ -388,14 +388,14 @@ export function checkExtensionInstalled() {
|
|
|
388
388
|
const platform = os.platform();
|
|
389
389
|
const browserDirs = [];
|
|
390
390
|
if (platform === 'darwin') {
|
|
391
|
-
browserDirs.push({ name: 'Chrome', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome') }, { name: 'Chrome Canary', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary') }, { name: 'Chromium', base: path.join(home, 'Library', 'Application Support', 'Chromium') }, { name: 'Edge', base: path.join(home, 'Library', 'Application Support', 'Microsoft Edge') });
|
|
391
|
+
browserDirs.push({ name: 'Chrome', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome') }, { name: 'Chrome Dev', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Dev') }, { name: 'Chrome Beta', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta') }, { name: 'Chrome Canary', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary') }, { name: 'Chromium', base: path.join(home, 'Library', 'Application Support', 'Chromium') }, { name: 'Edge', base: path.join(home, 'Library', 'Application Support', 'Microsoft Edge') });
|
|
392
392
|
}
|
|
393
393
|
else if (platform === 'linux') {
|
|
394
|
-
browserDirs.push({ name: 'Chrome', base: path.join(home, '.config', 'google-chrome') }, { name: 'Chromium', base: path.join(home, '.config', 'chromium') }, { name: 'Edge', base: path.join(home, '.config', 'microsoft-edge') });
|
|
394
|
+
browserDirs.push({ name: 'Chrome', base: path.join(home, '.config', 'google-chrome') }, { name: 'Chrome Dev', base: path.join(home, '.config', 'google-chrome-unstable') }, { name: 'Chrome Beta', base: path.join(home, '.config', 'google-chrome-beta') }, { name: 'Chromium', base: path.join(home, '.config', 'chromium') }, { name: 'Edge', base: path.join(home, '.config', 'microsoft-edge') });
|
|
395
395
|
}
|
|
396
396
|
else if (platform === 'win32') {
|
|
397
397
|
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
398
|
-
browserDirs.push({ name: 'Chrome', base: path.join(appData, 'Google', 'Chrome', 'User Data') }, { name: 'Edge', base: path.join(appData, 'Microsoft', 'Edge', 'User Data') });
|
|
398
|
+
browserDirs.push({ name: 'Chrome', base: path.join(appData, 'Google', 'Chrome', 'User Data') }, { name: 'Chrome Dev', base: path.join(appData, 'Google', 'Chrome Dev', 'User Data') }, { name: 'Chrome Beta', base: path.join(appData, 'Google', 'Chrome Beta', 'User Data') }, { name: 'Edge', base: path.join(appData, 'Microsoft', 'Edge', 'User Data') });
|
|
399
399
|
}
|
|
400
400
|
const profiles = enumerateProfiles(browserDirs.map(d => d.base));
|
|
401
401
|
const foundBrowsers = [];
|
package/dist/engine.js
CHANGED
|
@@ -12,6 +12,8 @@ import * as path from 'node:path';
|
|
|
12
12
|
import yaml from 'js-yaml';
|
|
13
13
|
import { Strategy, registerCommand } from './registry.js';
|
|
14
14
|
import { executePipeline } from './pipeline.js';
|
|
15
|
+
import { log } from './logger.js';
|
|
16
|
+
import { AdapterLoadError } from './errors.js';
|
|
15
17
|
/** Set of TS module paths that have been loaded */
|
|
16
18
|
const _loadedModules = new Set();
|
|
17
19
|
/**
|
|
@@ -81,7 +83,7 @@ function loadFromManifest(manifestPath, clisDir) {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
catch (err) {
|
|
84
|
-
|
|
86
|
+
log.warn(`Failed to load manifest ${manifestPath}: ${err.message}`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
@@ -103,7 +105,7 @@ async function discoverClisFromFs(dir) {
|
|
|
103
105
|
else if ((file.endsWith('.js') && !file.endsWith('.d.js')) ||
|
|
104
106
|
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))) {
|
|
105
107
|
promises.push(import(`file://${filePath}`).catch((err) => {
|
|
106
|
-
|
|
108
|
+
log.warn(`Failed to load module ${filePath}: ${err.message}`);
|
|
107
109
|
}));
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -150,7 +152,7 @@ function registerYamlCli(filePath, defaultSite) {
|
|
|
150
152
|
registerCommand(cmd);
|
|
151
153
|
}
|
|
152
154
|
catch (err) {
|
|
153
|
-
|
|
155
|
+
log.warn(`Failed to load ${filePath}: ${err.message}`);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
/**
|
|
@@ -167,7 +169,7 @@ export async function executeCommand(cmd, page, kwargs, debug = false) {
|
|
|
167
169
|
_loadedModules.add(modulePath);
|
|
168
170
|
}
|
|
169
171
|
catch (err) {
|
|
170
|
-
throw new
|
|
172
|
+
throw new AdapterLoadError(`Failed to load adapter module ${modulePath}: ${err.message}`, 'Check that the adapter file exists and has no syntax errors.');
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
// After loading, the module's cli() call will have updated the registry
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified error types for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All errors thrown by the framework should extend CliError so that
|
|
5
|
+
* the top-level handler in main.ts can render consistent, helpful output.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CliError extends Error {
|
|
8
|
+
/** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'ADAPTER_LOAD') */
|
|
9
|
+
readonly code: string;
|
|
10
|
+
/** Human-readable hint on how to fix the problem */
|
|
11
|
+
readonly hint?: string;
|
|
12
|
+
constructor(code: string, message: string, hint?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class BrowserConnectError extends CliError {
|
|
15
|
+
constructor(message: string, hint?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class AdapterLoadError extends CliError {
|
|
18
|
+
constructor(message: string, hint?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class CommandExecutionError extends CliError {
|
|
21
|
+
constructor(message: string, hint?: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class ConfigError extends CliError {
|
|
24
|
+
constructor(message: string, hint?: string);
|
|
25
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified error types for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All errors thrown by the framework should extend CliError so that
|
|
5
|
+
* the top-level handler in main.ts can render consistent, helpful output.
|
|
6
|
+
*/
|
|
7
|
+
export class CliError extends Error {
|
|
8
|
+
/** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'ADAPTER_LOAD') */
|
|
9
|
+
code;
|
|
10
|
+
/** Human-readable hint on how to fix the problem */
|
|
11
|
+
hint;
|
|
12
|
+
constructor(code, message, hint) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'CliError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.hint = hint;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class BrowserConnectError extends CliError {
|
|
20
|
+
constructor(message, hint) {
|
|
21
|
+
super('BROWSER_CONNECT', message, hint);
|
|
22
|
+
this.name = 'BrowserConnectError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class AdapterLoadError extends CliError {
|
|
26
|
+
constructor(message, hint) {
|
|
27
|
+
super('ADAPTER_LOAD', message, hint);
|
|
28
|
+
this.name = 'AdapterLoadError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class CommandExecutionError extends CliError {
|
|
32
|
+
constructor(message, hint) {
|
|
33
|
+
super('COMMAND_EXEC', message, hint);
|
|
34
|
+
this.name = 'CommandExecutionError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class ConfigError extends CliError {
|
|
38
|
+
constructor(message, hint) {
|
|
39
|
+
super('CONFIG', message, hint);
|
|
40
|
+
this.name = 'ConfigError';
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logging for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All framework output (warnings, debug info, errors) should go through
|
|
5
|
+
* this module so that verbosity levels are respected consistently.
|
|
6
|
+
*/
|
|
7
|
+
export declare const log: {
|
|
8
|
+
/** Informational message (always shown) */
|
|
9
|
+
info(msg: string): void;
|
|
10
|
+
/** Warning (always shown) */
|
|
11
|
+
warn(msg: string): void;
|
|
12
|
+
/** Error (always shown) */
|
|
13
|
+
error(msg: string): void;
|
|
14
|
+
/** Verbose output (only when OPENCLI_VERBOSE is set or -v flag) */
|
|
15
|
+
verbose(msg: string): void;
|
|
16
|
+
/** Debug output (only when DEBUG includes 'opencli') */
|
|
17
|
+
debug(msg: string): void;
|
|
18
|
+
/** Step-style debug (for pipeline steps, etc.) */
|
|
19
|
+
step(stepNum: number, total: number, op: string, preview?: string): void;
|
|
20
|
+
/** Step result summary */
|
|
21
|
+
stepResult(summary: string): void;
|
|
22
|
+
};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified logging for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All framework output (warnings, debug info, errors) should go through
|
|
5
|
+
* this module so that verbosity levels are respected consistently.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
function isVerbose() {
|
|
9
|
+
return !!process.env.OPENCLI_VERBOSE;
|
|
10
|
+
}
|
|
11
|
+
function isDebug() {
|
|
12
|
+
return !!process.env.DEBUG?.includes('opencli');
|
|
13
|
+
}
|
|
14
|
+
export const log = {
|
|
15
|
+
/** Informational message (always shown) */
|
|
16
|
+
info(msg) {
|
|
17
|
+
process.stderr.write(`${chalk.blue('ℹ')} ${msg}\n`);
|
|
18
|
+
},
|
|
19
|
+
/** Warning (always shown) */
|
|
20
|
+
warn(msg) {
|
|
21
|
+
process.stderr.write(`${chalk.yellow('⚠')} ${msg}\n`);
|
|
22
|
+
},
|
|
23
|
+
/** Error (always shown) */
|
|
24
|
+
error(msg) {
|
|
25
|
+
process.stderr.write(`${chalk.red('✖')} ${msg}\n`);
|
|
26
|
+
},
|
|
27
|
+
/** Verbose output (only when OPENCLI_VERBOSE is set or -v flag) */
|
|
28
|
+
verbose(msg) {
|
|
29
|
+
if (isVerbose()) {
|
|
30
|
+
process.stderr.write(`${chalk.dim('[verbose]')} ${msg}\n`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
/** Debug output (only when DEBUG includes 'opencli') */
|
|
34
|
+
debug(msg) {
|
|
35
|
+
if (isDebug()) {
|
|
36
|
+
process.stderr.write(`${chalk.dim('[debug]')} ${msg}\n`);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
/** Step-style debug (for pipeline steps, etc.) */
|
|
40
|
+
step(stepNum, total, op, preview = '') {
|
|
41
|
+
process.stderr.write(` ${chalk.dim(`[${stepNum}/${total}]`)} ${chalk.bold.cyan(op)}${preview}\n`);
|
|
42
|
+
},
|
|
43
|
+
/** Step result summary */
|
|
44
|
+
stepResult(summary) {
|
|
45
|
+
process.stderr.write(` ${chalk.dim(`→ ${summary}`)}\n`);
|
|
46
|
+
},
|
|
47
|
+
};
|
package/dist/main.js
CHANGED
|
@@ -10,14 +10,38 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { discoverClis, executeCommand } from './engine.js';
|
|
11
11
|
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
12
12
|
import { render as renderOutput } from './output.js';
|
|
13
|
-
import { PlaywrightMCP } from './browser.js';
|
|
13
|
+
import { PlaywrightMCP } from './browser/index.js';
|
|
14
14
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
15
15
|
import { PKG_VERSION } from './version.js';
|
|
16
|
+
import { getCompletions, printCompletionScript } from './completion.js';
|
|
17
|
+
import { CliError } from './errors.js';
|
|
16
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
19
|
const __dirname = path.dirname(__filename);
|
|
18
20
|
const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
|
|
19
21
|
const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
|
|
20
22
|
await discoverClis(BUILTIN_CLIS, USER_CLIS);
|
|
23
|
+
// ── Fast-path: handle --get-completions before commander parses ─────────
|
|
24
|
+
// Usage: opencli --get-completions --cursor <N> [word1 word2 ...]
|
|
25
|
+
const getCompIdx = process.argv.indexOf('--get-completions');
|
|
26
|
+
if (getCompIdx !== -1) {
|
|
27
|
+
const rest = process.argv.slice(getCompIdx + 1);
|
|
28
|
+
let cursor;
|
|
29
|
+
const words = [];
|
|
30
|
+
for (let i = 0; i < rest.length; i++) {
|
|
31
|
+
if (rest[i] === '--cursor' && i + 1 < rest.length) {
|
|
32
|
+
cursor = parseInt(rest[i + 1], 10);
|
|
33
|
+
i++; // skip the value
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
words.push(rest[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (cursor === undefined)
|
|
40
|
+
cursor = words.length;
|
|
41
|
+
const candidates = getCompletions(words, cursor);
|
|
42
|
+
process.stdout.write(candidates.join('\n') + '\n');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
21
45
|
const program = new Command();
|
|
22
46
|
program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
|
|
23
47
|
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
@@ -130,6 +154,12 @@ program.command('setup')
|
|
|
130
154
|
const { runSetup } = await import('./setup.js');
|
|
131
155
|
await runSetup({ cliVersion: PKG_VERSION, token: opts.token });
|
|
132
156
|
});
|
|
157
|
+
program.command('completion')
|
|
158
|
+
.description('Output shell completion script')
|
|
159
|
+
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
160
|
+
.action((shell) => {
|
|
161
|
+
printCompletionScript(shell);
|
|
162
|
+
});
|
|
133
163
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
134
164
|
const registry = getRegistry();
|
|
135
165
|
const siteGroups = new Map();
|
|
@@ -199,7 +229,12 @@ for (const [, cmd] of registry) {
|
|
|
199
229
|
renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
|
|
200
230
|
}
|
|
201
231
|
catch (err) {
|
|
202
|
-
if (
|
|
232
|
+
if (err instanceof CliError) {
|
|
233
|
+
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|
|
234
|
+
if (err.hint)
|
|
235
|
+
console.error(chalk.yellow(`Hint: ${err.hint}`));
|
|
236
|
+
}
|
|
237
|
+
else if (actionOpts.verbose && err.stack) {
|
|
203
238
|
console.error(chalk.red(err.stack));
|
|
204
239
|
}
|
|
205
240
|
else {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipeline executor: runs YAML pipeline steps sequentially.
|
|
3
3
|
*/
|
|
4
|
-
import chalk from 'chalk';
|
|
5
4
|
import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
|
|
6
5
|
import { stepFetch } from './steps/fetch.js';
|
|
7
6
|
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
|
|
8
7
|
import { stepIntercept } from './steps/intercept.js';
|
|
9
8
|
import { stepTap } from './steps/tap.js';
|
|
9
|
+
import { log } from '../logger.js';
|
|
10
10
|
/** Registry of all available step handlers */
|
|
11
11
|
const STEP_HANDLERS = {
|
|
12
12
|
navigate: stepNavigate,
|
|
@@ -43,7 +43,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
45
45
|
if (debug)
|
|
46
|
-
|
|
46
|
+
log.warn(`Unknown step: ${op}`);
|
|
47
47
|
}
|
|
48
48
|
if (debug)
|
|
49
49
|
debugStepResult(op, data);
|
|
@@ -59,24 +59,24 @@ function debugStepStart(stepNum, total, op, params) {
|
|
|
59
59
|
else if (params && typeof params === 'object' && !Array.isArray(params)) {
|
|
60
60
|
preview = ` (${Object.keys(params).join(', ')})`;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
log.step(stepNum, total, op, preview);
|
|
63
63
|
}
|
|
64
64
|
function debugStepResult(op, data) {
|
|
65
65
|
if (data === null || data === undefined) {
|
|
66
|
-
|
|
66
|
+
log.stepResult('(no data)');
|
|
67
67
|
}
|
|
68
68
|
else if (Array.isArray(data)) {
|
|
69
|
-
|
|
69
|
+
log.stepResult(`${data.length} items`);
|
|
70
70
|
}
|
|
71
71
|
else if (typeof data === 'object') {
|
|
72
72
|
const keys = Object.keys(data).slice(0, 5);
|
|
73
|
-
|
|
73
|
+
log.stepResult(`dict (${keys.join(', ')}${Object.keys(data).length > 5 ? '...' : ''})`);
|
|
74
74
|
}
|
|
75
75
|
else if (typeof data === 'string') {
|
|
76
76
|
const p = data.slice(0, 60).replace(/\n/g, '\\n');
|
|
77
|
-
|
|
77
|
+
log.stepResult(`"${p}${data.length > 60 ? '...' : ''}"`);
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
|
-
|
|
80
|
+
log.stepResult(`${typeof data}`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Browser interaction primitives.
|
|
4
4
|
*/
|
|
5
5
|
import type { IPage } from '../../types.js';
|
|
6
|
-
export declare function stepNavigate(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
7
|
-
export declare function stepClick(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
8
|
-
export declare function stepType(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
9
|
-
export declare function stepWait(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
10
|
-
export declare function stepPress(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
11
|
-
export declare function stepSnapshot(page: IPage, params: any, _data: any, _args: Record<string, any>): Promise<any>;
|
|
12
|
-
export declare function stepEvaluate(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
6
|
+
export declare function stepNavigate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
7
|
+
export declare function stepClick(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
8
|
+
export declare function stepType(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
9
|
+
export declare function stepWait(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
10
|
+
export declare function stepPress(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
11
|
+
export declare function stepSnapshot(page: IPage | null, params: any, _data: any, _args: Record<string, any>): Promise<any>;
|
|
12
|
+
export declare function stepEvaluate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Pipeline step: intercept — declarative XHR interception.
|
|
3
3
|
*/
|
|
4
4
|
import type { IPage } from '../../types.js';
|
|
5
|
-
export declare function stepIntercept(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
5
|
+
export declare function stepIntercept(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* 5. Returns the captured data (optionally sub-selected)
|
|
10
10
|
*/
|
|
11
11
|
import type { IPage } from '../../types.js';
|
|
12
|
-
export declare function stepTap(page: IPage, params: any, data: any, args: Record<string, any>): Promise<any>;
|
|
12
|
+
export declare function stepTap(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any>;
|
package/dist/setup.js
CHANGED
|
@@ -9,7 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import { createInterface } from 'node:readline/promises';
|
|
10
10
|
import { stdin as input, stdout as output } from 'node:process';
|
|
11
11
|
import { PLAYWRIGHT_TOKEN_ENV, checkExtensionInstalled, checkTokenConnectivity, discoverExtensionToken, fileExists, getDefaultShellRcPath, runBrowserDoctor, shortenPath, toolName, upsertJsonConfigToken, upsertShellToken, upsertTomlConfigToken, writeFileWithMkdir, } from './doctor.js';
|
|
12
|
-
import { getTokenFingerprint } from './browser.js';
|
|
12
|
+
import { getTokenFingerprint } from './browser/index.js';
|
|
13
13
|
import { checkboxPrompt } from './tui.js';
|
|
14
14
|
export async function runSetup(opts = {}) {
|
|
15
15
|
console.log();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"dev": "tsx src/main.ts",
|
|
18
18
|
"build": "tsc && npm run clean-yaml && npm run copy-yaml && npm run build-manifest",
|
|
19
19
|
"build-manifest": "node dist/build-manifest.js",
|
|
20
|
-
"clean-yaml": "node -
|
|
21
|
-
"copy-yaml": "node -
|
|
20
|
+
"clean-yaml": "node scripts/clean-yaml.cjs",
|
|
21
|
+
"copy-yaml": "node scripts/copy-yaml.cjs",
|
|
22
22
|
"start": "node dist/main.js",
|
|
23
|
+
"postinstall": "node scripts/postinstall.js || true",
|
|
23
24
|
"typecheck": "tsc --noEmit",
|
|
24
25
|
"lint": "tsc --noEmit",
|
|
25
26
|
"prepublishOnly": "npm run build",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean YAML files from dist/clis/ before copying fresh ones.
|
|
3
|
+
*/
|
|
4
|
+
const { readdirSync, rmSync, existsSync, statSync } = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function walk(dir) {
|
|
8
|
+
if (!existsSync(dir)) return;
|
|
9
|
+
for (const f of readdirSync(dir)) {
|
|
10
|
+
const fp = path.join(dir, f);
|
|
11
|
+
if (statSync(fp).isDirectory()) {
|
|
12
|
+
walk(fp);
|
|
13
|
+
} else if (/\.ya?ml$/.test(f)) {
|
|
14
|
+
rmSync(fp);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
walk('dist/clis');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy YAML files from src/clis/ to dist/clis/.
|
|
3
|
+
*/
|
|
4
|
+
const { readdirSync, copyFileSync, mkdirSync, existsSync, statSync } = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function walk(src, dst) {
|
|
8
|
+
if (!existsSync(src)) return;
|
|
9
|
+
for (const f of readdirSync(src)) {
|
|
10
|
+
const sp = path.join(src, f);
|
|
11
|
+
const dp = path.join(dst, f);
|
|
12
|
+
if (statSync(sp).isDirectory()) {
|
|
13
|
+
walk(sp, dp);
|
|
14
|
+
} else if (/\.ya?ml$/.test(f)) {
|
|
15
|
+
mkdirSync(path.dirname(dp), { recursive: true });
|
|
16
|
+
copyFileSync(sp, dp);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
walk('src/clis', 'dist/clis');
|