@crouton-kit/capture 1.3.2 → 1.3.4

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.
@@ -0,0 +1,101 @@
1
+ import { type ParsedArgs } from './types.js';
2
+ import { getActiveSession } from '../session-context.js';
3
+
4
+ export function parseCliArgs(argv: string[]): ParsedArgs {
5
+ const command = argv[0] ?? '';
6
+ const positional: string[] = [];
7
+ const parsed: Partial<ParsedArgs> = { command, positional };
8
+
9
+ for (let i = 1; i < argv.length; i++) {
10
+ const arg = argv[i];
11
+ const next = argv[i + 1];
12
+
13
+ if (arg === '--port' && next) {
14
+ parsed.port = parseInt(next, 10);
15
+ i++;
16
+ } else if (arg === '--out' && next) {
17
+ parsed.out = next;
18
+ i++;
19
+ } else if (arg === '--json') {
20
+ parsed.json = true;
21
+ } else if (arg === '--interactive') {
22
+ parsed.interactive = true;
23
+ } else if (arg === '--har-out' && next) {
24
+ parsed.harOut = next;
25
+ i++;
26
+ } else if (arg === '--record') {
27
+ parsed.record = true;
28
+ } else if (arg === '--duration' && next) {
29
+ parsed.duration = parseInt(next, 10);
30
+ i++;
31
+ } else if (arg === '--settle' && next) {
32
+ parsed.settle = parseInt(next, 10);
33
+ i++;
34
+ } else if (arg === '--file' && next) {
35
+ parsed.file = next;
36
+ i++;
37
+ } else if (arg === '--nested') {
38
+ parsed.nested = true;
39
+ } else if (arg === '--har' && next) {
40
+ parsed.har = next;
41
+ i++;
42
+ } else if (arg === '--new') {
43
+ parsed.new = true;
44
+ } else if (arg === '--target' && next) {
45
+ parsed.target = next;
46
+ i++;
47
+ } else if (arg === '--role' && next) {
48
+ parsed.role = next;
49
+ i++;
50
+ } else if (arg === '--into' && next) {
51
+ parsed.into = next;
52
+ i++;
53
+ } else if (arg === '--no-screenshot') {
54
+ parsed.noScreenshot = true;
55
+ } else if (arg === '--viewport' && next) {
56
+ parsed.viewport = next;
57
+ i++;
58
+ } else if (arg === '--full-page') {
59
+ parsed.fullPage = true;
60
+ } else if (arg === '--height' && next) {
61
+ parsed.height = parseInt(next, 10);
62
+ i++;
63
+ } else if (arg === '--filter-url' && next) {
64
+ parsed.filterUrl = next;
65
+ i++;
66
+ } else if (arg === '--filter-status' && next) {
67
+ parsed.filterStatus = next;
68
+ i++;
69
+ } else if (arg === '--filter-method' && next) {
70
+ parsed.filterMethod = next.toUpperCase();
71
+ i++;
72
+ } else if (arg === '--limit' && next) {
73
+ parsed.limit = parseInt(next, 10);
74
+ i++;
75
+ } else if (arg === '--help' || arg === '-h') {
76
+ parsed.help = true;
77
+ } else if (arg.startsWith('--')) {
78
+ console.error(`Unknown flag: ${arg}`);
79
+ process.exit(1);
80
+ } else {
81
+ positional.push(arg);
82
+ }
83
+ }
84
+
85
+ // Fill gaps from environment variables (set by pipeline orchestrators)
86
+ if (!parsed.target && process.env.CDP_TARGET) {
87
+ parsed.target = process.env.CDP_TARGET;
88
+ }
89
+ if (!parsed.har && process.env.CDP_HAR_ID) {
90
+ parsed.har = process.env.CDP_HAR_ID;
91
+ }
92
+
93
+ // Fill remaining gaps from active session context (explicit flags always win)
94
+ const session = getActiveSession();
95
+ if (session) {
96
+ if (!parsed.har && session.harId) parsed.har = session.harId;
97
+ if (!parsed.target && session.targetId) parsed.target = session.targetId;
98
+ }
99
+
100
+ return parsed as ParsedArgs;
101
+ }
@@ -0,0 +1,97 @@
1
+ import WebSocket from 'ws';
2
+
3
+ export class CDPClient {
4
+ private ws: WebSocket;
5
+ private messageId = 0;
6
+ private pendingRequests = new Map<
7
+ number,
8
+ { resolve: (v: unknown) => void; reject: (e: Error) => void }
9
+ >();
10
+ private pendingTimeouts = new Map<number, NodeJS.Timeout>();
11
+ private eventHandlers = new Map<string, Array<(params: unknown) => void>>();
12
+ private ready: Promise<void>;
13
+
14
+ constructor(wsUrl: string, connectTimeout = 5000) {
15
+ this.ws = new WebSocket(wsUrl);
16
+ this.ready = new Promise((resolve, reject) => {
17
+ const timeout = setTimeout(() => {
18
+ reject(new Error(`WebSocket connection timeout (${connectTimeout}ms)`));
19
+ }, connectTimeout);
20
+
21
+ this.ws.on('open', () => {
22
+ clearTimeout(timeout);
23
+ resolve();
24
+ });
25
+ this.ws.on('error', (err) => {
26
+ clearTimeout(timeout);
27
+ reject(err);
28
+ });
29
+ });
30
+
31
+ this.ws.on('message', (data: Buffer) => {
32
+ const msg = JSON.parse(data.toString());
33
+ if (msg.id !== undefined) {
34
+ const pending = this.pendingRequests.get(msg.id);
35
+ if (pending) {
36
+ this.pendingRequests.delete(msg.id);
37
+ const timer = this.pendingTimeouts.get(msg.id);
38
+ if (timer) {
39
+ clearTimeout(timer);
40
+ this.pendingTimeouts.delete(msg.id);
41
+ }
42
+ if (msg.error) {
43
+ pending.reject(new Error(msg.error.message));
44
+ } else {
45
+ pending.resolve(msg.result);
46
+ }
47
+ }
48
+ } else if (msg.method) {
49
+ const handlers = this.eventHandlers.get(msg.method);
50
+ handlers?.forEach((h) => h(msg.params));
51
+ }
52
+ });
53
+ }
54
+
55
+ async waitReady(): Promise<void> {
56
+ await this.ready;
57
+ }
58
+
59
+ async send(
60
+ method: string,
61
+ params: Record<string, unknown> = {},
62
+ timeout = 60000,
63
+ ): Promise<unknown> {
64
+ await this.ready;
65
+ const id = ++this.messageId;
66
+
67
+ return new Promise((resolve, reject) => {
68
+ this.pendingRequests.set(id, { resolve, reject });
69
+ this.ws.send(JSON.stringify({ id, method, params }));
70
+
71
+ const timer = setTimeout(() => {
72
+ if (this.pendingRequests.has(id)) {
73
+ this.pendingRequests.delete(id);
74
+ this.pendingTimeouts.delete(id);
75
+ reject(new Error(`CDP request timeout (${timeout}ms): ${method}`));
76
+ }
77
+ }, timeout);
78
+ this.pendingTimeouts.set(id, timer);
79
+ });
80
+ }
81
+
82
+ on(event: string, handler: (params: unknown) => void): void {
83
+ if (!this.eventHandlers.has(event)) {
84
+ this.eventHandlers.set(event, []);
85
+ }
86
+ this.eventHandlers.get(event)!.push(handler);
87
+ }
88
+
89
+ close(): void {
90
+ // Clear all pending timeouts to allow process to exit
91
+ for (const timer of this.pendingTimeouts.values()) {
92
+ clearTimeout(timer);
93
+ }
94
+ this.pendingTimeouts.clear();
95
+ this.ws.close();
96
+ }
97
+ }
@@ -0,0 +1,84 @@
1
+ import * as fs from 'fs';
2
+ import { withConnection } from '../connection.js';
3
+ import { HARRecorder } from '../har-recorder.js';
4
+ import { writeHarAndPrintSummary } from '../har-output.js';
5
+ import { type ParsedArgs } from '../types.js';
6
+
7
+ export async function cmdExec(parsed: ParsedArgs, _args: string[]): Promise<void> {
8
+ if (parsed.help) {
9
+ console.log(
10
+ 'Usage: capture exec <code> [--target <id>] [--record] [--file <path>] [--har <id>]\n\n' +
11
+ 'Execute JavaScript in a browser tab. Supports await (wrapped in async IIFE).\n\n' +
12
+ 'Examples:\n' +
13
+ ' capture exec "document.title" --target <id>\n' +
14
+ ' capture exec "await fetch(\'/api/data\').then(r=>r.json())" --target <id>\n' +
15
+ ' capture exec "document.querySelector(\'.btn\').click()" --target <id>\n' +
16
+ ' capture exec --file /tmp/scrape.js --target <id> --record',
17
+ );
18
+ process.exit(0);
19
+ }
20
+ let code: string;
21
+ if (parsed.file) {
22
+ code = fs.readFileSync(parsed.file, 'utf-8');
23
+ } else {
24
+ code = parsed.positional[0];
25
+ if (!code) {
26
+ console.error(
27
+ 'ERROR: Missing code to execute.\n\n' +
28
+ 'Usage: capture exec <code> [--target <id>] [--record] [--file <path>]\n\n' +
29
+ 'Examples:\n' +
30
+ ' capture exec "document.title" --target <id>\n' +
31
+ ' capture exec "document.querySelector(\'.btn\').click()" --target <id>\n' +
32
+ ' capture exec --file /tmp/scrape.js --target <id> --record',
33
+ );
34
+ process.exit(1);
35
+ }
36
+ }
37
+
38
+ await withConnection(
39
+ parsed,
40
+ async (client) => {
41
+ await client.send('Emulation.setFocusEmulationEnabled', { enabled: true });
42
+
43
+ // Standalone HAR recording (--record flag writes its own file)
44
+ let standaloneRecorder: HARRecorder | undefined;
45
+ if (parsed.record) {
46
+ standaloneRecorder = new HARRecorder(client);
47
+ await standaloneRecorder.start();
48
+ }
49
+
50
+ // Smart wrapping: only use async IIFE when code contains `await`.
51
+ // Without wrapping, Runtime.evaluate returns the completion value
52
+ // of the last expression, handling multi-statement code naturally.
53
+ const needsAsyncWrap = /\bawait\b/.test(code);
54
+ const expression = needsAsyncWrap
55
+ ? `(async () => { ${code} })()`
56
+ : code;
57
+
58
+ const evalResult = (await client.send('Runtime.evaluate', {
59
+ expression,
60
+ awaitPromise: true,
61
+ returnByValue: true,
62
+ })) as {
63
+ result?: { value?: unknown };
64
+ exceptionDetails?: { exception?: { description?: string } };
65
+ };
66
+
67
+ if (standaloneRecorder) {
68
+ const har = await standaloneRecorder.finish();
69
+ writeHarAndPrintSummary(har, parsed.harOut);
70
+ }
71
+
72
+ if (evalResult.exceptionDetails) {
73
+ console.error(
74
+ `ERROR: ${evalResult.exceptionDetails.exception?.description ?? 'Unknown error'}`,
75
+ );
76
+ process.exit(1);
77
+ }
78
+
79
+ console.log(JSON.stringify(evalResult.result?.value));
80
+ },
81
+ { settle: 3000 },
82
+ );
83
+ return;
84
+ }
@@ -0,0 +1,128 @@
1
+ import {
2
+ createHarRecording,
3
+ readHarRecording as readHarFile,
4
+ deleteHarRecording,
5
+ harFilePath,
6
+ } from '../../har-manager.js';
7
+ import { getActiveSession } from '../../session-context.js';
8
+ import { type ParsedArgs } from '../types.js';
9
+
10
+ export async function cmdHar(parsed: ParsedArgs, args: string[]): Promise<void> {
11
+ if (parsed.help) {
12
+ console.log(
13
+ 'Usage: capture har <create|read|delete>\n\n' +
14
+ ' create Create a new HAR recording (returns id)\n' +
15
+ ' read [id] Read accumulated HAR entries (id optional in active session)\n' +
16
+ ' delete <id> Delete a HAR recording\n\n' +
17
+ 'Filters (read):\n' +
18
+ ' --filter-url <pattern> Substring or regex match on URL\n' +
19
+ ' --filter-status <code> Status code, prefix (e.g. 4), or range (e.g. 400-499)\n' +
20
+ ' --filter-method <method> HTTP method (GET, POST, ...)\n' +
21
+ ' --limit <N> Return only the first N matching entries\n\n' +
22
+ 'Pass --har <id> to exec/navigate commands to append traffic to a recording.',
23
+ );
24
+ process.exit(0);
25
+ }
26
+ const subcommand = args[1];
27
+
28
+ if (subcommand === 'create') {
29
+ // Generate a short random ID
30
+ const { id, path: harPath } = createHarRecording();
31
+ console.log(JSON.stringify({ id, path: harPath }, null, 2));
32
+ console.error(
33
+ `\nPass --har ${id} to subsequent commands to record traffic.`,
34
+ );
35
+ return;
36
+ }
37
+
38
+ if (subcommand === 'read') {
39
+ // ID is optional — fall back to active session's harId
40
+ let id = args[2];
41
+ if (id && id.startsWith('--')) id = undefined as unknown as string;
42
+ if (!id) {
43
+ const session = getActiveSession();
44
+ if (session?.harId) {
45
+ id = session.harId;
46
+ } else {
47
+ console.error('Usage: capture har read [id]\n' +
48
+ ' ID is optional when a capture session is active.');
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ const har = readHarFile(id);
54
+ if (!har) {
55
+ console.error(`No HAR recording found for id: ${id}`);
56
+ process.exit(1);
57
+ }
58
+
59
+ let entries = har.log.entries;
60
+ const total = entries.length;
61
+ const filters: string[] = [];
62
+
63
+ if (parsed.filterUrl) {
64
+ const pattern = parsed.filterUrl;
65
+ let re: RegExp | null = null;
66
+ try { re = new RegExp(pattern, 'i'); } catch { re = null; }
67
+ entries = entries.filter((e) =>
68
+ re ? re.test(e.request.url) : e.request.url.toLowerCase().includes(pattern.toLowerCase()),
69
+ );
70
+ filters.push(`url~${pattern}`);
71
+ }
72
+ if (parsed.filterStatus) {
73
+ const s = parsed.filterStatus;
74
+ let matcher: (code: number) => boolean;
75
+ if (/^\d+-\d+$/.test(s)) {
76
+ const [lo, hi] = s.split('-').map((n) => parseInt(n, 10));
77
+ matcher = (c) => c >= lo && c <= hi;
78
+ } else if (/^\d+$/.test(s)) {
79
+ if (s.length < 3) {
80
+ matcher = (c) => String(c).startsWith(s);
81
+ } else {
82
+ const n = parseInt(s, 10);
83
+ matcher = (c) => c === n;
84
+ }
85
+ } else {
86
+ matcher = () => true;
87
+ }
88
+ entries = entries.filter((e) => matcher(e.response.status));
89
+ filters.push(`status=${s}`);
90
+ }
91
+ if (parsed.filterMethod) {
92
+ const m = parsed.filterMethod;
93
+ entries = entries.filter((e) => e.request.method.toUpperCase() === m);
94
+ filters.push(`method=${m}`);
95
+ }
96
+ if (typeof parsed.limit === 'number' && parsed.limit > 0) {
97
+ entries = entries.slice(0, parsed.limit);
98
+ filters.push(`limit=${parsed.limit}`);
99
+ }
100
+
101
+ const filtersLabel = filters.length > 0 ? ` [${filters.join(', ')}]` : '';
102
+ console.error(
103
+ `HAR: ${harFilePath(id)} (${entries.length}/${total} entries)${filtersLabel}`,
104
+ );
105
+ console.log(JSON.stringify({ log: { entries } }, null, 2));
106
+ return;
107
+ }
108
+
109
+ if (subcommand === 'delete') {
110
+ const id = args[2];
111
+ if (!id) {
112
+ console.error('Usage: capture har delete <id>');
113
+ process.exit(1);
114
+ }
115
+
116
+ deleteHarRecording(id);
117
+ console.log(JSON.stringify({ deleted: true, id }, null, 2));
118
+ return;
119
+ }
120
+
121
+ console.error(
122
+ 'Usage: capture har <create|read|delete>\n' +
123
+ ' create Create a new HAR recording (returns id)\n' +
124
+ ' read <id> Read accumulated HAR entries\n' +
125
+ ' delete <id> Delete a HAR recording',
126
+ );
127
+ process.exit(1);
128
+ }
@@ -0,0 +1,189 @@
1
+ import { detectCdpPortsAsync, detectCdpPort, getDefaultBrowserId } from '../detect.js';
2
+ import { listTargets, openTab } from '../targets.js';
3
+ import { navigateAndWait } from '../record.js';
4
+ import { CDPClient } from '../client.js';
5
+ import { updateActiveSession } from '../../session-context.js';
6
+ import { type ParsedArgs } from '../types.js';
7
+
8
+ export async function cmdDetect(parsed: ParsedArgs, _args: string[]): Promise<void> {
9
+ if (parsed.help) {
10
+ console.log(
11
+ 'Usage: capture detect\n\n' +
12
+ 'Detect CDP port (prioritizes default browser).\n' +
13
+ 'Returns JSON with port, app name, and all discovered endpoints.',
14
+ );
15
+ process.exit(0);
16
+ }
17
+ const endpoints = await detectCdpPortsAsync();
18
+ if (endpoints.length === 0) {
19
+ console.error(
20
+ 'ERROR: No browser with CDP found.\n\n' +
21
+ 'Fix: Start a browser with remote debugging enabled:\n' +
22
+ ' Arc: Already enabled by default\n' +
23
+ ' Chrome: --remote-debugging-port=9222\n' +
24
+ ' Electron apps expose CDP automatically',
25
+ );
26
+ process.exit(1);
27
+ }
28
+ const defaultBrowser = getDefaultBrowserId();
29
+ const preferred =
30
+ endpoints.find(
31
+ (p) => p.bundleId === defaultBrowser && !p.isElectron,
32
+ ) ??
33
+ endpoints.find((p) => !p.isElectron) ??
34
+ endpoints[0];
35
+ console.log(
36
+ JSON.stringify(
37
+ {
38
+ port: preferred.port,
39
+ app: preferred.app,
40
+ isElectron: preferred.isElectron,
41
+ all: endpoints.map((e) => ({
42
+ port: e.port,
43
+ app: e.app,
44
+ isElectron: e.isElectron,
45
+ })),
46
+ },
47
+ null,
48
+ 2,
49
+ ),
50
+ );
51
+ console.error(
52
+ `\nFound ${endpoints.length} CDP endpoint${endpoints.length > 1 ? 's' : ''}. Default: port ${preferred.port} (${preferred.app}).` +
53
+ `\n\nNext (most tasks): capture session start --url <your app url>` +
54
+ `\n (one-off): capture list --port ${preferred.port}`,
55
+ );
56
+ return;
57
+ }
58
+
59
+ export async function cmdList(parsed: ParsedArgs, _args: string[]): Promise<void> {
60
+ if (parsed.help) {
61
+ console.log(
62
+ 'Usage: capture list [--port <port>]\n\n' +
63
+ 'List all browser tabs across all CDP endpoints (or a specific port).\n' +
64
+ 'Returns JSON array of {id, title, url, port, app}.',
65
+ );
66
+ process.exit(0);
67
+ }
68
+ if (parsed.port) {
69
+ // Single port mode
70
+ const targets = await listTargets(parsed.port);
71
+ const pages = targets.filter((t) => t.type === 'page');
72
+ console.log(JSON.stringify(pages.map((t) => ({
73
+ id: t.id,
74
+ title: t.title,
75
+ url: t.url,
76
+ })), null, 2));
77
+ console.error(
78
+ `\n${pages.length} tab${pages.length !== 1 ? 's' : ''} on port ${parsed.port}.` +
79
+ `\n\nTarget a tab with: --target <id prefix> (first 8 chars sufficient)`,
80
+ );
81
+ } else {
82
+ // All endpoints mode
83
+ const endpoints = await detectCdpPortsAsync();
84
+ if (endpoints.length === 0) {
85
+ console.error('No CDP endpoints found.');
86
+ process.exit(1);
87
+ }
88
+ const allPages: { id: string; title: string; url: string; port: number; app: string }[] = [];
89
+ await Promise.all(endpoints.map(async (ep) => {
90
+ try {
91
+ const targets = await listTargets(ep.port);
92
+ const pages = targets.filter((t) => t.type === 'page');
93
+ for (const t of pages) {
94
+ allPages.push({ id: t.id, title: t.title, url: t.url, port: ep.port, app: ep.app });
95
+ }
96
+ } catch {
97
+ // Skip endpoints that fail to respond
98
+ }
99
+ }));
100
+ console.log(JSON.stringify(allPages, null, 2));
101
+ console.error(
102
+ `\n${allPages.length} tab${allPages.length !== 1 ? 's' : ''} across ${endpoints.length} CDP endpoint${endpoints.length !== 1 ? 's' : ''}.` +
103
+ `\n\nTarget a tab with: --target <id prefix> (first 8 chars sufficient)`,
104
+ );
105
+ }
106
+ return;
107
+ }
108
+
109
+ export async function cmdOpen(parsed: ParsedArgs, _args: string[]): Promise<void> {
110
+ if (parsed.help) {
111
+ console.log(
112
+ 'Usage: capture open <url> [--new] [--port <port>]\n\n' +
113
+ 'Open URL in browser (returns tab ID).\n\n' +
114
+ 'Example: capture open "https://app.example.com" --new',
115
+ );
116
+ process.exit(0);
117
+ }
118
+ const url = parsed.positional[0];
119
+ if (!url) {
120
+ console.error(
121
+ 'ERROR: Missing URL.\n\n' +
122
+ 'Usage: capture open <url> [--new] [--port <port>]\n\n' +
123
+ 'Example: capture open "https://app.example.com" --new',
124
+ );
125
+ process.exit(1);
126
+ }
127
+ const p = parsed.port ?? (await detectCdpPort());
128
+ const tab = await navigateAndWait(p, url, { forceNew: parsed.new });
129
+ console.log(JSON.stringify({
130
+ id: tab.id,
131
+ title: tab.title,
132
+ url: tab.url,
133
+ port: p,
134
+ }, null, 2));
135
+ console.error(
136
+ `\nOpened: ${tab.title || url}` +
137
+ `\n\nFor a multi-step validation, prefer a session (auto-targets this kind of tab):` +
138
+ `\n capture session start --url "${url}"` +
139
+ `\n\nOr interact one-off against this tab:` +
140
+ `\n capture a11y --port ${p} --target ${tab.id.slice(0, 8)} --interactive` +
141
+ `\n capture screenshot --port ${p} --target ${tab.id.slice(0, 8)}` +
142
+ `\n capture exec "<js>" --port ${p} --target ${tab.id.slice(0, 8)}`,
143
+ );
144
+ return;
145
+ }
146
+
147
+ export async function cmdResetTab(parsed: ParsedArgs, _args: string[]): Promise<void> {
148
+ if (parsed.help) {
149
+ console.log(
150
+ 'Usage: capture reset-tab <url> [--port <port>]\n\n' +
151
+ 'Abandon a stuck/unresponsive tab and open a fresh one.\n' +
152
+ 'Updates session context so subsequent commands auto-target the new tab.\n\n' +
153
+ 'Example: capture reset-tab "https://www.instagram.com/"',
154
+ );
155
+ process.exit(0);
156
+ }
157
+ const url = parsed.positional[0];
158
+ if (!url) {
159
+ console.error(
160
+ 'Usage: capture reset-tab <url> [--port <port>]',
161
+ );
162
+ process.exit(1);
163
+ }
164
+ const p = parsed.port ?? (await detectCdpPort());
165
+ const tab = await openTab(p, url);
166
+
167
+ // Wait for page load
168
+ if (tab.webSocketDebuggerUrl) {
169
+ const client = new CDPClient(tab.webSocketDebuggerUrl);
170
+ await client.waitReady();
171
+ await client.send('Page.enable');
172
+ await new Promise<void>((resolve) => {
173
+ const timer = setTimeout(() => resolve(), 10000);
174
+ client.on('Page.loadEventFired', () => {
175
+ clearTimeout(timer);
176
+ resolve();
177
+ });
178
+ });
179
+ client.close();
180
+ }
181
+
182
+ // Update session context so subsequent commands auto-target the new tab
183
+ updateActiveSession({ targetId: tab.id });
184
+
185
+ console.log(JSON.stringify({ id: tab.id, url: tab.url, port: p }, null, 2));
186
+ console.error(`\nNew tab opened. Session context updated.`);
187
+ console.error(`Use --target ${tab.id.slice(0, 8)} if passing target explicitly.`);
188
+ return;
189
+ }