@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.
- package/README.md +3 -2
- package/README.zh-CN.md +4 -3
- package/SKILL.md +7 -4
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +25 -92
- package/dist/browser.test.js +18 -1
- package/dist/cascade.d.ts +1 -1
- package/dist/cascade.js +42 -75
- package/dist/cli-manifest.json +80 -0
- package/dist/clis/coupang/add-to-cart.d.ts +1 -0
- package/dist/clis/coupang/add-to-cart.js +141 -0
- package/dist/clis/coupang/search.d.ts +1 -0
- package/dist/clis/coupang/search.js +453 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +30 -0
- package/dist/coupang.d.ts +24 -0
- package/dist/coupang.js +262 -0
- package/dist/coupang.test.d.ts +1 -0
- package/dist/coupang.test.js +62 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +226 -25
- package/dist/doctor.test.js +13 -6
- package/dist/engine.js +3 -3
- package/dist/engine.test.d.ts +4 -0
- package/dist/engine.test.js +67 -0
- package/dist/explore.js +1 -15
- package/dist/interceptor.d.ts +42 -0
- package/dist/interceptor.js +138 -0
- package/dist/main.js +8 -4
- package/dist/output.js +0 -5
- package/dist/pipeline/steps/intercept.js +4 -54
- package/dist/pipeline/steps/tap.js +11 -51
- package/dist/registry.d.ts +3 -1
- package/dist/registry.test.d.ts +4 -0
- package/dist/registry.test.js +90 -0
- package/dist/runtime.d.ts +15 -1
- package/dist/runtime.js +11 -6
- package/dist/setup.d.ts +4 -0
- package/dist/setup.js +145 -0
- package/dist/synthesize.js +5 -5
- package/dist/tui.d.ts +22 -0
- package/dist/tui.js +139 -0
- package/dist/validate.js +21 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +7 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/src/browser.test.ts +20 -1
- package/src/browser.ts +25 -87
- package/src/cascade.ts +47 -75
- package/src/clis/coupang/add-to-cart.ts +149 -0
- package/src/clis/coupang/search.ts +466 -0
- package/src/constants.ts +35 -0
- package/src/coupang.test.ts +78 -0
- package/src/coupang.ts +302 -0
- package/src/doctor.test.ts +15 -6
- package/src/doctor.ts +221 -25
- package/src/engine.test.ts +77 -0
- package/src/engine.ts +5 -5
- package/src/explore.ts +2 -15
- package/src/interceptor.ts +153 -0
- package/src/main.ts +9 -5
- package/src/output.ts +0 -4
- package/src/pipeline/executor.ts +15 -15
- package/src/pipeline/steps/intercept.ts +4 -55
- package/src/pipeline/steps/tap.ts +12 -51
- package/src/registry.test.ts +106 -0
- package/src/registry.ts +4 -1
- package/src/runtime.ts +22 -8
- package/src/setup.ts +169 -0
- package/src/synthesize.ts +5 -5
- package/src/tui.ts +171 -0
- package/src/validate.ts +22 -0
- package/src/verify.ts +10 -1
- 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
|
-
/**
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
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
|
-
/**
|
|
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
|
+
})();
|