opengstack 0.13.10 → 0.14.2

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 (189) hide show
  1. package/AGENTS.md +4 -4
  2. package/CLAUDE.md +127 -110
  3. package/README.md +10 -5
  4. package/SKILL.md +500 -70
  5. package/bin/opengstack.js +69 -69
  6. package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +7 -25
  7. package/{skills/benchmark/SKILL.md → commands/benchmark.md} +84 -108
  8. package/{skills/browse/SKILL.md → commands/browse.md} +60 -81
  9. package/{skills/ship/SKILL.md → commands/canary.md} +7 -27
  10. package/{skills/careful/SKILL.md → commands/careful.md} +2 -22
  11. package/{skills/canary/SKILL.md → commands/codex.md} +7 -26
  12. package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +7 -24
  13. package/commands/cso.md +70 -0
  14. package/commands/design-consultation.md +70 -0
  15. package/commands/design-review.md +70 -0
  16. package/commands/design-shotgun.md +70 -0
  17. package/commands/document-release.md +70 -0
  18. package/{skills/freeze/SKILL.md → commands/freeze.md} +3 -29
  19. package/{skills/guard/SKILL.md → commands/guard.md} +4 -35
  20. package/commands/investigate.md +70 -0
  21. package/commands/land-and-deploy.md +70 -0
  22. package/commands/office-hours.md +70 -0
  23. package/{skills/gstack-upgrade/SKILL.md → commands/opengstack-upgrade.md} +64 -79
  24. package/commands/plan-ceo-review.md +70 -0
  25. package/commands/plan-design-review.md +70 -0
  26. package/commands/plan-eng-review.md +70 -0
  27. package/commands/qa-only.md +70 -0
  28. package/commands/qa.md +70 -0
  29. package/commands/retro.md +70 -0
  30. package/commands/review.md +70 -0
  31. package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +22 -40
  32. package/commands/setup-deploy.md +70 -0
  33. package/commands/ship.md +70 -0
  34. package/commands/unfreeze.md +25 -0
  35. package/docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md +9 -9
  36. package/docs/designs/CONDUCTOR_CHROME_SIDEBAR_INTEGRATION.md +2 -2
  37. package/docs/designs/CONDUCTOR_SESSION_API.md +16 -16
  38. package/docs/designs/DESIGN_SHOTGUN.md +74 -74
  39. package/docs/designs/DESIGN_TOOLS_V1.md +111 -111
  40. package/docs/skills.md +483 -202
  41. package/package.json +42 -43
  42. package/scripts/analytics.ts +188 -0
  43. package/scripts/dev-skill.ts +83 -0
  44. package/scripts/discover-skills.ts +39 -0
  45. package/scripts/eval-compare.ts +97 -0
  46. package/scripts/eval-list.ts +117 -0
  47. package/scripts/eval-select.ts +86 -0
  48. package/scripts/eval-summary.ts +188 -0
  49. package/scripts/eval-watch.ts +172 -0
  50. package/scripts/gen-skill-docs.ts +473 -0
  51. package/scripts/resolvers/browse.ts +129 -0
  52. package/scripts/resolvers/codex-helpers.ts +133 -0
  53. package/scripts/resolvers/composition.ts +48 -0
  54. package/scripts/resolvers/confidence.ts +37 -0
  55. package/scripts/resolvers/constants.ts +50 -0
  56. package/scripts/resolvers/design.ts +950 -0
  57. package/scripts/resolvers/index.ts +59 -0
  58. package/scripts/resolvers/learnings.ts +96 -0
  59. package/scripts/resolvers/preamble.ts +505 -0
  60. package/scripts/resolvers/review.ts +884 -0
  61. package/scripts/resolvers/testing.ts +573 -0
  62. package/scripts/resolvers/types.ts +45 -0
  63. package/scripts/resolvers/utility.ts +421 -0
  64. package/scripts/skill-check.ts +190 -0
  65. package/scripts/cleanup.py +0 -100
  66. package/scripts/filter-skills.sh +0 -114
  67. package/scripts/filter_skills.py +0 -164
  68. package/scripts/install-skills.js +0 -60
  69. package/skills/autoplan/SKILL.md +0 -96
  70. package/skills/autoplan/SKILL.md.tmpl +0 -694
  71. package/skills/benchmark/SKILL.md.tmpl +0 -222
  72. package/skills/browse/SKILL.md.tmpl +0 -131
  73. package/skills/browse/bin/find-browse +0 -21
  74. package/skills/browse/bin/remote-slug +0 -14
  75. package/skills/browse/scripts/build-node-server.sh +0 -48
  76. package/skills/browse/src/activity.ts +0 -208
  77. package/skills/browse/src/browser-manager.ts +0 -959
  78. package/skills/browse/src/buffers.ts +0 -137
  79. package/skills/browse/src/bun-polyfill.cjs +0 -109
  80. package/skills/browse/src/cli.ts +0 -678
  81. package/skills/browse/src/commands.ts +0 -128
  82. package/skills/browse/src/config.ts +0 -150
  83. package/skills/browse/src/cookie-import-browser.ts +0 -625
  84. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  85. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  86. package/skills/browse/src/find-browse.ts +0 -61
  87. package/skills/browse/src/meta-commands.ts +0 -550
  88. package/skills/browse/src/platform.ts +0 -17
  89. package/skills/browse/src/read-commands.ts +0 -358
  90. package/skills/browse/src/server.ts +0 -1192
  91. package/skills/browse/src/sidebar-agent.ts +0 -280
  92. package/skills/browse/src/sidebar-utils.ts +0 -21
  93. package/skills/browse/src/snapshot.ts +0 -407
  94. package/skills/browse/src/url-validation.ts +0 -95
  95. package/skills/browse/src/write-commands.ts +0 -364
  96. package/skills/browse/test/activity.test.ts +0 -120
  97. package/skills/browse/test/adversarial-security.test.ts +0 -32
  98. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  99. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  100. package/skills/browse/test/commands.test.ts +0 -2075
  101. package/skills/browse/test/compare-board.test.ts +0 -342
  102. package/skills/browse/test/config.test.ts +0 -316
  103. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  104. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  105. package/skills/browse/test/file-drop.test.ts +0 -271
  106. package/skills/browse/test/find-browse.test.ts +0 -50
  107. package/skills/browse/test/findport.test.ts +0 -191
  108. package/skills/browse/test/fixtures/basic.html +0 -33
  109. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  110. package/skills/browse/test/fixtures/dialog.html +0 -15
  111. package/skills/browse/test/fixtures/empty.html +0 -2
  112. package/skills/browse/test/fixtures/forms.html +0 -55
  113. package/skills/browse/test/fixtures/iframe.html +0 -30
  114. package/skills/browse/test/fixtures/network-idle.html +0 -30
  115. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  116. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  117. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  118. package/skills/browse/test/fixtures/responsive.html +0 -49
  119. package/skills/browse/test/fixtures/snapshot.html +0 -55
  120. package/skills/browse/test/fixtures/spa.html +0 -24
  121. package/skills/browse/test/fixtures/states.html +0 -17
  122. package/skills/browse/test/fixtures/upload.html +0 -25
  123. package/skills/browse/test/gstack-config.test.ts +0 -138
  124. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  125. package/skills/browse/test/handoff.test.ts +0 -235
  126. package/skills/browse/test/path-validation.test.ts +0 -91
  127. package/skills/browse/test/platform.test.ts +0 -37
  128. package/skills/browse/test/server-auth.test.ts +0 -65
  129. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  130. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  131. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  132. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  133. package/skills/browse/test/snapshot.test.ts +0 -467
  134. package/skills/browse/test/state-ttl.test.ts +0 -35
  135. package/skills/browse/test/test-server.ts +0 -57
  136. package/skills/browse/test/url-validation.test.ts +0 -72
  137. package/skills/browse/test/watch.test.ts +0 -129
  138. package/skills/canary/SKILL.md.tmpl +0 -212
  139. package/skills/careful/SKILL.md.tmpl +0 -56
  140. package/skills/careful/bin/check-careful.sh +0 -112
  141. package/skills/codex/SKILL.md +0 -90
  142. package/skills/codex/SKILL.md.tmpl +0 -417
  143. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  144. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  145. package/skills/cso/SKILL.md +0 -93
  146. package/skills/cso/SKILL.md.tmpl +0 -606
  147. package/skills/design-consultation/SKILL.md +0 -94
  148. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  149. package/skills/design-review/SKILL.md +0 -94
  150. package/skills/design-review/SKILL.md.tmpl +0 -290
  151. package/skills/design-shotgun/SKILL.md +0 -91
  152. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  153. package/skills/document-release/SKILL.md +0 -91
  154. package/skills/document-release/SKILL.md.tmpl +0 -359
  155. package/skills/freeze/SKILL.md.tmpl +0 -77
  156. package/skills/freeze/bin/check-freeze.sh +0 -79
  157. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  158. package/skills/guard/SKILL.md.tmpl +0 -77
  159. package/skills/investigate/SKILL.md +0 -105
  160. package/skills/investigate/SKILL.md.tmpl +0 -194
  161. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  162. package/skills/office-hours/SKILL.md +0 -96
  163. package/skills/office-hours/SKILL.md.tmpl +0 -645
  164. package/skills/plan-ceo-review/SKILL.md +0 -94
  165. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  166. package/skills/plan-design-review/SKILL.md +0 -92
  167. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  168. package/skills/plan-eng-review/SKILL.md +0 -93
  169. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  170. package/skills/qa/SKILL.md +0 -95
  171. package/skills/qa/SKILL.md.tmpl +0 -316
  172. package/skills/qa/references/issue-taxonomy.md +0 -85
  173. package/skills/qa/templates/qa-report-template.md +0 -126
  174. package/skills/qa-only/SKILL.md +0 -89
  175. package/skills/qa-only/SKILL.md.tmpl +0 -101
  176. package/skills/retro/SKILL.md +0 -89
  177. package/skills/retro/SKILL.md.tmpl +0 -820
  178. package/skills/review/SKILL.md +0 -92
  179. package/skills/review/SKILL.md.tmpl +0 -281
  180. package/skills/review/TODOS-format.md +0 -62
  181. package/skills/review/checklist.md +0 -220
  182. package/skills/review/design-checklist.md +0 -132
  183. package/skills/review/greptile-triage.md +0 -220
  184. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  185. package/skills/setup-deploy/SKILL.md +0 -92
  186. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  187. package/skills/ship/SKILL.md.tmpl +0 -636
  188. package/skills/unfreeze/SKILL.md +0 -37
  189. package/skills/unfreeze/SKILL.md.tmpl +0 -36
@@ -1,688 +0,0 @@
1
- /**
2
- * Cookie picker UI — self-contained HTML page
3
- *
4
- * Dark theme, two-panel layout, vanilla HTML/CSS/JS.
5
- * Left: source browser domains with search + import buttons.
6
- * Right: imported domains with trash buttons.
7
- * No cookie values exposed anywhere.
8
- */
9
-
10
- export function getCookiePickerHTML(serverPort: number, authToken?: string): string {
11
- const baseUrl = `http://127.0.0.1:${serverPort}`;
12
-
13
- return `<!DOCTYPE html>
14
- <html lang="en">
15
- <head>
16
- <meta charset="utf-8">
17
- <meta name="viewport" content="width=device-width, initial-scale=1">
18
- <title>Cookie Import — gstack browse</title>
19
- <style>
20
- * { margin: 0; padding: 0; box-sizing: border-box; }
21
- body {
22
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
23
- background: #0a0a0a;
24
- color: #e0e0e0;
25
- height: 100vh;
26
- overflow: hidden;
27
- }
28
-
29
- /* ─── Header ──────────────────────────── */
30
- .header {
31
- display: flex;
32
- align-items: center;
33
- justify-content: space-between;
34
- padding: 16px 24px;
35
- border-bottom: 1px solid #222;
36
- background: #0f0f0f;
37
- }
38
- .header h1 {
39
- font-size: 16px;
40
- font-weight: 600;
41
- color: #fff;
42
- }
43
- .header .port {
44
- font-size: 12px;
45
- color: #666;
46
- font-family: 'SF Mono', 'Fira Code', monospace;
47
- }
48
-
49
- /* ─── Layout ──────────────────────────── */
50
- .container {
51
- display: flex;
52
- height: calc(100vh - 53px);
53
- }
54
- .panel {
55
- flex: 1;
56
- display: flex;
57
- flex-direction: column;
58
- overflow: hidden;
59
- }
60
- .panel-left {
61
- border-right: 1px solid #222;
62
- }
63
- .panel-header {
64
- padding: 16px 20px 12px;
65
- font-size: 11px;
66
- font-weight: 600;
67
- text-transform: uppercase;
68
- letter-spacing: 0.5px;
69
- color: #888;
70
- }
71
-
72
- /* ─── Browser Pills ───────────────────── */
73
- .browser-pills {
74
- display: flex;
75
- gap: 8px;
76
- padding: 0 20px 12px;
77
- flex-wrap: wrap;
78
- }
79
- .pill {
80
- padding: 6px 14px;
81
- border-radius: 20px;
82
- border: 1px solid #333;
83
- background: #1a1a1a;
84
- color: #aaa;
85
- font-size: 13px;
86
- cursor: pointer;
87
- transition: all 0.15s;
88
- display: flex;
89
- align-items: center;
90
- gap: 6px;
91
- }
92
- .pill:hover { border-color: #555; color: #ddd; }
93
- .pill.active {
94
- border-color: #4ade80;
95
- background: #0a2a14;
96
- color: #4ade80;
97
- }
98
- .pill .dot {
99
- width: 6px; height: 6px;
100
- border-radius: 50%;
101
- background: #4ade80;
102
- }
103
-
104
- /* ─── Profile Pills ─────────────────── */
105
- .profile-pills {
106
- display: flex;
107
- gap: 6px;
108
- padding: 0 20px 12px;
109
- flex-wrap: wrap;
110
- }
111
- .profile-pill {
112
- padding: 4px 10px;
113
- border-radius: 14px;
114
- border: 1px solid #2a2a2a;
115
- background: #141414;
116
- color: #888;
117
- font-size: 12px;
118
- cursor: pointer;
119
- transition: all 0.15s;
120
- }
121
- .profile-pill:hover { border-color: #444; color: #bbb; }
122
- .profile-pill.active {
123
- border-color: #60a5fa;
124
- background: #0a1a2a;
125
- color: #60a5fa;
126
- }
127
-
128
- /* ─── Search ──────────────────────────── */
129
- .search-wrap {
130
- padding: 0 20px 12px;
131
- }
132
- .search-input {
133
- width: 100%;
134
- padding: 8px 12px;
135
- border-radius: 8px;
136
- border: 1px solid #333;
137
- background: #141414;
138
- color: #e0e0e0;
139
- font-size: 13px;
140
- outline: none;
141
- transition: border-color 0.15s;
142
- }
143
- .search-input::placeholder { color: #555; }
144
- .search-input:focus { border-color: #555; }
145
-
146
- /* ─── Domain List ─────────────────────── */
147
- .domain-list {
148
- flex: 1;
149
- overflow-y: auto;
150
- padding: 0 12px;
151
- }
152
- .domain-list::-webkit-scrollbar { width: 6px; }
153
- .domain-list::-webkit-scrollbar-track { background: transparent; }
154
- .domain-list::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
155
-
156
- .domain-row {
157
- display: flex;
158
- align-items: center;
159
- padding: 8px 10px;
160
- border-radius: 6px;
161
- transition: background 0.1s;
162
- gap: 8px;
163
- }
164
- .domain-row:hover { background: #1a1a1a; }
165
- .domain-name {
166
- flex: 1;
167
- font-family: 'SF Mono', 'Fira Code', monospace;
168
- font-size: 13px;
169
- color: #ccc;
170
- overflow: hidden;
171
- text-overflow: ellipsis;
172
- white-space: nowrap;
173
- }
174
- .domain-count {
175
- font-size: 12px;
176
- color: #666;
177
- font-family: 'SF Mono', 'Fira Code', monospace;
178
- min-width: 28px;
179
- text-align: right;
180
- }
181
- .btn-add, .btn-trash {
182
- width: 28px; height: 28px;
183
- border-radius: 6px;
184
- border: 1px solid #333;
185
- background: #1a1a1a;
186
- color: #888;
187
- font-size: 16px;
188
- cursor: pointer;
189
- display: flex;
190
- align-items: center;
191
- justify-content: center;
192
- transition: all 0.15s;
193
- flex-shrink: 0;
194
- }
195
- .btn-add:hover { border-color: #4ade80; color: #4ade80; background: #0a2a14; }
196
- .btn-trash:hover { border-color: #f87171; color: #f87171; background: #2a0a0a; }
197
- .btn-add:disabled, .btn-trash:disabled {
198
- opacity: 0.3;
199
- cursor: not-allowed;
200
- pointer-events: none;
201
- }
202
- .btn-add.imported {
203
- border-color: #333;
204
- color: #4ade80;
205
- background: transparent;
206
- cursor: default;
207
- font-size: 14px;
208
- }
209
-
210
- /* ─── Footer ──────────────────────────── */
211
- .panel-footer {
212
- padding: 12px 20px;
213
- border-top: 1px solid #222;
214
- font-size: 12px;
215
- color: #666;
216
- display: flex;
217
- align-items: center;
218
- justify-content: space-between;
219
- }
220
- .btn-import-all {
221
- padding: 4px 12px;
222
- border-radius: 6px;
223
- border: 1px solid #333;
224
- background: #1a1a1a;
225
- color: #4ade80;
226
- font-size: 12px;
227
- cursor: pointer;
228
- transition: all 0.15s;
229
- }
230
- .btn-import-all:hover { border-color: #4ade80; background: #0a2a14; }
231
- .btn-import-all:disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
232
-
233
- /* ─── Imported Panel ──────────────────── */
234
- .imported-empty {
235
- flex: 1;
236
- display: flex;
237
- align-items: center;
238
- justify-content: center;
239
- color: #444;
240
- font-size: 13px;
241
- padding: 20px;
242
- text-align: center;
243
- }
244
-
245
- /* ─── Banner ──────────────────────────── */
246
- .banner {
247
- padding: 10px 20px;
248
- font-size: 13px;
249
- display: none;
250
- align-items: center;
251
- gap: 10px;
252
- }
253
- .banner.error {
254
- background: #1a0a0a;
255
- border-bottom: 1px solid #3a1111;
256
- color: #f87171;
257
- }
258
- .banner.info {
259
- background: #0a1a2a;
260
- border-bottom: 1px solid #112233;
261
- color: #60a5fa;
262
- }
263
- .banner .banner-text { flex: 1; }
264
- .banner .banner-close, .banner .banner-retry {
265
- background: none;
266
- border: 1px solid currentColor;
267
- color: inherit;
268
- padding: 3px 10px;
269
- border-radius: 4px;
270
- cursor: pointer;
271
- font-size: 12px;
272
- }
273
-
274
- /* ─── Spinner ─────────────────────────── */
275
- .spinner {
276
- display: inline-block;
277
- width: 14px; height: 14px;
278
- border: 2px solid #333;
279
- border-top-color: #4ade80;
280
- border-radius: 50%;
281
- animation: spin 0.6s linear infinite;
282
- }
283
- @keyframes spin { to { transform: rotate(360deg); } }
284
-
285
- .loading-row {
286
- display: flex;
287
- align-items: center;
288
- justify-content: center;
289
- padding: 40px;
290
- gap: 10px;
291
- color: #666;
292
- font-size: 13px;
293
- }
294
- </style>
295
- </head>
296
- <body>
297
-
298
- <div class="header">
299
- <h1>Cookie Import</h1>
300
- <span class="port">localhost:${serverPort}</span>
301
- </div>
302
-
303
- <div id="banner" class="banner"></div>
304
-
305
- <div class="container">
306
- <!-- Left Panel: Source Browser -->
307
- <div class="panel panel-left">
308
- <div class="panel-header">Source Browser</div>
309
- <div id="browser-pills" class="browser-pills"></div>
310
- <div id="profile-pills" class="profile-pills" style="display:none"></div>
311
- <div class="search-wrap">
312
- <input type="text" class="search-input" id="search" placeholder="Search domains..." />
313
- </div>
314
- <div class="domain-list" id="source-domains">
315
- <div class="loading-row"><span class="spinner"></span> Detecting browsers...</div>
316
- </div>
317
- <div class="panel-footer" id="source-footer"><span id="source-footer-text"></span><button class="btn-import-all" id="btn-import-all" style="display:none">Import All</button></div>
318
- </div>
319
-
320
- <!-- Right Panel: Imported -->
321
- <div class="panel panel-right">
322
- <div class="panel-header">Imported to Session</div>
323
- <div class="domain-list" id="imported-domains">
324
- <div class="imported-empty">No cookies imported yet</div>
325
- </div>
326
- <div class="panel-footer" id="imported-footer"></div>
327
- </div>
328
- </div>
329
-
330
- <script>
331
- (function() {
332
- const BASE = '${baseUrl}';
333
- const AUTH_TOKEN = '${authToken || ''}';
334
- let activeBrowser = null;
335
- let activeProfile = 'Default';
336
- let allProfiles = [];
337
- let allDomains = [];
338
- let importedSet = {}; // domain → count
339
- let inflight = {}; // domain → true (prevents double-click)
340
-
341
- const $pills = document.getElementById('browser-pills');
342
- const $profilePills = document.getElementById('profile-pills');
343
- const $search = document.getElementById('search');
344
- const $sourceDomains = document.getElementById('source-domains');
345
- const $importedDomains = document.getElementById('imported-domains');
346
- const $sourceFooter = document.getElementById('source-footer-text');
347
- const $btnImportAll = document.getElementById('btn-import-all');
348
- const $importedFooter = document.getElementById('imported-footer');
349
- const $banner = document.getElementById('banner');
350
-
351
- // ─── Banner ────────────────────────────
352
- function showBanner(msg, type, retryFn) {
353
- $banner.className = 'banner ' + type;
354
- $banner.style.display = 'flex';
355
- let html = '<span class="banner-text">' + escHtml(msg) + '</span>';
356
- if (retryFn) {
357
- html += '<button class="banner-retry" id="banner-retry">Retry</button>';
358
- }
359
- html += '<button class="banner-close" id="banner-close">×</button>';
360
- $banner.innerHTML = html;
361
- document.getElementById('banner-close').onclick = () => { $banner.style.display = 'none'; };
362
- if (retryFn) {
363
- document.getElementById('banner-retry').onclick = () => {
364
- $banner.style.display = 'none';
365
- retryFn();
366
- };
367
- }
368
- }
369
-
370
- function escHtml(s) {
371
- return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
372
- }
373
-
374
- // ─── API ────────────────────────────────
375
- async function api(path, opts) {
376
- const headers = { ...(opts?.headers || {}) };
377
- if (AUTH_TOKEN) headers['Authorization'] = 'Bearer ' + AUTH_TOKEN;
378
- const res = await fetch(BASE + '/cookie-picker' + path, { ...opts, headers });
379
- const data = await res.json();
380
- if (!res.ok) {
381
- const err = new Error(data.error || 'Request failed');
382
- err.code = data.code;
383
- err.action = data.action;
384
- throw err;
385
- }
386
- return data;
387
- }
388
-
389
- // ─── Init ───────────────────────────────
390
- async function init() {
391
- try {
392
- const [browserData, importedData] = await Promise.all([
393
- api('/browsers'),
394
- api('/imported'),
395
- ]);
396
-
397
- // Populate imported state
398
- for (const entry of importedData.domains) {
399
- importedSet[entry.domain] = entry.count;
400
- }
401
- renderImported();
402
-
403
- // Render browser pills
404
- const browsers = browserData.browsers;
405
- if (browsers.length === 0) {
406
- $sourceDomains.innerHTML = '<div class="imported-empty">No Chromium browsers detected</div>';
407
- return;
408
- }
409
-
410
- $pills.innerHTML = '';
411
- browsers.forEach(b => {
412
- const pill = document.createElement('button');
413
- pill.className = 'pill';
414
- pill.innerHTML = '<span class="dot"></span>' + escHtml(b.name);
415
- pill.onclick = () => selectBrowser(b.name);
416
- $pills.appendChild(pill);
417
- });
418
-
419
- // Auto-select first browser
420
- selectBrowser(browsers[0].name);
421
- } catch (err) {
422
- showBanner(err.message, 'error', init);
423
- $sourceDomains.innerHTML = '<div class="imported-empty">Failed to load</div>';
424
- }
425
- }
426
-
427
- // ─── Select Browser ────────────────────
428
- async function selectBrowser(name) {
429
- activeBrowser = name;
430
- activeProfile = 'Default';
431
-
432
- // Update pills
433
- $pills.querySelectorAll('.pill').forEach(p => {
434
- p.classList.toggle('active', p.textContent === name);
435
- });
436
-
437
- $sourceDomains.innerHTML = '<div class="loading-row"><span class="spinner"></span> Loading...</div>';
438
- $sourceFooter.textContent = '';
439
- $search.value = '';
440
-
441
- try {
442
- // Fetch profiles for this browser
443
- const profileData = await api('/profiles?browser=' + encodeURIComponent(name));
444
- allProfiles = profileData.profiles || [];
445
-
446
- if (allProfiles.length > 1) {
447
- // Show profile pills when multiple profiles exist
448
- $profilePills.style.display = 'flex';
449
- renderProfilePills();
450
- // Auto-select profile with the most recent/largest cookie DB, or Default
451
- activeProfile = allProfiles[0].name;
452
- } else {
453
- $profilePills.style.display = 'none';
454
- activeProfile = allProfiles.length === 1 ? allProfiles[0].name : 'Default';
455
- }
456
-
457
- await loadDomains();
458
- } catch (err) {
459
- showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null);
460
- $sourceDomains.innerHTML = '<div class="imported-empty">Failed to load</div>';
461
- $profilePills.style.display = 'none';
462
- }
463
- }
464
-
465
- // ─── Render Profile Pills ─────────────
466
- function renderProfilePills() {
467
- let html = '';
468
- for (const p of allProfiles) {
469
- const isActive = p.name === activeProfile;
470
- const label = p.displayName || p.name;
471
- html += '<button class="profile-pill' + (isActive ? ' active' : '') + '" data-profile="' + escHtml(p.name) + '">' + escHtml(label) + '</button>';
472
- }
473
- $profilePills.innerHTML = html;
474
-
475
- $profilePills.querySelectorAll('.profile-pill').forEach(btn => {
476
- btn.addEventListener('click', () => selectProfile(btn.dataset.profile));
477
- });
478
- }
479
-
480
- // ─── Select Profile ───────────────────
481
- async function selectProfile(profileName) {
482
- activeProfile = profileName;
483
- renderProfilePills();
484
-
485
- $sourceDomains.innerHTML = '<div class="loading-row"><span class="spinner"></span> Loading domains...</div>';
486
- $sourceFooter.textContent = '';
487
- $search.value = '';
488
-
489
- await loadDomains();
490
- }
491
-
492
- // ─── Load Domains ─────────────────────
493
- async function loadDomains() {
494
- try {
495
- const data = await api('/domains?browser=' + encodeURIComponent(activeBrowser) + '&profile=' + encodeURIComponent(activeProfile));
496
- allDomains = data.domains;
497
- renderSourceDomains();
498
- } catch (err) {
499
- showBanner(err.message, 'error', err.action === 'retry' ? () => loadDomains() : null);
500
- $sourceDomains.innerHTML = '<div class="imported-empty">Failed to load domains</div>';
501
- }
502
- }
503
-
504
- // ─── Render Source Domains ─────────────
505
- function renderSourceDomains() {
506
- const query = $search.value.toLowerCase();
507
- const filtered = query
508
- ? allDomains.filter(d => d.domain.toLowerCase().includes(query))
509
- : allDomains;
510
-
511
- if (filtered.length === 0) {
512
- $sourceDomains.innerHTML = '<div class="imported-empty">' +
513
- (query ? 'No matching domains' : 'No cookie domains found') + '</div>';
514
- $sourceFooter.textContent = '';
515
- return;
516
- }
517
-
518
- let html = '';
519
- for (const d of filtered) {
520
- const isImported = d.domain in importedSet;
521
- const isInflight = inflight[d.domain];
522
- html += '<div class="domain-row">';
523
- html += '<span class="domain-name">' + escHtml(d.domain) + '</span>';
524
- html += '<span class="domain-count">' + d.count + '</span>';
525
- if (isInflight) {
526
- html += '<span class="btn-add" disabled><span class="spinner" style="width:12px;height:12px;border-width:1.5px;"></span></span>';
527
- } else if (isImported) {
528
- html += '<span class="btn-add imported">&#10003;</span>';
529
- } else {
530
- html += '<button class="btn-add" data-domain="' + escHtml(d.domain) + '" title="Import">+</button>';
531
- }
532
- html += '</div>';
533
- }
534
- $sourceDomains.innerHTML = html;
535
-
536
- // Total counts
537
- const totalDomains = allDomains.length;
538
- const totalCookies = allDomains.reduce((s, d) => s + d.count, 0);
539
- $sourceFooter.textContent = totalDomains + ' domains · ' + totalCookies.toLocaleString() + ' cookies';
540
-
541
- // Show/hide Import All button
542
- const unimported = filtered.filter(d => !(d.domain in importedSet) && !inflight[d.domain]);
543
- if (unimported.length > 0) {
544
- $btnImportAll.style.display = '';
545
- $btnImportAll.disabled = false;
546
- $btnImportAll.textContent = 'Import All (' + unimported.length + ')';
547
- } else {
548
- $btnImportAll.style.display = 'none';
549
- }
550
-
551
- // Click handlers
552
- $sourceDomains.querySelectorAll('.btn-add[data-domain]').forEach(btn => {
553
- btn.addEventListener('click', () => importDomain(btn.dataset.domain));
554
- });
555
- }
556
-
557
- // ─── Import Domain ─────────────────────
558
- async function importDomain(domain) {
559
- if (inflight[domain] || domain in importedSet) return;
560
- inflight[domain] = true;
561
- renderSourceDomains();
562
-
563
- try {
564
- const data = await api('/import', {
565
- method: 'POST',
566
- headers: { 'Content-Type': 'application/json' },
567
- body: JSON.stringify({ browser: activeBrowser, domains: [domain], profile: activeProfile }),
568
- });
569
-
570
- if (data.domainCounts) {
571
- for (const [d, count] of Object.entries(data.domainCounts)) {
572
- importedSet[d] = (importedSet[d] || 0) + count;
573
- }
574
- }
575
- renderImported();
576
- } catch (err) {
577
- showBanner('Import failed for ' + domain + ': ' + err.message, 'error',
578
- err.action === 'retry' ? () => importDomain(domain) : null);
579
- } finally {
580
- delete inflight[domain];
581
- renderSourceDomains();
582
- }
583
- }
584
-
585
- // ─── Import All ───────────────────────
586
- async function importAll() {
587
- const query = $search.value.toLowerCase();
588
- const filtered = query
589
- ? allDomains.filter(d => d.domain.toLowerCase().includes(query))
590
- : allDomains;
591
- const toImport = filtered.filter(d => !(d.domain in importedSet) && !inflight[d.domain]);
592
- if (toImport.length === 0) return;
593
-
594
- $btnImportAll.disabled = true;
595
- $btnImportAll.textContent = 'Importing...';
596
-
597
- const domains = toImport.map(d => d.domain);
598
- try {
599
- const data = await api('/import', {
600
- method: 'POST',
601
- headers: { 'Content-Type': 'application/json' },
602
- body: JSON.stringify({ browser: activeBrowser, domains: domains, profile: activeProfile }),
603
- });
604
-
605
- if (data.domainCounts) {
606
- for (const [d, count] of Object.entries(data.domainCounts)) {
607
- importedSet[d] = (importedSet[d] || 0) + count;
608
- }
609
- }
610
- renderImported();
611
- } catch (err) {
612
- showBanner('Import all failed: ' + err.message, 'error',
613
- err.action === 'retry' ? () => importAll() : null);
614
- } finally {
615
- renderSourceDomains();
616
- }
617
- }
618
-
619
- $btnImportAll.addEventListener('click', importAll);
620
-
621
- // ─── Render Imported ───────────────────
622
- function renderImported() {
623
- const entries = Object.entries(importedSet).sort((a, b) => b[1] - a[1]);
624
-
625
- if (entries.length === 0) {
626
- $importedDomains.innerHTML = '<div class="imported-empty">No cookies imported yet</div>';
627
- $importedFooter.textContent = '';
628
- return;
629
- }
630
-
631
- let html = '';
632
- for (const [domain, count] of entries) {
633
- const isInflight = inflight['remove:' + domain];
634
- html += '<div class="domain-row">';
635
- html += '<span class="domain-name">' + escHtml(domain) + '</span>';
636
- html += '<span class="domain-count">' + count + '</span>';
637
- if (isInflight) {
638
- html += '<span class="btn-trash" disabled><span class="spinner" style="width:12px;height:12px;border-width:1.5px;border-top-color:#f87171;"></span></span>';
639
- } else {
640
- html += '<button class="btn-trash" data-domain="' + escHtml(domain) + '" title="Remove">&#128465;</button>';
641
- }
642
- html += '</div>';
643
- }
644
- $importedDomains.innerHTML = html;
645
-
646
- const totalCookies = entries.reduce((s, e) => s + e[1], 0);
647
- $importedFooter.textContent = entries.length + ' domains · ' + totalCookies.toLocaleString() + ' cookies imported';
648
-
649
- // Click handlers
650
- $importedDomains.querySelectorAll('.btn-trash[data-domain]').forEach(btn => {
651
- btn.addEventListener('click', () => removeDomain(btn.dataset.domain));
652
- });
653
- }
654
-
655
- // ─── Remove Domain ─────────────────────
656
- async function removeDomain(domain) {
657
- if (inflight['remove:' + domain]) return;
658
- inflight['remove:' + domain] = true;
659
- renderImported();
660
-
661
- try {
662
- await api('/remove', {
663
- method: 'POST',
664
- headers: { 'Content-Type': 'application/json' },
665
- body: JSON.stringify({ domains: [domain] }),
666
- });
667
- delete importedSet[domain];
668
- renderImported();
669
- renderSourceDomains(); // update checkmarks
670
- } catch (err) {
671
- showBanner('Remove failed for ' + domain + ': ' + err.message, 'error',
672
- err.action === 'retry' ? () => removeDomain(domain) : null);
673
- } finally {
674
- delete inflight['remove:' + domain];
675
- renderImported();
676
- }
677
- }
678
-
679
- // ─── Search ────────────────────────────
680
- $search.addEventListener('input', renderSourceDomains);
681
-
682
- // ─── Start ─────────────────────────────
683
- init();
684
- })();
685
- </script>
686
- </body>
687
- </html>`;
688
- }