@jackwener/opencli 0.5.1 → 0.6.0

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 (76) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +4 -3
  3. package/SKILL.md +7 -4
  4. package/dist/browser.d.ts +7 -3
  5. package/dist/browser.js +25 -92
  6. package/dist/browser.test.js +18 -1
  7. package/dist/cascade.d.ts +1 -1
  8. package/dist/cascade.js +42 -75
  9. package/dist/cli-manifest.json +80 -0
  10. package/dist/clis/coupang/add-to-cart.d.ts +1 -0
  11. package/dist/clis/coupang/add-to-cart.js +141 -0
  12. package/dist/clis/coupang/search.d.ts +1 -0
  13. package/dist/clis/coupang/search.js +453 -0
  14. package/dist/constants.d.ts +13 -0
  15. package/dist/constants.js +30 -0
  16. package/dist/coupang.d.ts +24 -0
  17. package/dist/coupang.js +262 -0
  18. package/dist/coupang.test.d.ts +1 -0
  19. package/dist/coupang.test.js +62 -0
  20. package/dist/doctor.d.ts +15 -0
  21. package/dist/doctor.js +226 -25
  22. package/dist/doctor.test.js +13 -6
  23. package/dist/engine.js +3 -3
  24. package/dist/engine.test.d.ts +4 -0
  25. package/dist/engine.test.js +67 -0
  26. package/dist/explore.js +1 -15
  27. package/dist/interceptor.d.ts +42 -0
  28. package/dist/interceptor.js +138 -0
  29. package/dist/main.js +8 -4
  30. package/dist/output.js +0 -5
  31. package/dist/pipeline/steps/intercept.js +4 -54
  32. package/dist/pipeline/steps/tap.js +11 -51
  33. package/dist/registry.d.ts +3 -1
  34. package/dist/registry.test.d.ts +4 -0
  35. package/dist/registry.test.js +90 -0
  36. package/dist/runtime.d.ts +15 -1
  37. package/dist/runtime.js +11 -6
  38. package/dist/setup.d.ts +4 -0
  39. package/dist/setup.js +145 -0
  40. package/dist/synthesize.js +5 -5
  41. package/dist/tui.d.ts +22 -0
  42. package/dist/tui.js +139 -0
  43. package/dist/validate.js +21 -0
  44. package/dist/verify.d.ts +7 -0
  45. package/dist/verify.js +7 -1
  46. package/dist/version.d.ts +4 -0
  47. package/dist/version.js +16 -0
  48. package/package.json +1 -1
  49. package/src/browser.test.ts +20 -1
  50. package/src/browser.ts +25 -87
  51. package/src/cascade.ts +47 -75
  52. package/src/clis/coupang/add-to-cart.ts +149 -0
  53. package/src/clis/coupang/search.ts +466 -0
  54. package/src/constants.ts +35 -0
  55. package/src/coupang.test.ts +78 -0
  56. package/src/coupang.ts +302 -0
  57. package/src/doctor.test.ts +15 -6
  58. package/src/doctor.ts +221 -25
  59. package/src/engine.test.ts +77 -0
  60. package/src/engine.ts +5 -5
  61. package/src/explore.ts +2 -15
  62. package/src/interceptor.ts +153 -0
  63. package/src/main.ts +9 -5
  64. package/src/output.ts +0 -4
  65. package/src/pipeline/executor.ts +15 -15
  66. package/src/pipeline/steps/intercept.ts +4 -55
  67. package/src/pipeline/steps/tap.ts +12 -51
  68. package/src/registry.test.ts +106 -0
  69. package/src/registry.ts +4 -1
  70. package/src/runtime.ts +22 -8
  71. package/src/setup.ts +169 -0
  72. package/src/synthesize.ts +5 -5
  73. package/src/tui.ts +171 -0
  74. package/src/validate.ts +22 -0
  75. package/src/verify.ts +10 -1
  76. package/src/version.ts +18 -0
package/src/setup.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * setup.ts — Interactive Playwright MCP token setup
3
+ *
4
+ * Discovers the extension token, shows an interactive checkbox
5
+ * for selecting which config files to update, and applies changes.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import chalk from 'chalk';
9
+ import { createInterface } from 'node:readline/promises';
10
+ import { stdin as input, stdout as output } from 'node:process';
11
+ import {
12
+ type DoctorReport,
13
+ PLAYWRIGHT_TOKEN_ENV,
14
+ discoverExtensionToken,
15
+ fileExists,
16
+ getDefaultShellRcPath,
17
+ runBrowserDoctor,
18
+ shortenPath,
19
+ toolName,
20
+ upsertJsonConfigToken,
21
+ upsertShellToken,
22
+ upsertTomlConfigToken,
23
+ writeFileWithMkdir,
24
+ } from './doctor.js';
25
+ import { getTokenFingerprint } from './browser.js';
26
+ import { type CheckboxItem, checkboxPrompt } from './tui.js';
27
+
28
+ export async function runSetup(opts: { cliVersion?: string; token?: string } = {}) {
29
+ console.log();
30
+ console.log(chalk.bold(' opencli setup') + chalk.dim(' — Playwright MCP token configuration'));
31
+ console.log();
32
+
33
+ // Step 1: Discover token
34
+ let token = opts.token ?? null;
35
+
36
+ if (!token) {
37
+ const extensionToken = discoverExtensionToken();
38
+ const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
39
+
40
+ if (extensionToken && envToken && extensionToken === envToken) {
41
+ token = extensionToken;
42
+ console.log(` ${chalk.green('✓')} Token auto-discovered from Chrome extension`);
43
+ console.log(` Fingerprint: ${chalk.bold(getTokenFingerprint(token) ?? 'unknown')}`);
44
+ } else if (extensionToken) {
45
+ token = extensionToken;
46
+ console.log(` ${chalk.green('✓')} Token discovered from Chrome extension ` +
47
+ chalk.dim(`(${getTokenFingerprint(token)})`));
48
+ if (envToken && envToken !== extensionToken) {
49
+ console.log(` ${chalk.yellow('!')} Environment has different token ` +
50
+ chalk.dim(`(${getTokenFingerprint(envToken)})`));
51
+ }
52
+ } else if (envToken) {
53
+ token = envToken;
54
+ console.log(` ${chalk.green('✓')} Token from environment variable ` +
55
+ chalk.dim(`(${getTokenFingerprint(token)})`));
56
+ }
57
+ } else {
58
+ console.log(` ${chalk.green('✓')} Using provided token ` +
59
+ chalk.dim(`(${getTokenFingerprint(token)})`));
60
+ }
61
+
62
+ if (!token) {
63
+ console.log(` ${chalk.yellow('!')} No token found. Please enter it manually.`);
64
+ console.log(chalk.dim(' (Find it in the Playwright MCP Bridge extension → Status page)'));
65
+ console.log();
66
+ const rl = createInterface({ input, output });
67
+ const answer = await rl.question(' Token: ');
68
+ rl.close();
69
+ token = answer.trim();
70
+ if (!token) {
71
+ console.log(chalk.red('\n No token provided. Aborting.\n'));
72
+ return;
73
+ }
74
+ }
75
+
76
+ const fingerprint = getTokenFingerprint(token) ?? 'unknown';
77
+ console.log();
78
+
79
+ // Step 2: Scan all config locations
80
+ const report = await runBrowserDoctor({ token, cliVersion: opts.cliVersion });
81
+
82
+ // Step 3: Build checkbox items
83
+ const items: CheckboxItem[] = [];
84
+
85
+ // Shell file
86
+ const shellPath = report.shellFiles[0]?.path ?? getDefaultShellRcPath();
87
+ const shellStatus = report.shellFiles[0];
88
+ const shellFp = shellStatus?.fingerprint;
89
+ const shellOk = shellFp === fingerprint;
90
+ const shellTool = toolName(shellPath) || 'Shell';
91
+ items.push({
92
+ label: padRight(shortenPath(shellPath), 50) + chalk.dim(` [${shellTool}]`),
93
+ value: `shell:${shellPath}`,
94
+ checked: !shellOk,
95
+ status: shellOk ? `configured (${shellFp})` : shellFp ? `mismatch (${shellFp})` : 'missing',
96
+ statusColor: shellOk ? 'green' : shellFp ? 'yellow' : 'red',
97
+ });
98
+
99
+ // Config files
100
+ for (const config of report.configs) {
101
+ const fp = config.fingerprint;
102
+ const ok = fp === fingerprint;
103
+ const tool = toolName(config.path);
104
+ items.push({
105
+ label: padRight(shortenPath(config.path), 50) + chalk.dim(tool ? ` [${tool}]` : ''),
106
+ value: `config:${config.path}`,
107
+ checked: !ok,
108
+ status: ok ? `configured (${fp})` : !config.exists ? 'will create' : fp ? `mismatch (${fp})` : 'missing',
109
+ statusColor: ok ? 'green' : 'yellow',
110
+ });
111
+ }
112
+
113
+ // Step 4: Show interactive checkbox
114
+ console.clear();
115
+ const selected = await checkboxPrompt(items, {
116
+ title: ` ${chalk.bold('opencli setup')} — token ${chalk.cyan(fingerprint)}`,
117
+ });
118
+
119
+ if (selected.length === 0) {
120
+ console.log(chalk.dim(' No changes made.\n'));
121
+ return;
122
+ }
123
+
124
+ // Step 5: Apply changes
125
+ const written: string[] = [];
126
+ let wroteShell = false;
127
+
128
+ for (const sel of selected) {
129
+ if (sel.startsWith('shell:')) {
130
+ const p = sel.slice('shell:'.length);
131
+ const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
132
+ writeFileWithMkdir(p, upsertShellToken(before, token));
133
+ written.push(p);
134
+ wroteShell = true;
135
+ } else if (sel.startsWith('config:')) {
136
+ const p = sel.slice('config:'.length);
137
+ const config = report.configs.find(c => c.path === p);
138
+ if (config && config.parseError) continue;
139
+ const before = fileExists(p) ? fs.readFileSync(p, 'utf-8') : '';
140
+ const format = config?.format ?? (p.endsWith('.toml') ? 'toml' : 'json');
141
+ const next = format === 'toml' ? upsertTomlConfigToken(before, token) : upsertJsonConfigToken(before, token);
142
+ writeFileWithMkdir(p, next);
143
+ written.push(p);
144
+ }
145
+ }
146
+
147
+ process.env[PLAYWRIGHT_TOKEN_ENV] = token;
148
+
149
+ // Step 6: Summary
150
+ if (written.length > 0) {
151
+ console.log(chalk.green.bold(` ✓ Updated ${written.length} file(s):`));
152
+ for (const p of written) {
153
+ const tool = toolName(p);
154
+ console.log(` ${chalk.dim('•')} ${shortenPath(p)}${tool ? chalk.dim(` [${tool}]`) : ''}`);
155
+ }
156
+ if (wroteShell) {
157
+ console.log();
158
+ console.log(chalk.cyan(` 💡 Run ${chalk.bold(`source ${shortenPath(shellPath)}`)} to apply token to current shell.`));
159
+ }
160
+ } else {
161
+ console.log(chalk.yellow(' No files were changed.'));
162
+ }
163
+ console.log();
164
+ }
165
+
166
+ function padRight(s: string, n: number): string {
167
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
168
+ return visible.length >= n ? s : s + ' '.repeat(n - visible.length);
169
+ }
package/src/synthesize.ts CHANGED
@@ -6,12 +6,12 @@
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import yaml from 'js-yaml';
9
+ import { VOLATILE_PARAMS, SEARCH_PARAMS, LIMIT_PARAMS, PAGINATION_PARAMS } from './constants.js';
9
10
 
10
- /** Volatile params to strip from generated URLs */
11
- const VOLATILE_PARAMS = new Set(['w_rid', 'wts', 'callback', '_', 'timestamp', 't', 'nonce', 'sign']);
12
- const SEARCH_PARAM_NAMES = new Set(['q', 'query', 'keyword', 'search', 'wd', 'kw', 'w', 'search_query']);
13
- const LIMIT_PARAM_NAMES = new Set(['ps', 'page_size', 'limit', 'count', 'per_page', 'size', 'num']);
14
- const PAGE_PARAM_NAMES = new Set(['pn', 'page', 'page_num', 'offset', 'cursor']);
11
+ /** Renamed aliases for backward compatibility with local references */
12
+ const SEARCH_PARAM_NAMES = SEARCH_PARAMS;
13
+ const LIMIT_PARAM_NAMES = LIMIT_PARAMS;
14
+ const PAGE_PARAM_NAMES = PAGINATION_PARAMS;
15
15
 
16
16
  export function synthesizeFromExplore(
17
17
  target: string,
package/src/tui.ts ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * tui.ts — Zero-dependency interactive TUI components
3
+ *
4
+ * Uses raw stdin mode + ANSI escape codes for interactive prompts.
5
+ */
6
+ import chalk from 'chalk';
7
+
8
+ export interface CheckboxItem {
9
+ label: string;
10
+ value: string;
11
+ checked: boolean;
12
+ /** Optional status to display after the label */
13
+ status?: string;
14
+ statusColor?: 'green' | 'yellow' | 'red' | 'dim';
15
+ }
16
+
17
+ /**
18
+ * Interactive multi-select checkbox prompt.
19
+ *
20
+ * Controls:
21
+ * ↑/↓ or j/k — navigate
22
+ * Space — toggle selection
23
+ * a — toggle all
24
+ * Enter — confirm
25
+ * q/Esc — cancel (returns empty)
26
+ */
27
+ export async function checkboxPrompt(
28
+ items: CheckboxItem[],
29
+ opts: { title?: string; hint?: string } = {},
30
+ ): Promise<string[]> {
31
+ if (items.length === 0) return [];
32
+
33
+ const { stdin, stdout } = process;
34
+ if (!stdin.isTTY) {
35
+ // Non-interactive: return all checked items
36
+ return items.filter(i => i.checked).map(i => i.value);
37
+ }
38
+
39
+ let cursor = 0;
40
+ const state = items.map(i => ({ ...i }));
41
+
42
+ function colorStatus(status: string | undefined, color: CheckboxItem['statusColor']): string {
43
+ if (!status) return '';
44
+ switch (color) {
45
+ case 'green': return chalk.green(status);
46
+ case 'yellow': return chalk.yellow(status);
47
+ case 'red': return chalk.red(status);
48
+ case 'dim': return chalk.dim(status);
49
+ default: return chalk.dim(status);
50
+ }
51
+ }
52
+
53
+ function render() {
54
+ // Move cursor to start and clear
55
+ let out = '';
56
+
57
+ if (opts.title) {
58
+ out += `\n${chalk.bold(opts.title)}\n\n`;
59
+ }
60
+
61
+ for (let i = 0; i < state.length; i++) {
62
+ const item = state[i];
63
+ const pointer = i === cursor ? chalk.cyan('❯') : ' ';
64
+ const checkbox = item.checked ? chalk.green('◉') : chalk.dim('○');
65
+ const label = i === cursor ? chalk.bold(item.label) : item.label;
66
+ const status = colorStatus(item.status, item.statusColor);
67
+ out += ` ${pointer} ${checkbox} ${label}${status ? ` ${status}` : ''}\n`;
68
+ }
69
+
70
+ out += `\n ${chalk.dim('↑↓ navigate · Space toggle · a all · Enter confirm · q cancel')}\n`;
71
+
72
+ return out;
73
+ }
74
+
75
+ return new Promise<string[]>((resolve) => {
76
+ const wasRaw = stdin.isRaw;
77
+ stdin.setRawMode(true);
78
+ stdin.resume();
79
+ stdout.write('\x1b[?25l'); // Hide cursor
80
+
81
+ let firstDraw = true;
82
+
83
+ function draw() {
84
+ // Clear previous render (skip on first draw)
85
+ if (!firstDraw) {
86
+ const lines = render().split('\n').length;
87
+ stdout.write(`\x1b[${lines}A\x1b[J`);
88
+ }
89
+ firstDraw = false;
90
+ stdout.write(render());
91
+ }
92
+
93
+ function cleanup() {
94
+ stdin.setRawMode(wasRaw ?? false);
95
+ stdin.pause();
96
+ stdin.removeListener('data', onData);
97
+ // Clear the TUI and restore cursor
98
+ const lines = render().split('\n').length;
99
+ stdout.write(`\x1b[${lines}A\x1b[J`);
100
+ stdout.write('\x1b[?25h'); // Show cursor
101
+ }
102
+
103
+ function onData(data: Buffer) {
104
+ const key = data.toString();
105
+
106
+ // Arrow up / k
107
+ if (key === '\x1b[A' || key === 'k') {
108
+ cursor = (cursor - 1 + state.length) % state.length;
109
+ draw();
110
+ return;
111
+ }
112
+
113
+ // Arrow down / j
114
+ if (key === '\x1b[B' || key === 'j') {
115
+ cursor = (cursor + 1) % state.length;
116
+ draw();
117
+ return;
118
+ }
119
+
120
+ // Space — toggle
121
+ if (key === ' ') {
122
+ state[cursor].checked = !state[cursor].checked;
123
+ draw();
124
+ return;
125
+ }
126
+
127
+ // Tab — toggle and move down
128
+ if (key === '\t') {
129
+ state[cursor].checked = !state[cursor].checked;
130
+ cursor = (cursor + 1) % state.length;
131
+ draw();
132
+ return;
133
+ }
134
+
135
+ // 'a' — toggle all
136
+ if (key === 'a') {
137
+ const allChecked = state.every(i => i.checked);
138
+ for (const item of state) item.checked = !allChecked;
139
+ draw();
140
+ return;
141
+ }
142
+
143
+ // Enter — confirm
144
+ if (key === '\r' || key === '\n') {
145
+ cleanup();
146
+ const selected = state.filter(i => i.checked).map(i => i.value);
147
+ // Show summary
148
+ stdout.write(` ${chalk.green('✓')} ${chalk.bold(`${selected.length} file(s) selected`)}\n\n`);
149
+ resolve(selected);
150
+ return;
151
+ }
152
+
153
+ // q / Esc — cancel
154
+ if (key === 'q' || key === '\x1b') {
155
+ cleanup();
156
+ stdout.write(` ${chalk.yellow('✗')} ${chalk.dim('Cancelled')}\n\n`);
157
+ resolve([]);
158
+ return;
159
+ }
160
+
161
+ // Ctrl+C — exit process
162
+ if (key === '\x03') {
163
+ cleanup();
164
+ process.exit(130);
165
+ }
166
+ }
167
+
168
+ stdin.on('data', onData);
169
+ draw();
170
+ });
171
+ }
package/src/validate.ts CHANGED
@@ -3,6 +3,14 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import yaml from 'js-yaml';
5
5
 
6
+ /** All recognized pipeline step names */
7
+ const KNOWN_STEP_NAMES = new Set([
8
+ 'navigate', 'click', 'type', 'wait', 'press', 'snapshot', 'scroll',
9
+ 'fetch', 'evaluate',
10
+ 'select', 'map', 'filter', 'sort', 'limit',
11
+ 'intercept', 'tap',
12
+ ]);
13
+
6
14
  export function validateClisWithTarget(dirs: string[], target?: string): any {
7
15
  const results: any[] = [];
8
16
  let errors = 0; let warnings = 0; let files = 0;
@@ -38,6 +46,20 @@ function validateYamlFile(filePath: string): any {
38
46
  if (def.pipeline && !Array.isArray(def.pipeline)) errors.push('"pipeline" must be an array');
39
47
  if (def.columns && !Array.isArray(def.columns)) errors.push('"columns" must be an array');
40
48
  if (def.args && typeof def.args !== 'object') errors.push('"args" must be an object');
49
+ // Validate pipeline step names (catch typos like 'navaigate')
50
+ if (Array.isArray(def.pipeline)) {
51
+ for (let i = 0; i < def.pipeline.length; i++) {
52
+ const step = def.pipeline[i];
53
+ if (step && typeof step === 'object') {
54
+ const stepKeys = Object.keys(step);
55
+ for (const key of stepKeys) {
56
+ if (!KNOWN_STEP_NAMES.has(key)) {
57
+ warnings.push(`Pipeline step ${i}: unknown step name "${key}" (did you mean one of: ${[...KNOWN_STEP_NAMES].join(', ')}?)`);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
41
63
  } catch (e: any) { errors.push(`YAML parse error: ${e.message}`); }
42
64
  return { path: filePath, errors, warnings };
43
65
  }
package/src/verify.ts CHANGED
@@ -1,9 +1,18 @@
1
- /** Verification: validate + smoke. */
1
+ /**
2
+ * Verification: runs validation and optional smoke test.
3
+ *
4
+ * The smoke test is intentionally kept as a stub — full browser-based
5
+ * smoke testing requires a running browser session and is better suited
6
+ * to the `opencli test` command or CI pipelines.
7
+ */
8
+
2
9
  import { validateClisWithTarget, renderValidationReport } from './validate.js';
10
+
3
11
  export async function verifyClis(opts: any): Promise<any> {
4
12
  const report = validateClisWithTarget([opts.builtinClis, opts.userClis], opts.target);
5
13
  return { ok: report.ok, validation: report, smoke: null };
6
14
  }
15
+
7
16
  export function renderVerifyReport(report: any): string {
8
17
  return renderValidationReport(report.validation);
9
18
  }
package/src/version.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Single source of truth for package version.
3
+ */
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
11
+
12
+ export const PKG_VERSION: string = (() => {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).version;
15
+ } catch {
16
+ return '0.0.0';
17
+ }
18
+ })();