btcp-browser-agent 0.1.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/CLAUDE.md +230 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/SKILL.md +143 -0
- package/SNAPSHOT_IMPROVEMENTS.md +302 -0
- package/USAGE.md +146 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/docs/browser-cli-design.md +500 -0
- package/examples/chrome-extension/CHANGELOG.md +210 -0
- package/examples/chrome-extension/DEBUG.md +231 -0
- package/examples/chrome-extension/ERROR_FIXED.md +147 -0
- package/examples/chrome-extension/QUICK_TEST.md +189 -0
- package/examples/chrome-extension/README.md +149 -0
- package/examples/chrome-extension/SESSION_ONLY_MODE.md +305 -0
- package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +97 -0
- package/examples/chrome-extension/build.js +43 -0
- package/examples/chrome-extension/manifest.json +37 -0
- package/examples/chrome-extension/package-lock.json +1063 -0
- package/examples/chrome-extension/package.json +21 -0
- package/examples/chrome-extension/popup.html +195 -0
- package/examples/chrome-extension/src/background.ts +12 -0
- package/examples/chrome-extension/src/content.ts +7 -0
- package/examples/chrome-extension/src/popup.ts +303 -0
- package/examples/chrome-extension/src/scenario-google-github.ts +389 -0
- package/examples/chrome-extension/test-page.html +127 -0
- package/examples/chrome-extension/tests/README.md +206 -0
- package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +380 -0
- package/examples/chrome-extension/tsconfig.json +14 -0
- package/examples/snapshots/README.md +207 -0
- package/examples/snapshots/amazon-com-detail.html +9528 -0
- package/examples/snapshots/amazon-com-detail.snapshot.txt +997 -0
- package/examples/snapshots/convert-snapshots.ts +97 -0
- package/examples/snapshots/edition-cnn-com.html +13292 -0
- package/examples/snapshots/edition-cnn-com.snapshot.txt +562 -0
- package/examples/snapshots/github-com-microsoft-vscode.html +2916 -0
- package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +455 -0
- package/examples/snapshots/google-search.html +20012 -0
- package/examples/snapshots/google-search.snapshot.txt +195 -0
- package/examples/snapshots/metadata.json +86 -0
- package/examples/snapshots/npr-org-templates.html +2031 -0
- package/examples/snapshots/npr-org-templates.snapshot.txt +224 -0
- package/examples/snapshots/stackoverflow-com.html +5216 -0
- package/examples/snapshots/stackoverflow-com.snapshot.txt +2404 -0
- package/examples/snapshots/test-all-mode.html +46 -0
- package/examples/snapshots/test-all-mode.snapshot.txt +5 -0
- package/examples/snapshots/validate.test.ts +296 -0
- package/package.json +65 -0
- package/packages/cli/package.json +42 -0
- package/packages/cli/src/__tests__/cli.test.ts +434 -0
- package/packages/cli/src/__tests__/errors.test.ts +226 -0
- package/packages/cli/src/__tests__/executor.test.ts +275 -0
- package/packages/cli/src/__tests__/formatter.test.ts +260 -0
- package/packages/cli/src/__tests__/parser.test.ts +288 -0
- package/packages/cli/src/__tests__/suggestions.test.ts +255 -0
- package/packages/cli/src/commands/back.ts +22 -0
- package/packages/cli/src/commands/check.ts +33 -0
- package/packages/cli/src/commands/clear.ts +33 -0
- package/packages/cli/src/commands/click.ts +32 -0
- package/packages/cli/src/commands/closetab.ts +31 -0
- package/packages/cli/src/commands/eval.ts +41 -0
- package/packages/cli/src/commands/fill.ts +30 -0
- package/packages/cli/src/commands/focus.ts +33 -0
- package/packages/cli/src/commands/forward.ts +22 -0
- package/packages/cli/src/commands/goto.ts +34 -0
- package/packages/cli/src/commands/help.ts +162 -0
- package/packages/cli/src/commands/hover.ts +34 -0
- package/packages/cli/src/commands/index.ts +129 -0
- package/packages/cli/src/commands/newtab.ts +35 -0
- package/packages/cli/src/commands/press.ts +40 -0
- package/packages/cli/src/commands/reload.ts +25 -0
- package/packages/cli/src/commands/screenshot.ts +27 -0
- package/packages/cli/src/commands/scroll.ts +64 -0
- package/packages/cli/src/commands/select.ts +35 -0
- package/packages/cli/src/commands/snapshot.ts +21 -0
- package/packages/cli/src/commands/tab.ts +32 -0
- package/packages/cli/src/commands/tabs.ts +26 -0
- package/packages/cli/src/commands/text.ts +27 -0
- package/packages/cli/src/commands/title.ts +17 -0
- package/packages/cli/src/commands/type.ts +38 -0
- package/packages/cli/src/commands/uncheck.ts +33 -0
- package/packages/cli/src/commands/url.ts +17 -0
- package/packages/cli/src/commands/wait.ts +54 -0
- package/packages/cli/src/errors.ts +164 -0
- package/packages/cli/src/executor.ts +68 -0
- package/packages/cli/src/formatter.ts +215 -0
- package/packages/cli/src/index.ts +257 -0
- package/packages/cli/src/parser.ts +195 -0
- package/packages/cli/src/suggestions.ts +207 -0
- package/packages/cli/src/terminal/Terminal.ts +365 -0
- package/packages/cli/src/terminal/index.ts +5 -0
- package/packages/cli/src/types.ts +155 -0
- package/packages/cli/tsconfig.json +20 -0
- package/packages/core/package.json +35 -0
- package/packages/core/src/actions.ts +1210 -0
- package/packages/core/src/errors.ts +296 -0
- package/packages/core/src/index.test.ts +638 -0
- package/packages/core/src/index.ts +220 -0
- package/packages/core/src/ref-map.ts +107 -0
- package/packages/core/src/snapshot.ts +873 -0
- package/packages/core/src/types.ts +536 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/extension/README.md +129 -0
- package/packages/extension/package.json +43 -0
- package/packages/extension/src/background.ts +888 -0
- package/packages/extension/src/content.ts +172 -0
- package/packages/extension/src/index.ts +579 -0
- package/packages/extension/src/session-manager.ts +385 -0
- package/packages/extension/src/session-types.ts +144 -0
- package/packages/extension/src/types.ts +162 -0
- package/packages/extension/tsconfig.json +28 -0
- package/src/index.ts +64 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @btcp/cli - Output formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats command results for terminal display.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CommandResult, FormattedOutput } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format a command result for display
|
|
11
|
+
*/
|
|
12
|
+
export function formatResult(result: CommandResult): FormattedOutput {
|
|
13
|
+
if (result.success) {
|
|
14
|
+
if (result.message) {
|
|
15
|
+
return { type: 'success', content: `✓ ${result.message}` };
|
|
16
|
+
}
|
|
17
|
+
if (result.data !== undefined) {
|
|
18
|
+
return { type: 'data', content: formatData(result.data) };
|
|
19
|
+
}
|
|
20
|
+
return { type: 'success', content: '✓ Done' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
type: 'error',
|
|
25
|
+
content: `✗ Error: ${result.error || 'Unknown error'}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format data for display
|
|
31
|
+
*/
|
|
32
|
+
export function formatData(data: unknown): string {
|
|
33
|
+
if (data === null || data === undefined) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof data === 'string') {
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
42
|
+
return String(data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(data)) {
|
|
46
|
+
return formatArray(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof data === 'object') {
|
|
50
|
+
return formatObject(data as Record<string, unknown>);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return String(data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format an array for display
|
|
58
|
+
*/
|
|
59
|
+
function formatArray(arr: unknown[]): string {
|
|
60
|
+
if (arr.length === 0) return '(empty)';
|
|
61
|
+
|
|
62
|
+
// Check if it's a simple array of primitives
|
|
63
|
+
if (arr.every((item) => typeof item !== 'object' || item === null)) {
|
|
64
|
+
return arr.map((item) => String(item)).join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Complex array - format each item
|
|
68
|
+
return arr.map((item, i) => `[${i}] ${formatData(item)}`).join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format an object for display
|
|
73
|
+
*/
|
|
74
|
+
function formatObject(obj: Record<string, unknown>): string {
|
|
75
|
+
const entries = Object.entries(obj);
|
|
76
|
+
if (entries.length === 0) return '{}';
|
|
77
|
+
|
|
78
|
+
// Check for specific known formats
|
|
79
|
+
if ('tabs' in obj && Array.isArray(obj.tabs)) {
|
|
80
|
+
// Tab list
|
|
81
|
+
return formatTabList(obj.tabs as TabInfo[]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Generic object formatting
|
|
85
|
+
return entries
|
|
86
|
+
.map(([key, value]) => {
|
|
87
|
+
const formattedValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
88
|
+
return `${key}: ${formattedValue}`;
|
|
89
|
+
})
|
|
90
|
+
.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface TabInfo {
|
|
94
|
+
id: number;
|
|
95
|
+
url?: string;
|
|
96
|
+
title?: string;
|
|
97
|
+
active: boolean;
|
|
98
|
+
index: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format tab list for display
|
|
103
|
+
*/
|
|
104
|
+
function formatTabList(tabs: TabInfo[]): string {
|
|
105
|
+
if (tabs.length === 0) return 'No tabs';
|
|
106
|
+
|
|
107
|
+
return tabs
|
|
108
|
+
.map((tab) => {
|
|
109
|
+
const active = tab.active ? '* ' : ' ';
|
|
110
|
+
const title = tab.title || '(untitled)';
|
|
111
|
+
const url = tab.url || '';
|
|
112
|
+
return `${active}[${tab.id}] ${title}\n ${url}`;
|
|
113
|
+
})
|
|
114
|
+
.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Format help text
|
|
119
|
+
*/
|
|
120
|
+
export function formatHelp(commands: { name: string; description: string; usage: string }[]): string {
|
|
121
|
+
const maxNameLen = Math.max(...commands.map((c) => c.name.length));
|
|
122
|
+
|
|
123
|
+
return commands
|
|
124
|
+
.map((cmd) => {
|
|
125
|
+
const padding = ' '.repeat(maxNameLen - cmd.name.length + 2);
|
|
126
|
+
return ` ${cmd.name}${padding}${cmd.description}`;
|
|
127
|
+
})
|
|
128
|
+
.join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format command help with detailed information
|
|
133
|
+
*/
|
|
134
|
+
export function formatCommandHelp(command: {
|
|
135
|
+
name: string;
|
|
136
|
+
description: string;
|
|
137
|
+
usage: string;
|
|
138
|
+
examples?: string[];
|
|
139
|
+
}): string {
|
|
140
|
+
let output = `${command.name}\n`;
|
|
141
|
+
output += '─'.repeat(30) + '\n\n';
|
|
142
|
+
output += `${command.description}\n\n`;
|
|
143
|
+
output += `Usage:\n ${command.usage}\n`;
|
|
144
|
+
|
|
145
|
+
if (command.examples && command.examples.length > 0) {
|
|
146
|
+
output += '\nExamples:\n';
|
|
147
|
+
output += command.examples.map((ex) => ` ${ex}`).join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add selector help for commands that use selectors
|
|
151
|
+
const selectorCommands = ['click', 'type', 'fill', 'clear', 'hover', 'focus', 'check', 'uncheck', 'select', 'scroll', 'wait', 'text'];
|
|
152
|
+
if (selectorCommands.includes(command.name)) {
|
|
153
|
+
output += '\n\nSelectors:\n';
|
|
154
|
+
output += ' @ref:N Element reference from snapshot\n';
|
|
155
|
+
output += ' #id CSS ID selector\n';
|
|
156
|
+
output += ' .class CSS class selector\n';
|
|
157
|
+
output += ' tag HTML tag name';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format screenshot as base64 data URL info
|
|
165
|
+
*/
|
|
166
|
+
export function formatScreenshot(dataUrl: string): string {
|
|
167
|
+
// Extract format and size from data URL
|
|
168
|
+
const match = dataUrl.match(/^data:image\/(\w+);base64,/);
|
|
169
|
+
if (match) {
|
|
170
|
+
const format = match[1];
|
|
171
|
+
const base64 = dataUrl.slice(match[0].length);
|
|
172
|
+
const sizeBytes = Math.ceil((base64.length * 3) / 4);
|
|
173
|
+
const sizeKb = (sizeBytes / 1024).toFixed(1);
|
|
174
|
+
return `Screenshot captured (${format}, ${sizeKb} KB)`;
|
|
175
|
+
}
|
|
176
|
+
return 'Screenshot captured';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Format error with suggestions
|
|
181
|
+
*/
|
|
182
|
+
export function formatErrorWithSuggestions(
|
|
183
|
+
error: string,
|
|
184
|
+
suggestions?: string[]
|
|
185
|
+
): string {
|
|
186
|
+
let output = `✗ Error: ${error}`;
|
|
187
|
+
|
|
188
|
+
if (suggestions && suggestions.length > 0) {
|
|
189
|
+
output += '\n\nSuggestions:';
|
|
190
|
+
for (const suggestion of suggestions) {
|
|
191
|
+
output += `\n • ${suggestion}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Format success message with next steps
|
|
200
|
+
*/
|
|
201
|
+
export function formatSuccessWithNextSteps(
|
|
202
|
+
message: string,
|
|
203
|
+
nextSteps?: string[]
|
|
204
|
+
): string {
|
|
205
|
+
let output = `✓ ${message}`;
|
|
206
|
+
|
|
207
|
+
if (nextSteps && nextSteps.length > 0) {
|
|
208
|
+
output += '\n\nNext steps:';
|
|
209
|
+
for (const step of nextSteps) {
|
|
210
|
+
output += `\n ${step}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @btcp/cli
|
|
3
|
+
*
|
|
4
|
+
* Browser-based CLI for browser automation.
|
|
5
|
+
* Commands are sent directly from Chrome extension.
|
|
6
|
+
*
|
|
7
|
+
* @example Single command:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createCLI } from '@btcp/cli';
|
|
10
|
+
* import { createClient } from '@btcp/extension';
|
|
11
|
+
*
|
|
12
|
+
* const client = createClient();
|
|
13
|
+
* const cli = createCLI(client);
|
|
14
|
+
*
|
|
15
|
+
* await cli.execute('goto https://example.com');
|
|
16
|
+
* await cli.execute('snapshot');
|
|
17
|
+
* await cli.execute('click @ref:1');
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example Multiple commands (split by \n):
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Execute multiple commands - stops on first error
|
|
23
|
+
* await cli.execute(`
|
|
24
|
+
* goto https://example.com
|
|
25
|
+
* snapshot
|
|
26
|
+
* click @ref:1
|
|
27
|
+
* fill @ref:2 "hello"
|
|
28
|
+
* `);
|
|
29
|
+
*
|
|
30
|
+
* // Or use executeAll for detailed results
|
|
31
|
+
* const batch = await cli.executeAll(`
|
|
32
|
+
* goto https://example.com
|
|
33
|
+
* snapshot
|
|
34
|
+
* click @ref:1
|
|
35
|
+
* `);
|
|
36
|
+
* console.log(batch.allSucceeded, batch.results);
|
|
37
|
+
*
|
|
38
|
+
* // Continue on error
|
|
39
|
+
* const batch2 = await cli.executeAll(commands, { continueOnError: true });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example With terminal UI:
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { createCLI, Terminal } from '@btcp/cli';
|
|
45
|
+
* import { createClient } from '@btcp/extension';
|
|
46
|
+
*
|
|
47
|
+
* const client = createClient();
|
|
48
|
+
* const cli = createCLI(client);
|
|
49
|
+
*
|
|
50
|
+
* const terminal = new Terminal(document.getElementById('terminal')!, {
|
|
51
|
+
* onExecute: (input) => cli.execute(input),
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import type { CLI, CommandClient, CommandResult, CommandHandler, BatchResult } from './types.js';
|
|
57
|
+
import { parseCommand } from './parser.js';
|
|
58
|
+
import { executeCommand } from './executor.js';
|
|
59
|
+
import { formatResult, formatHelp, formatCommandHelp } from './formatter.js';
|
|
60
|
+
import { getAllCommands, getCommand } from './commands/index.js';
|
|
61
|
+
import { CLIError } from './errors.js';
|
|
62
|
+
|
|
63
|
+
// Re-export types
|
|
64
|
+
export type {
|
|
65
|
+
CLI,
|
|
66
|
+
CommandClient,
|
|
67
|
+
CommandResult,
|
|
68
|
+
CommandHandler,
|
|
69
|
+
BatchResult,
|
|
70
|
+
ParsedCommand,
|
|
71
|
+
FormattedOutput,
|
|
72
|
+
HistoryEntry,
|
|
73
|
+
TerminalConfig,
|
|
74
|
+
} from './types.js';
|
|
75
|
+
|
|
76
|
+
// Re-export utilities
|
|
77
|
+
export { parseCommand, getFlagString, getFlagNumber, getFlagBool } from './parser.js';
|
|
78
|
+
export { executeCommand } from './executor.js';
|
|
79
|
+
export {
|
|
80
|
+
formatResult,
|
|
81
|
+
formatHelp,
|
|
82
|
+
formatCommandHelp,
|
|
83
|
+
formatData,
|
|
84
|
+
formatScreenshot,
|
|
85
|
+
formatErrorWithSuggestions,
|
|
86
|
+
formatSuccessWithNextSteps,
|
|
87
|
+
} from './formatter.js';
|
|
88
|
+
export { commands, getCommand, getAllCommands } from './commands/index.js';
|
|
89
|
+
export {
|
|
90
|
+
CLIError,
|
|
91
|
+
CommandNotFoundError,
|
|
92
|
+
InvalidArgumentsError,
|
|
93
|
+
ParseError,
|
|
94
|
+
ExecutionError,
|
|
95
|
+
ElementNotFoundError,
|
|
96
|
+
NavigationError,
|
|
97
|
+
TimeoutError,
|
|
98
|
+
} from './errors.js';
|
|
99
|
+
export {
|
|
100
|
+
findSimilarCommands,
|
|
101
|
+
getContextualSuggestion,
|
|
102
|
+
getNextStepSuggestions,
|
|
103
|
+
commandCategories,
|
|
104
|
+
getCommandCategory,
|
|
105
|
+
workflowSuggestions,
|
|
106
|
+
} from './suggestions.js';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Execute a single command line
|
|
110
|
+
*/
|
|
111
|
+
async function executeSingle(client: CommandClient, line: string): Promise<CommandResult> {
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
|
|
114
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
115
|
+
// Empty line or comment
|
|
116
|
+
return { success: true, message: '' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const command = parseCommand(trimmed);
|
|
121
|
+
return await executeCommand(client, command);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof CLIError) {
|
|
124
|
+
// Use formatted error with suggestions
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: error.toFormattedString(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
return { success: false, error: message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a CLI instance
|
|
137
|
+
*/
|
|
138
|
+
export function createCLI(client: CommandClient): CLI {
|
|
139
|
+
return {
|
|
140
|
+
async execute(input: string): Promise<CommandResult> {
|
|
141
|
+
const trimmed = input.trim();
|
|
142
|
+
|
|
143
|
+
if (!trimmed) {
|
|
144
|
+
return { success: true, message: '' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if input contains multiple commands (split by \n)
|
|
148
|
+
const lines = trimmed.split('\n').filter((line) => {
|
|
149
|
+
const l = line.trim();
|
|
150
|
+
return l && !l.startsWith('#');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (lines.length === 0) {
|
|
154
|
+
return { success: true, message: '' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Single command - execute directly
|
|
158
|
+
if (lines.length === 1) {
|
|
159
|
+
return executeSingle(client, lines[0]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Multiple commands - execute sequentially, stop on first error
|
|
163
|
+
const results: CommandResult[] = [];
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const result = await executeSingle(client, line);
|
|
166
|
+
results.push(result);
|
|
167
|
+
|
|
168
|
+
if (!result.success) {
|
|
169
|
+
// Return aggregated result on failure
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: `Command failed at line ${results.length}: ${result.error}`,
|
|
173
|
+
data: { results, failedAt: results.length - 1 },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// All commands succeeded - return last result with summary
|
|
179
|
+
const lastResult = results[results.length - 1];
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
message: lastResult.message || `Executed ${results.length} commands`,
|
|
183
|
+
data: lastResult.data ?? { results },
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async executeAll(
|
|
188
|
+
input: string,
|
|
189
|
+
options?: { continueOnError?: boolean }
|
|
190
|
+
): Promise<BatchResult> {
|
|
191
|
+
const trimmed = input.trim();
|
|
192
|
+
const continueOnError = options?.continueOnError ?? false;
|
|
193
|
+
|
|
194
|
+
if (!trimmed) {
|
|
195
|
+
return {
|
|
196
|
+
results: [],
|
|
197
|
+
allSucceeded: true,
|
|
198
|
+
firstFailedIndex: -1,
|
|
199
|
+
executed: 0,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Split by newlines, filter empty lines and comments
|
|
204
|
+
const lines = trimmed.split('\n').filter((line) => {
|
|
205
|
+
const l = line.trim();
|
|
206
|
+
return l && !l.startsWith('#');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const results: CommandResult[] = [];
|
|
210
|
+
let firstFailedIndex = -1;
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < lines.length; i++) {
|
|
213
|
+
const result = await executeSingle(client, lines[i]);
|
|
214
|
+
results.push(result);
|
|
215
|
+
|
|
216
|
+
if (!result.success && firstFailedIndex === -1) {
|
|
217
|
+
firstFailedIndex = i;
|
|
218
|
+
if (!continueOnError) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
results,
|
|
226
|
+
allSucceeded: firstFailedIndex === -1,
|
|
227
|
+
firstFailedIndex,
|
|
228
|
+
executed: results.length,
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
getCommands(): CommandHandler[] {
|
|
233
|
+
return getAllCommands();
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
getHelp(commandName?: string): string {
|
|
237
|
+
if (commandName) {
|
|
238
|
+
const cmd = getCommand(commandName);
|
|
239
|
+
if (!cmd) {
|
|
240
|
+
return `Unknown command: ${commandName}`;
|
|
241
|
+
}
|
|
242
|
+
return formatCommandHelp(cmd);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const commandList = getAllCommands().map((cmd) => ({
|
|
246
|
+
name: cmd.name,
|
|
247
|
+
description: cmd.description,
|
|
248
|
+
usage: cmd.usage,
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
return `BTCP Browser CLI - Available Commands\n\n${formatHelp(commandList)}\n\nType "help <command>" for more info`;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Re-export terminal (will be created next)
|
|
257
|
+
export { Terminal, type TerminalOptions } from './terminal/index.js';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @btcp/cli - Command parser
|
|
3
|
+
*
|
|
4
|
+
* Parses CLI input strings into structured commands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ParsedCommand } from './types.js';
|
|
8
|
+
import { ParseError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a CLI input string into a structured command
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* parseCommand('goto https://example.com')
|
|
15
|
+
* // { name: 'goto', args: ['https://example.com'], flags: {}, raw: '...' }
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* parseCommand('click @ref:5 --wait 1000')
|
|
19
|
+
* // { name: 'click', args: ['@ref:5'], flags: { wait: '1000' }, raw: '...' }
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* parseCommand('type @ref:1 "hello world"')
|
|
23
|
+
* // { name: 'type', args: ['@ref:1', 'hello world'], flags: {}, raw: '...' }
|
|
24
|
+
*/
|
|
25
|
+
export function parseCommand(input: string): ParsedCommand {
|
|
26
|
+
const raw = input;
|
|
27
|
+
const trimmed = input.trim();
|
|
28
|
+
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
throw new ParseError('Empty command');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tokens = tokenize(trimmed);
|
|
34
|
+
|
|
35
|
+
if (tokens.length === 0) {
|
|
36
|
+
throw new ParseError('Empty command');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const name = tokens[0].toLowerCase();
|
|
40
|
+
const { args, flags } = parseArgs(tokens.slice(1));
|
|
41
|
+
|
|
42
|
+
return { name, args, flags, raw };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Tokenize input string, handling quoted strings
|
|
47
|
+
*/
|
|
48
|
+
function tokenize(input: string): string[] {
|
|
49
|
+
const tokens: string[] = [];
|
|
50
|
+
let current = '';
|
|
51
|
+
let inQuote: string | null = null;
|
|
52
|
+
let escapeNext = false;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < input.length; i++) {
|
|
55
|
+
const char = input[i];
|
|
56
|
+
|
|
57
|
+
if (escapeNext) {
|
|
58
|
+
current += char;
|
|
59
|
+
escapeNext = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (char === '\\') {
|
|
64
|
+
escapeNext = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (inQuote) {
|
|
69
|
+
if (char === inQuote) {
|
|
70
|
+
// End of quoted string
|
|
71
|
+
inQuote = null;
|
|
72
|
+
} else {
|
|
73
|
+
current += char;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (char === '"' || char === "'") {
|
|
79
|
+
// Start of quoted string
|
|
80
|
+
inQuote = char;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (char === ' ' || char === '\t') {
|
|
85
|
+
// Whitespace - end current token
|
|
86
|
+
if (current) {
|
|
87
|
+
tokens.push(current);
|
|
88
|
+
current = '';
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
current += char;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle unterminated quote
|
|
97
|
+
if (inQuote) {
|
|
98
|
+
throw new ParseError(`Unterminated ${inQuote === '"' ? 'double' : 'single'} quote`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add final token
|
|
102
|
+
if (current) {
|
|
103
|
+
tokens.push(current);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return tokens;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse tokens into args and flags
|
|
111
|
+
*/
|
|
112
|
+
function parseArgs(tokens: string[]): { args: string[]; flags: Record<string, string | boolean> } {
|
|
113
|
+
const args: string[] = [];
|
|
114
|
+
const flags: Record<string, string | boolean> = {};
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
117
|
+
const token = tokens[i];
|
|
118
|
+
|
|
119
|
+
if (token.startsWith('--')) {
|
|
120
|
+
// Long flag
|
|
121
|
+
const flagContent = token.slice(2);
|
|
122
|
+
|
|
123
|
+
if (flagContent.includes('=')) {
|
|
124
|
+
// --flag=value format
|
|
125
|
+
const [key, ...valueParts] = flagContent.split('=');
|
|
126
|
+
flags[key] = valueParts.join('=');
|
|
127
|
+
} else if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
|
|
128
|
+
// --flag value format
|
|
129
|
+
flags[flagContent] = tokens[++i];
|
|
130
|
+
} else {
|
|
131
|
+
// Boolean flag
|
|
132
|
+
flags[flagContent] = true;
|
|
133
|
+
}
|
|
134
|
+
} else if (token.startsWith('-') && token.length === 2) {
|
|
135
|
+
// Short flag (e.g., -v)
|
|
136
|
+
const flagChar = token[1];
|
|
137
|
+
|
|
138
|
+
if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
|
|
139
|
+
// -f value format
|
|
140
|
+
flags[flagChar] = tokens[++i];
|
|
141
|
+
} else {
|
|
142
|
+
// Boolean flag
|
|
143
|
+
flags[flagChar] = true;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Positional argument
|
|
147
|
+
args.push(token);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { args, flags };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get flag value as string
|
|
156
|
+
*/
|
|
157
|
+
export function getFlagString(
|
|
158
|
+
flags: Record<string, string | boolean>,
|
|
159
|
+
name: string,
|
|
160
|
+
defaultValue?: string
|
|
161
|
+
): string | undefined {
|
|
162
|
+
const value = flags[name];
|
|
163
|
+
if (value === undefined) return defaultValue;
|
|
164
|
+
if (typeof value === 'boolean') return defaultValue;
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get flag value as number
|
|
170
|
+
*/
|
|
171
|
+
export function getFlagNumber(
|
|
172
|
+
flags: Record<string, string | boolean>,
|
|
173
|
+
name: string,
|
|
174
|
+
defaultValue?: number
|
|
175
|
+
): number | undefined {
|
|
176
|
+
const value = flags[name];
|
|
177
|
+
if (value === undefined) return defaultValue;
|
|
178
|
+
if (typeof value === 'boolean') return defaultValue;
|
|
179
|
+
const num = Number(value);
|
|
180
|
+
return isNaN(num) ? defaultValue : num;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get flag value as boolean
|
|
185
|
+
*/
|
|
186
|
+
export function getFlagBool(
|
|
187
|
+
flags: Record<string, string | boolean>,
|
|
188
|
+
name: string,
|
|
189
|
+
defaultValue = false
|
|
190
|
+
): boolean {
|
|
191
|
+
const value = flags[name];
|
|
192
|
+
if (value === undefined) return defaultValue;
|
|
193
|
+
if (typeof value === 'boolean') return value;
|
|
194
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
195
|
+
}
|