opendevbrowser 0.0.10

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 ADDED
@@ -0,0 +1,241 @@
1
+ # OpenDevBrowser
2
+
3
+ [![npm version](https://img.shields.io/npm/v/opendevbrowser.svg?style=flat-square)](https://registry.npmjs.org/opendevbrowser)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
6
+ [![OpenCode Plugin](https://img.shields.io/badge/OpenCode-Plugin-green.svg?style=flat-square)](https://opencode.ai)
7
+ [![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg?style=flat-square)](https://github.com/freshtechbro/opendevbrowser)
8
+
9
+ > **Script-first browser automation for AI agents.** Snapshot → Refs → Actions.
10
+
11
+ OpenDevBrowser is an [OpenCode](https://opencode.ai) plugin that gives AI agents direct browser control via Chrome DevTools Protocol. Launch browsers, capture page snapshots, and interact with elements using stable refs.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ### For Humans
18
+
19
+ ```bash
20
+ # Interactive installer (recommended)
21
+ npx opendevbrowser
22
+
23
+ # Or specify location
24
+ npx opendevbrowser --global # ~/.config/opencode/opencode.json
25
+ npx opendevbrowser --local # ./opencode.json
26
+
27
+ # Full install (config + extension assets)
28
+ npx opendevbrowser --full
29
+ ```
30
+
31
+ Restart OpenCode after installation.
32
+
33
+ OpenCode discovers skills in `.opencode/skill` (project) and `~/.config/opencode/skill` (global) first; `.claude/skills` is compatibility-only. The CLI installs bundled skills into the OpenCode-native locations by default.
34
+
35
+ ### Agent Installation (OpenCode)
36
+
37
+ Recommended (CLI, installs plugin + config + bundled skills + extension assets):
38
+
39
+ ```bash
40
+ npx opendevbrowser --full --global --no-prompt
41
+ ```
42
+
43
+ Manual fallback (edit OpenCode config):
44
+
45
+ ```json
46
+ {
47
+ "$schema": "https://opencode.ai/config.json",
48
+ "plugin": ["opendevbrowser"]
49
+ }
50
+ ```
51
+
52
+ Config location: `~/.config/opencode/opencode.json`
53
+
54
+ Restart OpenCode, then run `opendevbrowser_status` to verify the plugin is loaded.
55
+
56
+ ---
57
+
58
+ ## Quick Start
59
+
60
+ ```
61
+ 1. Launch a browser session
62
+ 2. Navigate to a URL
63
+ 3. Take a snapshot to get element refs
64
+ 4. Interact using refs (click, type, select)
65
+ 5. Re-snapshot after navigation
66
+ ```
67
+
68
+ ### Core Workflow
69
+
70
+ | Step | Tool | Purpose |
71
+ |------|------|---------|
72
+ | 1 | `opendevbrowser_launch` | Start managed Chrome session |
73
+ | 2 | `opendevbrowser_goto` | Navigate to URL |
74
+ | 3 | `opendevbrowser_snapshot` | Get page structure with refs |
75
+ | 4 | `opendevbrowser_click` / `opendevbrowser_type` | Interact with elements |
76
+ | 5 | `opendevbrowser_close` | Clean up session |
77
+
78
+ ---
79
+
80
+ ## Features
81
+
82
+ ### Browser Control
83
+ - **Launch & Connect** - Start managed Chrome or connect to existing browsers
84
+ - **Multi-Tab Support** - Create, switch, and manage browser tabs
85
+ - **Profile Persistence** - Maintain login sessions across runs
86
+ - **Headless Mode** - Run without visible browser window
87
+
88
+ ### Page Interaction
89
+ - **Snapshot** - Accessibility-tree based page capture (token-efficient)
90
+ - **Click** - Click elements by ref
91
+ - **Type** - Enter text into inputs
92
+ - **Select** - Choose dropdown options
93
+ - **Scroll** - Scroll page or elements
94
+ - **Wait** - Wait for selectors or navigation
95
+
96
+ ### DevTools Integration
97
+ - **Console Capture** - Monitor console.log, errors, warnings
98
+ - **Network Tracking** - Capture XHR/fetch requests and responses
99
+ - **Screenshot** - Full page or element screenshots
100
+ - **Performance** - Page load metrics
101
+
102
+ ### Export & Clone
103
+ - **DOM Capture** - Extract sanitized HTML with inline styles
104
+ - **React Emitter** - Generate React component code from pages
105
+ - **CSS Extraction** - Pull computed styles
106
+
107
+ ---
108
+
109
+ ## Chrome Extension (Optional)
110
+
111
+ The extension enables **Mode C** - attach to existing logged-in browser tabs without launching a new browser.
112
+
113
+ ### Auto-Pair Feature
114
+
115
+ The plugin and extension can automatically pair:
116
+
117
+ 1. **Plugin side**: Auto-generates secure token on first run (saved to config)
118
+ 2. **Extension side**: Enable "Auto-Pair" toggle and click Connect
119
+ 3. Extension fetches token from plugin's relay server
120
+ 4. Connection established with color indicator (green = connected)
121
+
122
+ ### Manual Setup
123
+
124
+ 1. Install extension from Chrome Web Store or load unpacked from `~/.cache/opencode/opendevbrowser-extension/`
125
+ 2. Open extension popup
126
+ 3. Enter same port/token as plugin config
127
+ 4. Click Connect
128
+
129
+ ---
130
+
131
+ ## Configuration
132
+
133
+ Optional config file: `~/.config/opencode/opendevbrowser.jsonc`
134
+
135
+ ```jsonc
136
+ {
137
+ "headless": false,
138
+ "profile": "default",
139
+ "persistProfile": true,
140
+ "snapshot": { "maxChars": 16000, "maxNodes": 1000 },
141
+ "export": { "maxNodes": 1000, "inlineStyles": true },
142
+ "devtools": { "showFullUrls": false, "showFullConsole": false },
143
+ "security": {
144
+ "allowRawCDP": false,
145
+ "allowNonLocalCdp": false,
146
+ "allowUnsafeExport": false
147
+ },
148
+ "continuity": {
149
+ "enabled": true,
150
+ "filePath": "opendevbrowser_continuity.md",
151
+ "nudge": {
152
+ "enabled": true,
153
+ "keywords": [
154
+ "plan",
155
+ "multi-step",
156
+ "multi step",
157
+ "long-running",
158
+ "long running",
159
+ "refactor",
160
+ "migration",
161
+ "rollout",
162
+ "release",
163
+ "upgrade",
164
+ "investigate",
165
+ "follow-up",
166
+ "continue"
167
+ ],
168
+ "maxAgeMs": 60000
169
+ }
170
+ },
171
+ "relayPort": 8787,
172
+ "relayToken": "auto-generated-on-first-run"
173
+ }
174
+ ```
175
+
176
+ All fields optional. Plugin works with sensible defaults.
177
+
178
+ ---
179
+
180
+ ## CLI Commands
181
+
182
+ | Command | Description |
183
+ |---------|-------------|
184
+ | `npx opendevbrowser` | Interactive install |
185
+ | `npx opendevbrowser --global` | Install to global config |
186
+ | `npx opendevbrowser --local` | Install to project config |
187
+ | `npx opendevbrowser --with-config` | Also create opendevbrowser.jsonc |
188
+ | `npx opendevbrowser --full` | Full install (config + extension assets) |
189
+ | `npx opendevbrowser --update` | Clear cache, trigger reinstall |
190
+ | `npx opendevbrowser --uninstall` | Remove from config |
191
+ | `npx opendevbrowser --version` | Show version |
192
+
193
+ ---
194
+
195
+ ## Security
196
+
197
+ - **Relay Authentication** - Cryptographically secure tokens, timing-safe comparison
198
+ - **Origin Validation** - Only localhost and Chrome extensions can pair
199
+ - **CDP Localhost-Only** - Remote CDP endpoints blocked by default
200
+ - **Data Redaction** - Console/network output redacts tokens and API keys
201
+ - **Export Sanitization** - Scripts and event handlers stripped from exports
202
+ - **Atomic Config Writes** - Prevents config corruption on crash
203
+
204
+ ---
205
+
206
+ ## Updating
207
+
208
+ ```bash
209
+ # Option 1: Clear cache (recommended)
210
+ rm -rf ~/.cache/opencode/node_modules/opendevbrowser
211
+ # Then restart OpenCode
212
+
213
+ # Option 2: Use CLI
214
+ npx opendevbrowser --update
215
+ ```
216
+
217
+ Release checklist: `docs/DISTRIBUTION_PLAN.md`
218
+
219
+ ---
220
+
221
+ ## Development
222
+
223
+ ```bash
224
+ npm install
225
+ npm run build # Compile to dist/
226
+ npm run test # Run tests with coverage
227
+ npm run lint # ESLint checks
228
+ npm run extension:build # Compile extension
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Privacy
234
+
235
+ See [Privacy Policy](docs/privacy.md) for data handling details.
236
+
237
+ ---
238
+
239
+ ## License
240
+
241
+ MIT
@@ -0,0 +1,128 @@
1
+ // src/extension-extractor.ts
2
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, rmSync, renameSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+ import { fileURLToPath } from "url";
6
+ var EXTENSION_DIR_NAME = "opendevbrowser";
7
+ var VERSION_FILE = ".version";
8
+ function getConfigDir() {
9
+ return join(homedir(), ".config", "opencode", EXTENSION_DIR_NAME, "extension");
10
+ }
11
+ function getPackageVersion() {
12
+ try {
13
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
14
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
15
+ return pkg.version || "0.0.0";
16
+ } catch (error) {
17
+ console.warn("[opendevbrowser] Failed to read package.json for extension version:", error);
18
+ return "0.0.0";
19
+ }
20
+ }
21
+ function getInstalledVersion(destDir) {
22
+ try {
23
+ const versionPath = join(destDir, VERSION_FILE);
24
+ if (existsSync(versionPath)) {
25
+ return readFileSync(versionPath, "utf-8").trim();
26
+ }
27
+ } catch (error) {
28
+ console.warn("[opendevbrowser] Failed to read installed extension version:", error);
29
+ }
30
+ return null;
31
+ }
32
+ function getBundledExtensionPath() {
33
+ const candidates = [
34
+ join(dirname(fileURLToPath(import.meta.url)), "..", "extension"),
35
+ join(dirname(fileURLToPath(import.meta.url)), "..", "..", "extension")
36
+ ];
37
+ for (const candidate of candidates) {
38
+ if (existsSync(join(candidate, "manifest.json"))) {
39
+ return candidate;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ function isCompleteInstall(dir) {
45
+ const required = ["manifest.json", VERSION_FILE];
46
+ return required.every((file) => existsSync(join(dir, file)));
47
+ }
48
+ function extractExtension() {
49
+ const bundledPath = getBundledExtensionPath();
50
+ if (!bundledPath) {
51
+ return null;
52
+ }
53
+ const destDir = getConfigDir();
54
+ const currentVersion = getPackageVersion();
55
+ const installedVersion = getInstalledVersion(destDir);
56
+ if (installedVersion === currentVersion && isCompleteInstall(destDir)) {
57
+ return destDir;
58
+ }
59
+ const parentDir = dirname(destDir);
60
+ const stagingDir = join(parentDir, `.opendevbrowser-staging-${process.pid}-${Date.now()}`);
61
+ const backupDir = join(parentDir, `.opendevbrowser-backup-${process.pid}-${Date.now()}`);
62
+ try {
63
+ mkdirSync(stagingDir, { recursive: true });
64
+ const itemsToCopy = ["manifest.json", "popup.html", "dist", "icons"];
65
+ for (const item of itemsToCopy) {
66
+ const src = join(bundledPath, item);
67
+ const dest = join(stagingDir, item);
68
+ if (existsSync(src)) {
69
+ cpSync(src, dest, { recursive: true, force: true });
70
+ }
71
+ }
72
+ writeFileSync(join(stagingDir, VERSION_FILE), currentVersion, "utf-8");
73
+ if (!isCompleteInstall(stagingDir)) {
74
+ throw new Error("Staging directory incomplete after copy");
75
+ }
76
+ if (existsSync(destDir)) {
77
+ renameSync(destDir, backupDir);
78
+ }
79
+ renameSync(stagingDir, destDir);
80
+ if (existsSync(backupDir)) {
81
+ rmSync(backupDir, { recursive: true, force: true });
82
+ }
83
+ return destDir;
84
+ } catch (error) {
85
+ if (existsSync(backupDir) && !existsSync(destDir)) {
86
+ try {
87
+ renameSync(backupDir, destDir);
88
+ } catch (rollbackError) {
89
+ console.warn(`[opendevbrowser] Warning: Rollback failed for ${backupDir}:`, rollbackError);
90
+ }
91
+ }
92
+ if (existsSync(stagingDir)) {
93
+ try {
94
+ rmSync(stagingDir, { recursive: true, force: true });
95
+ } catch (stagingCleanupError) {
96
+ console.warn(`[opendevbrowser] Warning: Failed to clean up staging directory ${stagingDir}:`, stagingCleanupError);
97
+ }
98
+ }
99
+ if (existsSync(backupDir)) {
100
+ try {
101
+ rmSync(backupDir, { recursive: true, force: true });
102
+ } catch (backupCleanupError) {
103
+ console.warn(`[opendevbrowser] Warning: Failed to clean up backup directory ${backupDir}:`, backupCleanupError);
104
+ }
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+ function getExtensionPath() {
110
+ const destDir = getConfigDir();
111
+ if (isCompleteInstall(destDir)) {
112
+ return destDir;
113
+ }
114
+ return getBundledExtensionPath();
115
+ }
116
+
117
+ // src/utils/crypto.ts
118
+ import { randomBytes } from "crypto";
119
+ function generateSecureToken() {
120
+ return randomBytes(32).toString("hex");
121
+ }
122
+
123
+ export {
124
+ generateSecureToken,
125
+ extractExtension,
126
+ getExtensionPath
127
+ };
128
+ //# sourceMappingURL=chunk-R5VUZEUU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/extension-extractor.ts","../src/utils/crypto.ts"],"sourcesContent":["import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, rmSync, renameSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\n\nconst EXTENSION_DIR_NAME = \"opendevbrowser\";\nconst VERSION_FILE = \".version\";\n\nfunction getConfigDir(): string {\n return join(homedir(), \".config\", \"opencode\", EXTENSION_DIR_NAME, \"extension\");\n}\n\nfunction getPackageVersion(): string {\n try {\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n return pkg.version || \"0.0.0\";\n } catch (error) {\n console.warn(\"[opendevbrowser] Failed to read package.json for extension version:\", error);\n return \"0.0.0\";\n }\n}\n\nfunction getInstalledVersion(destDir: string): string | null {\n try {\n const versionPath = join(destDir, VERSION_FILE);\n if (existsSync(versionPath)) {\n return readFileSync(versionPath, \"utf-8\").trim();\n }\n } catch (error) {\n console.warn(\"[opendevbrowser] Failed to read installed extension version:\", error);\n }\n return null;\n}\n\nfunction getBundledExtensionPath(): string | null {\n const candidates = [\n join(dirname(fileURLToPath(import.meta.url)), \"..\", \"extension\"),\n join(dirname(fileURLToPath(import.meta.url)), \"..\", \"..\", \"extension\")\n ];\n for (const candidate of candidates) {\n if (existsSync(join(candidate, \"manifest.json\"))) {\n return candidate;\n }\n }\n return null;\n}\n\nfunction isCompleteInstall(dir: string): boolean {\n const required = [\"manifest.json\", VERSION_FILE];\n return required.every(file => existsSync(join(dir, file)));\n}\n\nexport function extractExtension(): string | null {\n const bundledPath = getBundledExtensionPath();\n if (!bundledPath) {\n return null;\n }\n\n const destDir = getConfigDir();\n const currentVersion = getPackageVersion();\n const installedVersion = getInstalledVersion(destDir);\n\n // Early return if version matches and installation is complete\n if (installedVersion === currentVersion && isCompleteInstall(destDir)) {\n return destDir;\n }\n\n // Create staging directory (sibling to destDir for same-device rename)\n const parentDir = dirname(destDir);\n const stagingDir = join(parentDir, `.opendevbrowser-staging-${process.pid}-${Date.now()}`);\n const backupDir = join(parentDir, `.opendevbrowser-backup-${process.pid}-${Date.now()}`);\n\n try {\n // Step 1: Copy to staging\n mkdirSync(stagingDir, { recursive: true });\n const itemsToCopy = [\"manifest.json\", \"popup.html\", \"dist\", \"icons\"];\n for (const item of itemsToCopy) {\n const src = join(bundledPath, item);\n const dest = join(stagingDir, item);\n if (existsSync(src)) {\n cpSync(src, dest, { recursive: true, force: true });\n }\n }\n writeFileSync(join(stagingDir, VERSION_FILE), currentVersion, \"utf-8\");\n\n // Step 2: Validate staging is complete\n if (!isCompleteInstall(stagingDir)) {\n throw new Error(\"Staging directory incomplete after copy\");\n }\n\n // Step 3: Atomic swap\n if (existsSync(destDir)) {\n renameSync(destDir, backupDir);\n }\n renameSync(stagingDir, destDir);\n\n // Step 4: Cleanup backup\n if (existsSync(backupDir)) {\n rmSync(backupDir, { recursive: true, force: true });\n }\n\n return destDir;\n } catch (error) {\n // Rollback: restore backup if it exists\n if (existsSync(backupDir) && !existsSync(destDir)) {\n try {\n renameSync(backupDir, destDir);\n } catch (rollbackError) {\n console.warn(`[opendevbrowser] Warning: Rollback failed for ${backupDir}:`, rollbackError);\n }\n }\n // Cleanup staging\n if (existsSync(stagingDir)) {\n try {\n rmSync(stagingDir, { recursive: true, force: true });\n } catch (stagingCleanupError) {\n console.warn(`[opendevbrowser] Warning: Failed to clean up staging directory ${stagingDir}:`, stagingCleanupError);\n }\n }\n // Cleanup backup\n if (existsSync(backupDir)) {\n try {\n rmSync(backupDir, { recursive: true, force: true });\n } catch (backupCleanupError) {\n console.warn(`[opendevbrowser] Warning: Failed to clean up backup directory ${backupDir}:`, backupCleanupError);\n }\n }\n throw error;\n }\n}\n\nexport function getExtensionPath(): string | null {\n const destDir = getConfigDir();\n if (isCompleteInstall(destDir)) {\n return destDir;\n }\n return getBundledExtensionPath();\n}\n","import { randomBytes } from \"crypto\";\n\nexport function generateSecureToken(): string {\n return randomBytes(32).toString(\"hex\");\n}\n"],"mappings":";AAAA,SAAS,YAAY,WAAW,QAAQ,cAAc,eAAe,QAAQ,kBAAkB;AAC/F,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAE9B,IAAM,qBAAqB;AAC3B,IAAM,eAAe;AAErB,SAAS,eAAuB;AAC9B,SAAO,KAAK,QAAQ,GAAG,WAAW,YAAY,oBAAoB,WAAW;AAC/E;AAEA,SAAS,oBAA4B;AACnC,MAAI;AACF,UAAM,UAAU,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,cAAc;AAClF,UAAM,MAAM,KAAK,MAAM,aAAa,SAAS,OAAO,CAAC;AACrD,WAAO,IAAI,WAAW;AAAA,EACxB,SAAS,OAAO;AACd,YAAQ,KAAK,uEAAuE,KAAK;AACzF,WAAO;AAAA,EACT;AACF;AAEA,SAAS,oBAAoB,SAAgC;AAC3D,MAAI;AACF,UAAM,cAAc,KAAK,SAAS,YAAY;AAC9C,QAAI,WAAW,WAAW,GAAG;AAC3B,aAAO,aAAa,aAAa,OAAO,EAAE,KAAK;AAAA,IACjD;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,KAAK,gEAAgE,KAAK;AAAA,EACpF;AACA,SAAO;AACT;AAEA,SAAS,0BAAyC;AAChD,QAAM,aAAa;AAAA,IACjB,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,WAAW;AAAA,IAC/D,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,MAAM,WAAW;AAAA,EACvE;AACA,aAAW,aAAa,YAAY;AAClC,QAAI,WAAW,KAAK,WAAW,eAAe,CAAC,GAAG;AAChD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,KAAsB;AAC/C,QAAM,WAAW,CAAC,iBAAiB,YAAY;AAC/C,SAAO,SAAS,MAAM,UAAQ,WAAW,KAAK,KAAK,IAAI,CAAC,CAAC;AAC3D;AAEO,SAAS,mBAAkC;AAChD,QAAM,cAAc,wBAAwB;AAC5C,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,aAAa;AAC7B,QAAM,iBAAiB,kBAAkB;AACzC,QAAM,mBAAmB,oBAAoB,OAAO;AAGpD,MAAI,qBAAqB,kBAAkB,kBAAkB,OAAO,GAAG;AACrE,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,QAAQ,OAAO;AACjC,QAAM,aAAa,KAAK,WAAW,2BAA2B,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE;AACzF,QAAM,YAAY,KAAK,WAAW,0BAA0B,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE;AAEvF,MAAI;AAEF,cAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AACzC,UAAM,cAAc,CAAC,iBAAiB,cAAc,QAAQ,OAAO;AACnE,eAAW,QAAQ,aAAa;AAC9B,YAAM,MAAM,KAAK,aAAa,IAAI;AAClC,YAAM,OAAO,KAAK,YAAY,IAAI;AAClC,UAAI,WAAW,GAAG,GAAG;AACnB,eAAO,KAAK,MAAM,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACpD;AAAA,IACF;AACA,kBAAc,KAAK,YAAY,YAAY,GAAG,gBAAgB,OAAO;AAGrE,QAAI,CAAC,kBAAkB,UAAU,GAAG;AAClC,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAGA,QAAI,WAAW,OAAO,GAAG;AACvB,iBAAW,SAAS,SAAS;AAAA,IAC/B;AACA,eAAW,YAAY,OAAO;AAG9B,QAAI,WAAW,SAAS,GAAG;AACzB,aAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,IACpD;AAEA,WAAO;AAAA,EACT,SAAS,OAAO;AAEd,QAAI,WAAW,SAAS,KAAK,CAAC,WAAW,OAAO,GAAG;AACjD,UAAI;AACF,mBAAW,WAAW,OAAO;AAAA,MAC/B,SAAS,eAAe;AACtB,gBAAQ,KAAK,iDAAiD,SAAS,KAAK,aAAa;AAAA,MAC3F;AAAA,IACF;AAEA,QAAI,WAAW,UAAU,GAAG;AAC1B,UAAI;AACF,eAAO,YAAY,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACrD,SAAS,qBAAqB;AAC5B,gBAAQ,KAAK,kEAAkE,UAAU,KAAK,mBAAmB;AAAA,MACnH;AAAA,IACF;AAEA,QAAI,WAAW,SAAS,GAAG;AACzB,UAAI;AACF,eAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MACpD,SAAS,oBAAoB;AAC3B,gBAAQ,KAAK,iEAAiE,SAAS,KAAK,kBAAkB;AAAA,MAChH;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAEO,SAAS,mBAAkC;AAChD,QAAM,UAAU,aAAa;AAC7B,MAAI,kBAAkB,OAAO,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,SAAO,wBAAwB;AACjC;;;AC1IA,SAAS,mBAAmB;AAErB,SAAS,sBAA8B;AAC5C,SAAO,YAAY,EAAE,EAAE,SAAS,KAAK;AACvC;","names":[]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node