opengstack 0.13.9 → 0.14.0

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 (152) hide show
  1. package/{skills/land-and-deploy/SKILL.md → commands/autoplan.md} +0 -16
  2. package/{skills/benchmark/SKILL.md → commands/benchmark.md} +0 -17
  3. package/{skills/browse/SKILL.md → commands/browse.md} +0 -17
  4. package/{skills/ship/SKILL.md → commands/canary.md} +0 -18
  5. package/{skills/careful/SKILL.md → commands/careful.md} +0 -20
  6. package/{skills/canary/SKILL.md → commands/codex.md} +0 -17
  7. package/{skills/connect-chrome/SKILL.md → commands/connect-chrome.md} +0 -15
  8. package/commands/cso.md +72 -0
  9. package/commands/design-consultation.md +72 -0
  10. package/commands/design-review.md +72 -0
  11. package/commands/design-shotgun.md +72 -0
  12. package/commands/document-release.md +72 -0
  13. package/{skills/freeze/SKILL.md → commands/freeze.md} +0 -26
  14. package/{skills/gstack-upgrade/SKILL.md → commands/gstack-upgrade.md} +0 -14
  15. package/{skills/guard/SKILL.md → commands/guard.md} +0 -31
  16. package/commands/investigate.md +72 -0
  17. package/commands/land-and-deploy.md +72 -0
  18. package/commands/office-hours.md +72 -0
  19. package/commands/plan-ceo-review.md +72 -0
  20. package/commands/plan-design-review.md +72 -0
  21. package/commands/plan-eng-review.md +72 -0
  22. package/commands/qa-only.md +72 -0
  23. package/commands/qa.md +72 -0
  24. package/commands/retro.md +72 -0
  25. package/commands/review.md +72 -0
  26. package/{skills/setup-browser-cookies/SKILL.md → commands/setup-browser-cookies.md} +0 -14
  27. package/commands/setup-deploy.md +72 -0
  28. package/commands/ship.md +72 -0
  29. package/{skills/unfreeze/SKILL.md → commands/unfreeze.md} +0 -12
  30. package/package.json +4 -4
  31. package/scripts/install-commands.js +45 -0
  32. package/scripts/install-skills.js +4 -7
  33. package/skills/autoplan/SKILL.md +0 -96
  34. package/skills/autoplan/SKILL.md.tmpl +0 -694
  35. package/skills/benchmark/SKILL.md.tmpl +0 -222
  36. package/skills/browse/SKILL.md.tmpl +0 -131
  37. package/skills/browse/bin/find-browse +0 -21
  38. package/skills/browse/bin/remote-slug +0 -14
  39. package/skills/browse/scripts/build-node-server.sh +0 -48
  40. package/skills/browse/src/activity.ts +0 -208
  41. package/skills/browse/src/browser-manager.ts +0 -959
  42. package/skills/browse/src/buffers.ts +0 -137
  43. package/skills/browse/src/bun-polyfill.cjs +0 -109
  44. package/skills/browse/src/cli.ts +0 -678
  45. package/skills/browse/src/commands.ts +0 -128
  46. package/skills/browse/src/config.ts +0 -150
  47. package/skills/browse/src/cookie-import-browser.ts +0 -625
  48. package/skills/browse/src/cookie-picker-routes.ts +0 -230
  49. package/skills/browse/src/cookie-picker-ui.ts +0 -688
  50. package/skills/browse/src/find-browse.ts +0 -61
  51. package/skills/browse/src/meta-commands.ts +0 -550
  52. package/skills/browse/src/platform.ts +0 -17
  53. package/skills/browse/src/read-commands.ts +0 -358
  54. package/skills/browse/src/server.ts +0 -1192
  55. package/skills/browse/src/sidebar-agent.ts +0 -280
  56. package/skills/browse/src/sidebar-utils.ts +0 -21
  57. package/skills/browse/src/snapshot.ts +0 -407
  58. package/skills/browse/src/url-validation.ts +0 -95
  59. package/skills/browse/src/write-commands.ts +0 -364
  60. package/skills/browse/test/activity.test.ts +0 -120
  61. package/skills/browse/test/adversarial-security.test.ts +0 -32
  62. package/skills/browse/test/browser-manager-unit.test.ts +0 -17
  63. package/skills/browse/test/bun-polyfill.test.ts +0 -72
  64. package/skills/browse/test/commands.test.ts +0 -2075
  65. package/skills/browse/test/compare-board.test.ts +0 -342
  66. package/skills/browse/test/config.test.ts +0 -316
  67. package/skills/browse/test/cookie-import-browser.test.ts +0 -519
  68. package/skills/browse/test/cookie-picker-routes.test.ts +0 -260
  69. package/skills/browse/test/file-drop.test.ts +0 -271
  70. package/skills/browse/test/find-browse.test.ts +0 -50
  71. package/skills/browse/test/findport.test.ts +0 -191
  72. package/skills/browse/test/fixtures/basic.html +0 -33
  73. package/skills/browse/test/fixtures/cursor-interactive.html +0 -22
  74. package/skills/browse/test/fixtures/dialog.html +0 -15
  75. package/skills/browse/test/fixtures/empty.html +0 -2
  76. package/skills/browse/test/fixtures/forms.html +0 -55
  77. package/skills/browse/test/fixtures/iframe.html +0 -30
  78. package/skills/browse/test/fixtures/network-idle.html +0 -30
  79. package/skills/browse/test/fixtures/qa-eval-checkout.html +0 -108
  80. package/skills/browse/test/fixtures/qa-eval-spa.html +0 -98
  81. package/skills/browse/test/fixtures/qa-eval.html +0 -51
  82. package/skills/browse/test/fixtures/responsive.html +0 -49
  83. package/skills/browse/test/fixtures/snapshot.html +0 -55
  84. package/skills/browse/test/fixtures/spa.html +0 -24
  85. package/skills/browse/test/fixtures/states.html +0 -17
  86. package/skills/browse/test/fixtures/upload.html +0 -25
  87. package/skills/browse/test/gstack-config.test.ts +0 -138
  88. package/skills/browse/test/gstack-update-check.test.ts +0 -514
  89. package/skills/browse/test/handoff.test.ts +0 -235
  90. package/skills/browse/test/path-validation.test.ts +0 -91
  91. package/skills/browse/test/platform.test.ts +0 -37
  92. package/skills/browse/test/server-auth.test.ts +0 -65
  93. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +0 -226
  94. package/skills/browse/test/sidebar-agent.test.ts +0 -199
  95. package/skills/browse/test/sidebar-integration.test.ts +0 -320
  96. package/skills/browse/test/sidebar-unit.test.ts +0 -96
  97. package/skills/browse/test/snapshot.test.ts +0 -467
  98. package/skills/browse/test/state-ttl.test.ts +0 -35
  99. package/skills/browse/test/test-server.ts +0 -57
  100. package/skills/browse/test/url-validation.test.ts +0 -72
  101. package/skills/browse/test/watch.test.ts +0 -129
  102. package/skills/canary/SKILL.md.tmpl +0 -212
  103. package/skills/careful/SKILL.md.tmpl +0 -56
  104. package/skills/careful/bin/check-careful.sh +0 -112
  105. package/skills/codex/SKILL.md +0 -90
  106. package/skills/codex/SKILL.md.tmpl +0 -417
  107. package/skills/connect-chrome/SKILL.md.tmpl +0 -195
  108. package/skills/cso/ACKNOWLEDGEMENTS.md +0 -14
  109. package/skills/cso/SKILL.md +0 -93
  110. package/skills/cso/SKILL.md.tmpl +0 -606
  111. package/skills/design-consultation/SKILL.md +0 -94
  112. package/skills/design-consultation/SKILL.md.tmpl +0 -415
  113. package/skills/design-review/SKILL.md +0 -94
  114. package/skills/design-review/SKILL.md.tmpl +0 -290
  115. package/skills/design-shotgun/SKILL.md +0 -91
  116. package/skills/design-shotgun/SKILL.md.tmpl +0 -285
  117. package/skills/document-release/SKILL.md +0 -91
  118. package/skills/document-release/SKILL.md.tmpl +0 -359
  119. package/skills/freeze/SKILL.md.tmpl +0 -77
  120. package/skills/freeze/bin/check-freeze.sh +0 -79
  121. package/skills/gstack-upgrade/SKILL.md.tmpl +0 -222
  122. package/skills/guard/SKILL.md.tmpl +0 -77
  123. package/skills/investigate/SKILL.md +0 -105
  124. package/skills/investigate/SKILL.md.tmpl +0 -194
  125. package/skills/land-and-deploy/SKILL.md.tmpl +0 -881
  126. package/skills/office-hours/SKILL.md +0 -96
  127. package/skills/office-hours/SKILL.md.tmpl +0 -645
  128. package/skills/plan-ceo-review/SKILL.md +0 -94
  129. package/skills/plan-ceo-review/SKILL.md.tmpl +0 -811
  130. package/skills/plan-design-review/SKILL.md +0 -92
  131. package/skills/plan-design-review/SKILL.md.tmpl +0 -446
  132. package/skills/plan-eng-review/SKILL.md +0 -93
  133. package/skills/plan-eng-review/SKILL.md.tmpl +0 -303
  134. package/skills/qa/SKILL.md +0 -95
  135. package/skills/qa/SKILL.md.tmpl +0 -316
  136. package/skills/qa/references/issue-taxonomy.md +0 -85
  137. package/skills/qa/templates/qa-report-template.md +0 -126
  138. package/skills/qa-only/SKILL.md +0 -89
  139. package/skills/qa-only/SKILL.md.tmpl +0 -101
  140. package/skills/retro/SKILL.md +0 -89
  141. package/skills/retro/SKILL.md.tmpl +0 -820
  142. package/skills/review/SKILL.md +0 -92
  143. package/skills/review/SKILL.md.tmpl +0 -281
  144. package/skills/review/TODOS-format.md +0 -62
  145. package/skills/review/checklist.md +0 -220
  146. package/skills/review/design-checklist.md +0 -132
  147. package/skills/review/greptile-triage.md +0 -220
  148. package/skills/setup-browser-cookies/SKILL.md.tmpl +0 -81
  149. package/skills/setup-deploy/SKILL.md +0 -92
  150. package/skills/setup-deploy/SKILL.md.tmpl +0 -215
  151. package/skills/ship/SKILL.md.tmpl +0 -636
  152. package/skills/unfreeze/SKILL.md.tmpl +0 -36
@@ -1,364 +0,0 @@
1
- /**
2
- * Write commands — navigate and interact with pages (side effects)
3
- *
4
- * goto, back, forward, reload, click, fill, select, hover, type,
5
- * press, scroll, wait, viewport, cookie, header, useragent
6
- */
7
-
8
- import type { BrowserManager } from './browser-manager';
9
- import { findInstalledBrowsers, importCookies, listSupportedBrowserNames } from './cookie-import-browser';
10
- import { validateNavigationUrl } from './url-validation';
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import { TEMP_DIR, isPathWithin } from './platform';
14
-
15
- export async function handleWriteCommand(
16
- command: string,
17
- args: string[],
18
- bm: BrowserManager
19
- ): Promise<string> {
20
- const page = bm.getPage();
21
- // Frame-aware target for locator-based operations (click, fill, etc.)
22
- const target = bm.getActiveFrameOrPage();
23
- const inFrame = bm.getFrame() !== null;
24
-
25
- switch (command) {
26
- case 'goto': {
27
- if (inFrame) throw new Error('Cannot use goto inside a frame. Run \'frame main\' first.');
28
- const url = args[0];
29
- if (!url) throw new Error('Usage: browse goto <url>');
30
- await validateNavigationUrl(url);
31
- const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
32
- const status = response?.status() || 'unknown';
33
- return `Navigated to ${url} (${status})`;
34
- }
35
-
36
- case 'back': {
37
- if (inFrame) throw new Error('Cannot use back inside a frame. Run \'frame main\' first.');
38
- await page.goBack({ waitUntil: 'domcontentloaded', timeout: 15000 });
39
- return `Back → ${page.url()}`;
40
- }
41
-
42
- case 'forward': {
43
- if (inFrame) throw new Error('Cannot use forward inside a frame. Run \'frame main\' first.');
44
- await page.goForward({ waitUntil: 'domcontentloaded', timeout: 15000 });
45
- return `Forward → ${page.url()}`;
46
- }
47
-
48
- case 'reload': {
49
- if (inFrame) throw new Error('Cannot use reload inside a frame. Run \'frame main\' first.');
50
- await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 });
51
- return `Reloaded ${page.url()}`;
52
- }
53
-
54
- case 'click': {
55
- const selector = args[0];
56
- if (!selector) throw new Error('Usage: browse click <selector>');
57
-
58
- // Auto-route: if ref points to a real <option> inside a <select>, use selectOption
59
- const role = bm.getRefRole(selector);
60
- if (role === 'option') {
61
- const resolved = await bm.resolveRef(selector);
62
- if ('locator' in resolved) {
63
- const optionInfo = await resolved.locator.evaluate(el => {
64
- if (el.tagName !== 'OPTION') return null; // custom [role=option], not real <option>
65
- const option = el as HTMLOptionElement;
66
- const select = option.closest('select');
67
- if (!select) return null;
68
- return { value: option.value, text: option.text };
69
- });
70
- if (optionInfo) {
71
- await resolved.locator.locator('xpath=ancestor::select').selectOption(optionInfo.value, { timeout: 5000 });
72
- return `Selected "${optionInfo.text}" (auto-routed from click on <option>) → now at ${page.url()}`;
73
- }
74
- // Real <option> with no parent <select> or custom [role=option] — fall through to normal click
75
- }
76
- }
77
-
78
- const resolved = await bm.resolveRef(selector);
79
- try {
80
- if ('locator' in resolved) {
81
- await resolved.locator.click({ timeout: 5000 });
82
- } else {
83
- await target.locator(resolved.selector).click({ timeout: 5000 });
84
- }
85
- } catch (err: any) {
86
- // Enhanced error guidance: clicking <option> elements always fails (not visible / timeout)
87
- const isOption = 'locator' in resolved
88
- ? await resolved.locator.evaluate(el => el.tagName === 'OPTION').catch(() => false)
89
- : await target.locator(resolved.selector).evaluate(
90
- el => el.tagName === 'OPTION'
91
- ).catch(() => false);
92
- if (isOption) {
93
- throw new Error(
94
- `Cannot click <option> elements. Use 'browse select <parent-select> <value>' instead of 'click' for dropdown options.`
95
- );
96
- }
97
- throw err;
98
- }
99
- // Wait for network to settle (catches XHR/fetch triggered by clicks)
100
- await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
101
- return `Clicked ${selector} → now at ${page.url()}`;
102
- }
103
-
104
- case 'fill': {
105
- const [selector, ...valueParts] = args;
106
- const value = valueParts.join(' ');
107
- if (!selector || !value) throw new Error('Usage: browse fill <selector> <value>');
108
- const resolved = await bm.resolveRef(selector);
109
- if ('locator' in resolved) {
110
- await resolved.locator.fill(value, { timeout: 5000 });
111
- } else {
112
- await target.locator(resolved.selector).fill(value, { timeout: 5000 });
113
- }
114
- // Wait for network to settle (form validation XHRs)
115
- await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
116
- return `Filled ${selector}`;
117
- }
118
-
119
- case 'select': {
120
- const [selector, ...valueParts] = args;
121
- const value = valueParts.join(' ');
122
- if (!selector || !value) throw new Error('Usage: browse select <selector> <value>');
123
- const resolved = await bm.resolveRef(selector);
124
- if ('locator' in resolved) {
125
- await resolved.locator.selectOption(value, { timeout: 5000 });
126
- } else {
127
- await target.locator(resolved.selector).selectOption(value, { timeout: 5000 });
128
- }
129
- // Wait for network to settle (dropdown-triggered requests)
130
- await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
131
- return `Selected "${value}" in ${selector}`;
132
- }
133
-
134
- case 'hover': {
135
- const selector = args[0];
136
- if (!selector) throw new Error('Usage: browse hover <selector>');
137
- const resolved = await bm.resolveRef(selector);
138
- if ('locator' in resolved) {
139
- await resolved.locator.hover({ timeout: 5000 });
140
- } else {
141
- await target.locator(resolved.selector).hover({ timeout: 5000 });
142
- }
143
- return `Hovered ${selector}`;
144
- }
145
-
146
- case 'type': {
147
- const text = args.join(' ');
148
- if (!text) throw new Error('Usage: browse type <text>');
149
- await page.keyboard.type(text);
150
- return `Typed ${text.length} characters`;
151
- }
152
-
153
- case 'press': {
154
- const key = args[0];
155
- if (!key) throw new Error('Usage: browse press <key> (e.g., Enter, Tab, Escape)');
156
- await page.keyboard.press(key);
157
- return `Pressed ${key}`;
158
- }
159
-
160
- case 'scroll': {
161
- const selector = args[0];
162
- if (selector) {
163
- const resolved = await bm.resolveRef(selector);
164
- if ('locator' in resolved) {
165
- await resolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 });
166
- } else {
167
- await target.locator(resolved.selector).scrollIntoViewIfNeeded({ timeout: 5000 });
168
- }
169
- return `Scrolled ${selector} into view`;
170
- }
171
- await target.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
172
- return 'Scrolled to bottom';
173
- }
174
-
175
- case 'wait': {
176
- const selector = args[0];
177
- if (!selector) throw new Error('Usage: browse wait <selector|--networkidle|--load|--domcontentloaded>');
178
- if (selector === '--networkidle') {
179
- const timeout = args[1] ? parseInt(args[1], 10) : 15000;
180
- await page.waitForLoadState('networkidle', { timeout });
181
- return 'Network idle';
182
- }
183
- if (selector === '--load') {
184
- await page.waitForLoadState('load');
185
- return 'Page loaded';
186
- }
187
- if (selector === '--domcontentloaded') {
188
- await page.waitForLoadState('domcontentloaded');
189
- return 'DOM content loaded';
190
- }
191
- const timeout = args[1] ? parseInt(args[1], 10) : 15000;
192
- const resolved = await bm.resolveRef(selector);
193
- if ('locator' in resolved) {
194
- await resolved.locator.waitFor({ state: 'visible', timeout });
195
- } else {
196
- await target.locator(resolved.selector).waitFor({ state: 'visible', timeout });
197
- }
198
- return `Element ${selector} appeared`;
199
- }
200
-
201
- case 'viewport': {
202
- const size = args[0];
203
- if (!size || !size.includes('x')) throw new Error('Usage: browse viewport <WxH> (e.g., 375x812)');
204
- const [w, h] = size.split('x').map(Number);
205
- await bm.setViewport(w, h);
206
- return `Viewport set to ${w}x${h}`;
207
- }
208
-
209
- case 'cookie': {
210
- const cookieStr = args[0];
211
- if (!cookieStr || !cookieStr.includes('=')) throw new Error('Usage: browse cookie <name>=<value>');
212
- const eq = cookieStr.indexOf('=');
213
- const name = cookieStr.slice(0, eq);
214
- const value = cookieStr.slice(eq + 1);
215
- const url = new URL(page.url());
216
- await page.context().addCookies([{
217
- name,
218
- value,
219
- domain: url.hostname,
220
- path: '/',
221
- }]);
222
- return `Cookie set: ${name}=****`;
223
- }
224
-
225
- case 'header': {
226
- const headerStr = args[0];
227
- if (!headerStr || !headerStr.includes(':')) throw new Error('Usage: browse header <name>:<value>');
228
- const sep = headerStr.indexOf(':');
229
- const name = headerStr.slice(0, sep).trim();
230
- const value = headerStr.slice(sep + 1).trim();
231
- await bm.setExtraHeader(name, value);
232
- const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token'];
233
- const redactedValue = sensitiveHeaders.includes(name.toLowerCase()) ? '****' : value;
234
- return `Header set: ${name}: ${redactedValue}`;
235
- }
236
-
237
- case 'useragent': {
238
- const ua = args.join(' ');
239
- if (!ua) throw new Error('Usage: browse useragent <string>');
240
- bm.setUserAgent(ua);
241
- const error = await bm.recreateContext();
242
- if (error) {
243
- return `User agent set to "${ua}" but: ${error}`;
244
- }
245
- return `User agent set: ${ua}`;
246
- }
247
-
248
- case 'upload': {
249
- const [selector, ...filePaths] = args;
250
- if (!selector || filePaths.length === 0) throw new Error('Usage: browse upload <selector> <file1> [file2...]');
251
-
252
- // Validate all files exist before upload
253
- for (const fp of filePaths) {
254
- if (!fs.existsSync(fp)) throw new Error(`File not found: ${fp}`);
255
- }
256
-
257
- const resolved = await bm.resolveRef(selector);
258
- if ('locator' in resolved) {
259
- await resolved.locator.setInputFiles(filePaths);
260
- } else {
261
- await target.locator(resolved.selector).setInputFiles(filePaths);
262
- }
263
-
264
- const fileInfo = filePaths.map(fp => {
265
- const stat = fs.statSync(fp);
266
- return `${path.basename(fp)} (${stat.size}B)`;
267
- }).join(', ');
268
- return `Uploaded: ${fileInfo}`;
269
- }
270
-
271
- case 'dialog-accept': {
272
- const text = args.length > 0 ? args.join(' ') : null;
273
- bm.setDialogAutoAccept(true);
274
- bm.setDialogPromptText(text);
275
- return text
276
- ? `Dialogs will be accepted with text: "${text}"`
277
- : 'Dialogs will be accepted';
278
- }
279
-
280
- case 'dialog-dismiss': {
281
- bm.setDialogAutoAccept(false);
282
- bm.setDialogPromptText(null);
283
- return 'Dialogs will be dismissed';
284
- }
285
-
286
- case 'cookie-import': {
287
- const filePath = args[0];
288
- if (!filePath) throw new Error('Usage: browse cookie-import <json-file>');
289
- // Path validation — prevent reading arbitrary files
290
- if (path.isAbsolute(filePath)) {
291
- const safeDirs = [TEMP_DIR, process.cwd()];
292
- const resolved = path.resolve(filePath);
293
- if (!safeDirs.some(dir => isPathWithin(resolved, dir))) {
294
- throw new Error(`Path must be within: ${safeDirs.join(', ')}`);
295
- }
296
- }
297
- if (path.normalize(filePath).includes('..')) {
298
- throw new Error('Path traversal sequences (..) are not allowed');
299
- }
300
- if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
301
- const raw = fs.readFileSync(filePath, 'utf-8');
302
- let cookies: any[];
303
- try { cookies = JSON.parse(raw); } catch { throw new Error(`Invalid JSON in ${filePath}`); }
304
- if (!Array.isArray(cookies)) throw new Error('Cookie file must contain a JSON array');
305
-
306
- // Auto-fill domain from current page URL when missing (consistent with cookie command)
307
- const pageUrl = new URL(page.url());
308
- const defaultDomain = pageUrl.hostname;
309
-
310
- for (const c of cookies) {
311
- if (!c.name || c.value === undefined) throw new Error('Each cookie must have "name" and "value" fields');
312
- if (!c.domain) c.domain = defaultDomain;
313
- if (!c.path) c.path = '/';
314
- }
315
-
316
- await page.context().addCookies(cookies);
317
- return `Loaded ${cookies.length} cookies from ${filePath}`;
318
- }
319
-
320
- case 'cookie-import-browser': {
321
- // Two modes:
322
- // 1. Direct CLI import: cookie-import-browser <browser> --domain <domain> [--profile <profile>]
323
- // 2. Open picker UI: cookie-import-browser [browser]
324
- const browserArg = args[0];
325
- const domainIdx = args.indexOf('--domain');
326
- const profileIdx = args.indexOf('--profile');
327
- const profile = (profileIdx !== -1 && profileIdx + 1 < args.length) ? args[profileIdx + 1] : 'Default';
328
-
329
- if (domainIdx !== -1 && domainIdx + 1 < args.length) {
330
- // Direct import mode — no UI
331
- const domain = args[domainIdx + 1];
332
- const browser = browserArg || 'comet';
333
- const result = await importCookies(browser, [domain], profile);
334
- if (result.cookies.length > 0) {
335
- await page.context().addCookies(result.cookies);
336
- }
337
- const msg = [`Imported ${result.count} cookies for ${domain} from ${browser}`];
338
- if (result.failed > 0) msg.push(`(${result.failed} failed to decrypt)`);
339
- return msg.join(' ');
340
- }
341
-
342
- // Picker UI mode — open in user's browser
343
- const port = bm.serverPort;
344
- if (!port) throw new Error('Server port not available');
345
-
346
- const browsers = findInstalledBrowsers();
347
- if (browsers.length === 0) {
348
- throw new Error(`No Chromium browsers found. Supported: ${listSupportedBrowserNames().join(', ')}`);
349
- }
350
-
351
- const pickerUrl = `http://127.0.0.1:${port}/cookie-picker`;
352
- try {
353
- Bun.spawn(['open', pickerUrl], { stdout: 'ignore', stderr: 'ignore' });
354
- } catch {
355
- // open may fail silently — URL is in the message below
356
- }
357
-
358
- return `Cookie picker opened at ${pickerUrl}\nDetected browsers: ${browsers.map(b => b.name).join(', ')}\nSelect domains to import, then close the picker when done.`;
359
- }
360
-
361
- default:
362
- throw new Error(`Unknown write command: ${command}`);
363
- }
364
- }
@@ -1,120 +0,0 @@
1
- import { describe, it, expect } from 'bun:test';
2
- import { filterArgs, emitActivity, getActivityAfter, getActivityHistory, subscribe } from '../src/activity';
3
-
4
- describe('filterArgs — privacy filtering', () => {
5
- it('redacts fill value for password fields', () => {
6
- expect(filterArgs('fill', ['#password', 'mysecret123'])).toEqual(['#password', '[REDACTED]']);
7
- expect(filterArgs('fill', ['input[type=passwd]', 'abc'])).toEqual(['input[type=passwd]', '[REDACTED]']);
8
- });
9
-
10
- it('preserves fill value for non-password fields', () => {
11
- expect(filterArgs('fill', ['#email', 'user@test.com'])).toEqual(['#email', 'user@test.com']);
12
- });
13
-
14
- it('redacts type command args', () => {
15
- expect(filterArgs('type', ['my password'])).toEqual(['[REDACTED]']);
16
- });
17
-
18
- it('redacts Authorization header', () => {
19
- expect(filterArgs('header', ['Authorization:Bearer abc123'])).toEqual(['Authorization:[REDACTED]']);
20
- });
21
-
22
- it('preserves non-sensitive headers', () => {
23
- expect(filterArgs('header', ['Content-Type:application/json'])).toEqual(['Content-Type:application/json']);
24
- });
25
-
26
- it('redacts cookie values', () => {
27
- expect(filterArgs('cookie', ['session_id=abc123'])).toEqual(['session_id=[REDACTED]']);
28
- });
29
-
30
- it('redacts sensitive URL query params', () => {
31
- const result = filterArgs('goto', ['https://example.com?api_key=secret&page=1']);
32
- expect(result[0]).toContain('api_key=%5BREDACTED%5D');
33
- expect(result[0]).toContain('page=1');
34
- });
35
-
36
- it('preserves non-sensitive URL query params', () => {
37
- const result = filterArgs('goto', ['https://example.com?page=1&sort=name']);
38
- expect(result[0]).toBe('https://example.com?page=1&sort=name');
39
- });
40
-
41
- it('handles empty args', () => {
42
- expect(filterArgs('click', [])).toEqual([]);
43
- });
44
-
45
- it('handles non-URL non-sensitive args', () => {
46
- expect(filterArgs('click', ['@e3'])).toEqual(['@e3']);
47
- });
48
- });
49
-
50
- describe('emitActivity', () => {
51
- it('emits with auto-incremented id', () => {
52
- const e1 = emitActivity({ type: 'command_start', command: 'goto', args: ['https://example.com'] });
53
- const e2 = emitActivity({ type: 'command_end', command: 'goto', status: 'ok', duration: 100 });
54
- expect(e2.id).toBe(e1.id + 1);
55
- });
56
-
57
- it('truncates long results', () => {
58
- const longResult = 'x'.repeat(500);
59
- const entry = emitActivity({ type: 'command_end', command: 'text', result: longResult });
60
- expect(entry.result!.length).toBeLessThanOrEqual(203); // 200 + "..."
61
- });
62
-
63
- it('applies privacy filtering', () => {
64
- const entry = emitActivity({ type: 'command_start', command: 'type', args: ['my secret password'] });
65
- expect(entry.args).toEqual(['[REDACTED]']);
66
- });
67
- });
68
-
69
- describe('getActivityAfter', () => {
70
- it('returns entries after cursor', () => {
71
- const e1 = emitActivity({ type: 'command_start', command: 'test1' });
72
- const e2 = emitActivity({ type: 'command_start', command: 'test2' });
73
- const result = getActivityAfter(e1.id);
74
- expect(result.entries.some(e => e.id === e2.id)).toBe(true);
75
- expect(result.gap).toBe(false);
76
- });
77
-
78
- it('returns all entries when cursor is 0', () => {
79
- emitActivity({ type: 'command_start', command: 'test3' });
80
- const result = getActivityAfter(0);
81
- expect(result.entries.length).toBeGreaterThan(0);
82
- });
83
- });
84
-
85
- describe('getActivityHistory', () => {
86
- it('returns limited entries', () => {
87
- for (let i = 0; i < 5; i++) {
88
- emitActivity({ type: 'command_start', command: `history-test-${i}` });
89
- }
90
- const result = getActivityHistory(3);
91
- expect(result.entries.length).toBeLessThanOrEqual(3);
92
- });
93
- });
94
-
95
- describe('subscribe', () => {
96
- it('receives new events', async () => {
97
- const received: any[] = [];
98
- const unsub = subscribe((entry) => received.push(entry));
99
-
100
- emitActivity({ type: 'command_start', command: 'sub-test' });
101
-
102
- // queueMicrotask is async — wait a tick
103
- await new Promise(resolve => setTimeout(resolve, 10));
104
-
105
- expect(received.length).toBeGreaterThanOrEqual(1);
106
- expect(received[received.length - 1].command).toBe('sub-test');
107
- unsub();
108
- });
109
-
110
- it('stops receiving after unsubscribe', async () => {
111
- const received: any[] = [];
112
- const unsub = subscribe((entry) => received.push(entry));
113
- unsub();
114
-
115
- emitActivity({ type: 'command_start', command: 'should-not-see' });
116
- await new Promise(resolve => setTimeout(resolve, 10));
117
-
118
- expect(received.filter(e => e.command === 'should-not-see').length).toBe(0);
119
- });
120
- });
@@ -1,32 +0,0 @@
1
- /**
2
- * Adversarial security tests — XSS and boundary-check hardening
3
- *
4
- * Test 19: Sidepanel escapes entry.command in activity feed (prevents XSS)
5
- * Test 20: Freeze hook uses trailing slash in boundary check (prevents prefix collision)
6
- */
7
-
8
- import { describe, test, expect } from 'bun:test';
9
- import * as fs from 'fs';
10
- import * as path from 'path';
11
-
12
- describe('Adversarial security', () => {
13
- test('sidepanel escapes entry.command in activity feed', () => {
14
- const source = fs.readFileSync(
15
- path.join(import.meta.dir, '../../extension/sidepanel.js'),
16
- 'utf-8',
17
- );
18
- // entry.command must be wrapped in escapeHtml() to prevent XSS injection
19
- // via crafted command names in the activity feed
20
- expect(source).toContain('escapeHtml(entry.command');
21
- });
22
-
23
- test('freeze hook uses trailing slash in boundary check', () => {
24
- const source = fs.readFileSync(
25
- path.join(import.meta.dir, '../../freeze/bin/check-freeze.sh'),
26
- 'utf-8',
27
- );
28
- // The boundary check must use "${FREEZE_DIR}/" with a trailing slash
29
- // to prevent prefix collision (e.g., /app matching /application)
30
- expect(source).toContain('"${FREEZE_DIR}/"');
31
- });
32
- });
@@ -1,17 +0,0 @@
1
- import { describe, it, expect } from 'bun:test';
2
-
3
- // ─── BrowserManager basic unit tests ─────────────────────────────
4
-
5
- describe('BrowserManager defaults', () => {
6
- it('getConnectionMode defaults to launched', async () => {
7
- const { BrowserManager } = await import('../src/browser-manager');
8
- const bm = new BrowserManager();
9
- expect(bm.getConnectionMode()).toBe('launched');
10
- });
11
-
12
- it('getRefMap returns empty array initially', async () => {
13
- const { BrowserManager } = await import('../src/browser-manager');
14
- const bm = new BrowserManager();
15
- expect(bm.getRefMap()).toEqual([]);
16
- });
17
- });
@@ -1,72 +0,0 @@
1
- import { describe, test, expect, afterAll } from 'bun:test';
2
- import * as path from 'path';
3
-
4
- // Load the polyfill into a fresh object (don't clobber globalThis.Bun)
5
- const polyfillPath = path.resolve(import.meta.dir, '../src/bun-polyfill.cjs');
6
-
7
- describe('bun-polyfill', () => {
8
- // We test the polyfill by requiring it in a subprocess under Node.js
9
- // since it's designed for Node, not Bun.
10
-
11
- test('Bun.sleep resolves after delay', async () => {
12
- const result = Bun.spawnSync(['node', '-e', `
13
- require('${polyfillPath}');
14
- (async () => {
15
- const start = Date.now();
16
- await Bun.sleep(50);
17
- const elapsed = Date.now() - start;
18
- console.log(elapsed >= 40 ? 'OK' : 'TOO_FAST');
19
- })();
20
- `], { stdout: 'pipe', stderr: 'pipe' });
21
- expect(result.stdout.toString().trim()).toBe('OK');
22
- expect(result.exitCode).toBe(0);
23
- });
24
-
25
- test('Bun.spawnSync runs a command and returns stdout', () => {
26
- const result = Bun.spawnSync(['node', '-e', `
27
- require('${polyfillPath}');
28
- const r = Bun.spawnSync(['echo', 'hello'], { stdout: 'pipe' });
29
- console.log(r.stdout.toString().trim());
30
- console.log('exit:' + r.exitCode);
31
- `], { stdout: 'pipe', stderr: 'pipe' });
32
- const lines = result.stdout.toString().trim().split('\n');
33
- expect(lines[0]).toBe('hello');
34
- expect(lines[1]).toBe('exit:0');
35
- });
36
-
37
- test('Bun.spawn launches a process with pid', async () => {
38
- const result = Bun.spawnSync(['node', '-e', `
39
- require('${polyfillPath}');
40
- const p = Bun.spawn(['echo', 'test'], { stdio: ['pipe', 'pipe', 'pipe'] });
41
- console.log(typeof p.pid === 'number' ? 'HAS_PID' : 'NO_PID');
42
- console.log(typeof p.kill === 'function' ? 'HAS_KILL' : 'NO_KILL');
43
- console.log(typeof p.unref === 'function' ? 'HAS_UNREF' : 'NO_UNREF');
44
- `], { stdout: 'pipe', stderr: 'pipe' });
45
- const lines = result.stdout.toString().trim().split('\n');
46
- expect(lines[0]).toBe('HAS_PID');
47
- expect(lines[1]).toBe('HAS_KILL');
48
- expect(lines[2]).toBe('HAS_UNREF');
49
- });
50
-
51
- test('Bun.serve creates an HTTP server that responds', async () => {
52
- const result = Bun.spawnSync(['node', '-e', `
53
- require('${polyfillPath}');
54
- const server = Bun.serve({
55
- port: 0, // Note: polyfill uses port directly, so we pick one
56
- hostname: '127.0.0.1',
57
- fetch(req) {
58
- return new Response(JSON.stringify({ ok: true }), {
59
- headers: { 'Content-Type': 'application/json' },
60
- });
61
- },
62
- });
63
- // The polyfill doesn't support port 0, so we test the object shape
64
- console.log(typeof server.stop === 'function' ? 'HAS_STOP' : 'NO_STOP');
65
- console.log(typeof server.port === 'number' ? 'HAS_PORT' : 'NO_PORT');
66
- server.stop();
67
- `], { stdout: 'pipe', stderr: 'pipe' });
68
- const lines = result.stdout.toString().trim().split('\n');
69
- expect(lines[0]).toBe('HAS_STOP');
70
- expect(lines[1]).toBe('HAS_PORT');
71
- });
72
- });