opendevbrowser 0.0.11 → 0.0.12

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.
@@ -1,108 +1,368 @@
1
1
  <!doctype html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="dark" />
6
7
  <title>OpenDevBrowser</title>
7
8
  <style>
9
+ :root {
10
+ color-scheme: dark;
11
+ --bg-deep: #060910;
12
+ --bg-surface: #0b1220;
13
+ --panel: rgba(12, 20, 33, 0.74);
14
+ --panel-strong: rgba(15, 24, 40, 0.86);
15
+ --stroke: rgba(255, 255, 255, 0.12);
16
+ --stroke-strong: rgba(255, 255, 255, 0.2);
17
+ --text: #e8edf6;
18
+ --muted: #9aa6bd;
19
+ --accent: #20d5c6;
20
+ --accent-2: #6ee7ff;
21
+ --accent-soft: rgba(32, 213, 198, 0.22);
22
+ --danger: #f16b4e;
23
+ --shadow: 0 14px 32px rgba(3, 8, 18, 0.55);
24
+ }
25
+
26
+ * {
27
+ box-sizing: border-box;
28
+ }
29
+
8
30
  body {
9
- font-family: Arial, sans-serif;
10
31
  margin: 0;
11
- padding: 12px;
12
- min-width: 280px;
32
+ min-width: 336px;
33
+ position: relative;
34
+ overflow: hidden;
35
+ background:
36
+ radial-gradient(circle at 10% 20%, rgba(32, 213, 198, 0.2), transparent 40%),
37
+ radial-gradient(circle at 90% 0%, rgba(110, 231, 255, 0.2), transparent 35%),
38
+ linear-gradient(160deg, var(--bg-deep) 0%, var(--bg-surface) 100%);
39
+ color: var(--text);
40
+ font-family: "Space Grotesk", "Manrope", "Sora", "Avenir Next", "Segoe UI", sans-serif;
41
+ letter-spacing: 0.01em;
42
+ }
43
+
44
+ body::after {
45
+ content: "";
46
+ position: absolute;
47
+ inset: -40% -10% auto auto;
48
+ width: 220px;
49
+ height: 220px;
50
+ background: radial-gradient(circle, rgba(32, 213, 198, 0.18), transparent 70%);
51
+ filter: blur(20px);
52
+ pointer-events: none;
53
+ }
54
+
55
+ .app {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 14px;
59
+ padding: 16px;
60
+ animation: rise 400ms ease both;
13
61
  }
14
- h1 {
15
- font-size: 14px;
16
- margin: 0 0 8px;
62
+
63
+ @keyframes rise {
64
+ from {
65
+ opacity: 0;
66
+ transform: translateY(6px);
67
+ }
68
+ to {
69
+ opacity: 1;
70
+ transform: translateY(0);
71
+ }
17
72
  }
18
- .status-container {
73
+
74
+ .app-header {
19
75
  display: flex;
20
76
  align-items: center;
21
- gap: 8px;
22
- margin-bottom: 12px;
77
+ justify-content: space-between;
78
+ gap: 12px;
23
79
  }
24
- .status-indicator {
80
+
81
+ .brand {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 10px;
85
+ }
86
+
87
+ .brand-mark {
25
88
  width: 12px;
26
89
  height: 12px;
90
+ border-radius: 4px;
91
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
92
+ box-shadow: 0 0 14px rgba(32, 213, 198, 0.55);
93
+ }
94
+
95
+ .brand-title {
96
+ font-size: 13px;
97
+ font-weight: 600;
98
+ }
99
+
100
+ .brand-subtitle {
101
+ font-size: 9px;
102
+ color: var(--muted);
103
+ text-transform: uppercase;
104
+ letter-spacing: 0.22em;
105
+ margin-top: 2px;
106
+ }
107
+
108
+ .status-pill {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ gap: 6px;
112
+ padding: 4px 10px;
113
+ border: 1px solid var(--stroke);
114
+ background: rgba(255, 255, 255, 0.06);
115
+ border-radius: 999px;
116
+ font-size: 10px;
117
+ color: var(--muted);
118
+ backdrop-filter: blur(10px);
119
+ transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
120
+ }
121
+
122
+ .status-pill.connected {
123
+ color: var(--text);
124
+ border-color: rgba(32, 213, 198, 0.5);
125
+ background: var(--accent-soft);
126
+ }
127
+
128
+ .status-indicator {
129
+ width: 8px;
130
+ height: 8px;
27
131
  border-radius: 50%;
28
- background-color: #dc3545;
29
- transition: background-color 0.3s ease;
132
+ background: var(--danger);
133
+ box-shadow: 0 0 8px rgba(241, 107, 78, 0.4);
134
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
30
135
  }
136
+
31
137
  .status-indicator.connected {
32
- background-color: #28a745;
138
+ background: var(--accent);
139
+ box-shadow: 0 0 12px rgba(32, 213, 198, 0.55);
33
140
  }
34
- #status {
35
- font-size: 12px;
141
+
142
+ .panel {
143
+ position: relative;
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 10px;
147
+ padding: 12px;
148
+ border-radius: 14px;
149
+ border: 1px solid var(--stroke);
150
+ background: var(--panel);
151
+ box-shadow: var(--shadow);
152
+ backdrop-filter: blur(18px) saturate(120%);
153
+ }
154
+
155
+ .panel::before {
156
+ content: "";
157
+ position: absolute;
158
+ inset: 0;
159
+ border-radius: 14px;
160
+ pointer-events: none;
161
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
36
162
  }
163
+
37
164
  label {
38
- font-size: 11px;
165
+ font-size: 10px;
166
+ color: var(--muted);
39
167
  display: block;
40
- margin-bottom: 4px;
168
+ margin-bottom: 6px;
41
169
  }
42
- label.toggle {
170
+
171
+ input[type="number"],
172
+ input[type="text"] {
173
+ width: 100%;
174
+ padding: 8px 10px;
175
+ border-radius: 10px;
176
+ border: 1px solid rgba(255, 255, 255, 0.12);
177
+ background: var(--panel-strong);
178
+ color: var(--text);
179
+ font-size: 12px;
180
+ outline: none;
181
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
182
+ }
183
+
184
+ input[type="number"]:focus,
185
+ input[type="text"]:focus {
186
+ border-color: rgba(32, 213, 198, 0.6);
187
+ box-shadow: 0 0 0 2px rgba(32, 213, 198, 0.12);
188
+ }
189
+
190
+ input:disabled {
191
+ opacity: 0.6;
192
+ }
193
+
194
+ .row {
43
195
  display: flex;
44
196
  align-items: center;
45
- gap: 6px;
46
- margin-bottom: 10px;
197
+ justify-content: space-between;
198
+ gap: 10px;
199
+ padding-top: 10px;
47
200
  }
48
- input {
49
- width: 100%;
50
- padding: 6px;
51
- margin-bottom: 10px;
52
- border: 1px solid #ccc;
53
- border-radius: 4px;
54
- box-sizing: border-box;
201
+
202
+ .row + .row {
203
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
55
204
  }
56
- input[type="checkbox"] {
57
- width: auto;
58
- margin: 0;
59
- padding: 0;
205
+
206
+ .row-title {
207
+ font-size: 12px;
208
+ color: var(--text);
60
209
  }
61
- input:disabled {
62
- background-color: #f5f5f5;
63
- color: #999;
210
+
211
+ .row-subtitle {
212
+ font-size: 10px;
213
+ color: var(--muted);
214
+ margin-top: 2px;
215
+ }
216
+
217
+ .switch {
218
+ position: relative;
219
+ display: inline-flex;
220
+ align-items: center;
221
+ width: 40px;
222
+ height: 22px;
223
+ }
224
+
225
+ .switch input {
226
+ opacity: 0;
227
+ width: 0;
228
+ height: 0;
229
+ }
230
+
231
+ .slider {
232
+ position: absolute;
233
+ inset: 0;
234
+ background: rgba(255, 255, 255, 0.12);
235
+ border: 1px solid rgba(255, 255, 255, 0.16);
236
+ border-radius: 999px;
237
+ transition: background 0.2s ease, border-color 0.2s ease;
238
+ }
239
+
240
+ .slider::before {
241
+ content: "";
242
+ position: absolute;
243
+ width: 16px;
244
+ height: 16px;
245
+ left: 2px;
246
+ top: 2px;
247
+ border-radius: 999px;
248
+ background: #0b111a;
249
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
250
+ transition: transform 0.2s ease;
64
251
  }
65
- button {
252
+
253
+ .switch input:checked + .slider {
254
+ background: rgba(32, 213, 198, 0.3);
255
+ border-color: rgba(32, 213, 198, 0.6);
256
+ }
257
+
258
+ .switch input:checked + .slider::before {
259
+ transform: translateX(18px);
260
+ background: #0f1f22;
261
+ box-shadow: 0 0 0 1px rgba(32, 213, 198, 0.4) inset;
262
+ }
263
+
264
+ .primary {
66
265
  width: 100%;
67
- padding: 8px;
68
- border: 1px solid #222;
69
- background: #111;
70
- color: #fff;
266
+ padding: 10px 12px;
267
+ border: none;
268
+ border-radius: 12px;
269
+ font-size: 12px;
270
+ font-weight: 600;
271
+ letter-spacing: 0.02em;
272
+ color: #081014;
273
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
71
274
  cursor: pointer;
72
- border-radius: 4px;
73
- transition: background-color 0.2s ease;
275
+ box-shadow: 0 10px 24px rgba(32, 213, 198, 0.28);
276
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
277
+ }
278
+
279
+ .primary:hover {
280
+ transform: translateY(-1px);
281
+ box-shadow: 0 12px 28px rgba(32, 213, 198, 0.34);
74
282
  }
75
- button:hover {
76
- background: #333;
283
+
284
+ .primary:active {
285
+ transform: translateY(0);
77
286
  }
78
- .auto-pair-note {
287
+
288
+ .field-note {
79
289
  font-size: 10px;
80
- color: #666;
81
- margin-top: -6px;
82
- margin-bottom: 10px;
290
+ color: var(--muted);
291
+ margin-top: 6px;
292
+ }
293
+
294
+ @media (prefers-reduced-motion: reduce) {
295
+ * {
296
+ transition: none !important;
297
+ animation: none !important;
298
+ }
83
299
  }
84
300
  </style>
85
301
  </head>
86
302
  <body>
87
- <h1>OpenDevBrowser</h1>
88
- <div class="status-container">
89
- <div id="statusIndicator" class="status-indicator"></div>
90
- <div id="status">Disconnected</div>
303
+ <div class="app">
304
+ <header class="app-header">
305
+ <div class="brand">
306
+ <div class="brand-mark"></div>
307
+ <div>
308
+ <div class="brand-title">OpenDevBrowser</div>
309
+ <div class="brand-subtitle">Relay Extension</div>
310
+ </div>
311
+ </div>
312
+ <div id="statusPill" class="status-pill">
313
+ <div id="statusIndicator" class="status-indicator"></div>
314
+ <div id="status">Disconnected</div>
315
+ </div>
316
+ </header>
317
+
318
+ <section class="panel">
319
+ <div class="field">
320
+ <label for="relayPort">Relay port</label>
321
+ <input id="relayPort" type="number" min="1" max="65535" />
322
+ </div>
323
+
324
+ <div class="row">
325
+ <div>
326
+ <div class="row-title">Auto-connect</div>
327
+ <div class="row-subtitle">Reconnect on browser start</div>
328
+ </div>
329
+ <label class="switch">
330
+ <input id="autoConnect" type="checkbox" />
331
+ <span class="slider"></span>
332
+ </label>
333
+ </div>
334
+
335
+ <div class="row">
336
+ <div>
337
+ <div class="row-title">Auto-pair</div>
338
+ <div class="row-subtitle">Fetch token from running plugin</div>
339
+ </div>
340
+ <label class="switch">
341
+ <input id="autoPair" type="checkbox" />
342
+ <span class="slider"></span>
343
+ </label>
344
+ </div>
345
+
346
+ <div class="row">
347
+ <div>
348
+ <div class="row-title">Require pairing token</div>
349
+ <div class="row-subtitle">Disable only for local dev</div>
350
+ </div>
351
+ <label class="switch">
352
+ <input id="pairingEnabled" type="checkbox" />
353
+ <span class="slider"></span>
354
+ </label>
355
+ </div>
356
+
357
+ <div class="field">
358
+ <label for="pairingToken">Pairing token</label>
359
+ <input id="pairingToken" type="text" placeholder="Enter token or enable auto-pair" />
360
+ <div id="statusNote" class="field-note">Local relay only. Tokens stay on-device.</div>
361
+ </div>
362
+ </section>
363
+
364
+ <button id="toggle" class="primary">Connect</button>
91
365
  </div>
92
- <label for="relayPort">Relay port</label>
93
- <input id="relayPort" type="number" min="1" max="65535" />
94
- <label class="toggle" for="autoPair">
95
- <input id="autoPair" type="checkbox" />
96
- Auto-Pair (fetch token from plugin)
97
- </label>
98
- <div class="auto-pair-note">When enabled, token is fetched automatically from running plugin</div>
99
- <label class="toggle" for="pairingEnabled">
100
- <input id="pairingEnabled" type="checkbox" />
101
- Require pairing token
102
- </label>
103
- <label for="pairingToken">Pairing token</label>
104
- <input id="pairingToken" type="text" placeholder="Enter token or enable Auto-Pair" />
105
- <button id="toggle">Connect</button>
106
366
  <script type="module" src="dist/popup.js"></script>
107
367
  </body>
108
368
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opendevbrowser",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "OpenCode plugin for browser automation via CDP with snapshot-refs-actions workflow",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/skills/AGENTS.md CHANGED
@@ -16,6 +16,7 @@ Applies to `skills/` and subdirectories. Extends root `AGENTS.md`.
16
16
  - Keep guidance short, script-first, and snapshot-first.
17
17
  - Keep examples aligned with `opendevbrowser_*` tool names.
18
18
  - Do not include secrets or captured page data in skill content.
19
+ - If adding a consolidated skill pack, update `docs/REFACTORING_PLAN.md` and `docs/CLI.md`.
19
20
 
20
21
  ## Skill Format Specification
21
22
 
@@ -1,128 +0,0 @@
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
@@ -1 +0,0 @@
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":"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","names":[]}