btcp-browser-agent 0.1.0 → 0.1.1

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 (136) hide show
  1. package/package.json +8 -9
  2. package/packages/core/dist/actions.d.ts +97 -0
  3. package/packages/core/dist/actions.js +940 -0
  4. package/packages/core/dist/errors.d.ts +138 -0
  5. package/packages/core/dist/errors.js +157 -0
  6. package/packages/core/dist/index.d.ts +120 -0
  7. package/packages/core/dist/index.js +134 -0
  8. package/packages/core/dist/ref-map.d.ts +16 -0
  9. package/packages/core/dist/ref-map.js +91 -0
  10. package/packages/core/dist/snapshot.d.ts +37 -0
  11. package/packages/core/dist/snapshot.js +751 -0
  12. package/packages/core/dist/types.d.ts +396 -0
  13. package/packages/core/dist/types.js +7 -0
  14. package/packages/extension/dist/background.d.ts +227 -0
  15. package/packages/extension/dist/background.js +737 -0
  16. package/packages/extension/dist/content.d.ts +18 -0
  17. package/packages/extension/dist/content.js +149 -0
  18. package/packages/extension/dist/index.d.ts +228 -0
  19. package/packages/extension/dist/index.js +350 -0
  20. package/packages/extension/dist/session-manager.d.ts +87 -0
  21. package/packages/extension/dist/session-manager.js +322 -0
  22. package/packages/extension/{src/session-types.ts → dist/session-types.d.ts} +113 -144
  23. package/packages/extension/dist/session-types.js +5 -0
  24. package/packages/extension/dist/types.d.ts +88 -0
  25. package/packages/extension/dist/types.js +7 -0
  26. package/CLAUDE.md +0 -230
  27. package/SKILL.md +0 -143
  28. package/SNAPSHOT_IMPROVEMENTS.md +0 -302
  29. package/USAGE.md +0 -146
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js.map +0 -1
  32. package/docs/browser-cli-design.md +0 -500
  33. package/examples/chrome-extension/CHANGELOG.md +0 -210
  34. package/examples/chrome-extension/DEBUG.md +0 -231
  35. package/examples/chrome-extension/ERROR_FIXED.md +0 -147
  36. package/examples/chrome-extension/QUICK_TEST.md +0 -189
  37. package/examples/chrome-extension/README.md +0 -149
  38. package/examples/chrome-extension/SESSION_ONLY_MODE.md +0 -305
  39. package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +0 -97
  40. package/examples/chrome-extension/build.js +0 -43
  41. package/examples/chrome-extension/manifest.json +0 -37
  42. package/examples/chrome-extension/package-lock.json +0 -1063
  43. package/examples/chrome-extension/package.json +0 -21
  44. package/examples/chrome-extension/popup.html +0 -195
  45. package/examples/chrome-extension/src/background.ts +0 -12
  46. package/examples/chrome-extension/src/content.ts +0 -7
  47. package/examples/chrome-extension/src/popup.ts +0 -303
  48. package/examples/chrome-extension/src/scenario-google-github.ts +0 -389
  49. package/examples/chrome-extension/test-page.html +0 -127
  50. package/examples/chrome-extension/tests/README.md +0 -206
  51. package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +0 -380
  52. package/examples/chrome-extension/tsconfig.json +0 -14
  53. package/examples/snapshots/README.md +0 -207
  54. package/examples/snapshots/amazon-com-detail.html +0 -9528
  55. package/examples/snapshots/amazon-com-detail.snapshot.txt +0 -997
  56. package/examples/snapshots/convert-snapshots.ts +0 -97
  57. package/examples/snapshots/edition-cnn-com.html +0 -13292
  58. package/examples/snapshots/edition-cnn-com.snapshot.txt +0 -562
  59. package/examples/snapshots/github-com-microsoft-vscode.html +0 -2916
  60. package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +0 -455
  61. package/examples/snapshots/google-search.html +0 -20012
  62. package/examples/snapshots/google-search.snapshot.txt +0 -195
  63. package/examples/snapshots/metadata.json +0 -86
  64. package/examples/snapshots/npr-org-templates.html +0 -2031
  65. package/examples/snapshots/npr-org-templates.snapshot.txt +0 -224
  66. package/examples/snapshots/stackoverflow-com.html +0 -5216
  67. package/examples/snapshots/stackoverflow-com.snapshot.txt +0 -2404
  68. package/examples/snapshots/test-all-mode.html +0 -46
  69. package/examples/snapshots/test-all-mode.snapshot.txt +0 -5
  70. package/examples/snapshots/validate.test.ts +0 -296
  71. package/packages/cli/package.json +0 -42
  72. package/packages/cli/src/__tests__/cli.test.ts +0 -434
  73. package/packages/cli/src/__tests__/errors.test.ts +0 -226
  74. package/packages/cli/src/__tests__/executor.test.ts +0 -275
  75. package/packages/cli/src/__tests__/formatter.test.ts +0 -260
  76. package/packages/cli/src/__tests__/parser.test.ts +0 -288
  77. package/packages/cli/src/__tests__/suggestions.test.ts +0 -255
  78. package/packages/cli/src/commands/back.ts +0 -22
  79. package/packages/cli/src/commands/check.ts +0 -33
  80. package/packages/cli/src/commands/clear.ts +0 -33
  81. package/packages/cli/src/commands/click.ts +0 -32
  82. package/packages/cli/src/commands/closetab.ts +0 -31
  83. package/packages/cli/src/commands/eval.ts +0 -41
  84. package/packages/cli/src/commands/fill.ts +0 -30
  85. package/packages/cli/src/commands/focus.ts +0 -33
  86. package/packages/cli/src/commands/forward.ts +0 -22
  87. package/packages/cli/src/commands/goto.ts +0 -34
  88. package/packages/cli/src/commands/help.ts +0 -162
  89. package/packages/cli/src/commands/hover.ts +0 -34
  90. package/packages/cli/src/commands/index.ts +0 -129
  91. package/packages/cli/src/commands/newtab.ts +0 -35
  92. package/packages/cli/src/commands/press.ts +0 -40
  93. package/packages/cli/src/commands/reload.ts +0 -25
  94. package/packages/cli/src/commands/screenshot.ts +0 -27
  95. package/packages/cli/src/commands/scroll.ts +0 -64
  96. package/packages/cli/src/commands/select.ts +0 -35
  97. package/packages/cli/src/commands/snapshot.ts +0 -21
  98. package/packages/cli/src/commands/tab.ts +0 -32
  99. package/packages/cli/src/commands/tabs.ts +0 -26
  100. package/packages/cli/src/commands/text.ts +0 -27
  101. package/packages/cli/src/commands/title.ts +0 -17
  102. package/packages/cli/src/commands/type.ts +0 -38
  103. package/packages/cli/src/commands/uncheck.ts +0 -33
  104. package/packages/cli/src/commands/url.ts +0 -17
  105. package/packages/cli/src/commands/wait.ts +0 -54
  106. package/packages/cli/src/errors.ts +0 -164
  107. package/packages/cli/src/executor.ts +0 -68
  108. package/packages/cli/src/formatter.ts +0 -215
  109. package/packages/cli/src/index.ts +0 -257
  110. package/packages/cli/src/parser.ts +0 -195
  111. package/packages/cli/src/suggestions.ts +0 -207
  112. package/packages/cli/src/terminal/Terminal.ts +0 -365
  113. package/packages/cli/src/terminal/index.ts +0 -5
  114. package/packages/cli/src/types.ts +0 -155
  115. package/packages/cli/tsconfig.json +0 -20
  116. package/packages/core/package.json +0 -35
  117. package/packages/core/src/actions.ts +0 -1210
  118. package/packages/core/src/errors.ts +0 -296
  119. package/packages/core/src/index.test.ts +0 -638
  120. package/packages/core/src/index.ts +0 -220
  121. package/packages/core/src/ref-map.ts +0 -107
  122. package/packages/core/src/snapshot.ts +0 -873
  123. package/packages/core/src/types.ts +0 -536
  124. package/packages/core/tsconfig.json +0 -23
  125. package/packages/extension/README.md +0 -129
  126. package/packages/extension/package.json +0 -43
  127. package/packages/extension/src/background.ts +0 -888
  128. package/packages/extension/src/content.ts +0 -172
  129. package/packages/extension/src/index.ts +0 -579
  130. package/packages/extension/src/session-manager.ts +0 -385
  131. package/packages/extension/src/types.ts +0 -162
  132. package/packages/extension/tsconfig.json +0 -28
  133. package/src/index.ts +0 -64
  134. package/tsconfig.build.json +0 -12
  135. package/tsconfig.json +0 -26
  136. package/vitest.config.ts +0 -13
@@ -1,215 +0,0 @@
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
- }
@@ -1,257 +0,0 @@
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';
@@ -1,195 +0,0 @@
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
- }