@runchr/gstack-antigravity 0.1.1 → 0.1.3

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.

Potentially problematic release.


This version of @runchr/gstack-antigravity might be problematic. Click here for more details.

Files changed (229) hide show
  1. package/.agents/skills/gstack/.agents/skills/gstack/SKILL.md +651 -0
  2. package/.agents/skills/gstack/.agents/skills/gstack-autoplan/SKILL.md +678 -0
  3. package/.agents/skills/gstack/.agents/skills/gstack-benchmark/SKILL.md +482 -0
  4. package/.agents/skills/gstack/.agents/skills/gstack-browse/SKILL.md +511 -0
  5. package/.agents/skills/gstack/.agents/skills/gstack-canary/SKILL.md +486 -0
  6. package/.agents/skills/gstack/.agents/skills/gstack-careful/SKILL.md +50 -0
  7. package/.agents/skills/gstack/.agents/skills/gstack-cso/SKILL.md +607 -0
  8. package/.agents/skills/gstack/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
  9. package/.agents/skills/gstack/.agents/skills/gstack-design-review/SKILL.md +988 -0
  10. package/.agents/skills/gstack/.agents/skills/gstack-document-release/SKILL.md +604 -0
  11. package/.agents/skills/gstack/.agents/skills/gstack-freeze/SKILL.md +67 -0
  12. package/.agents/skills/gstack/.agents/skills/gstack-guard/SKILL.md +62 -0
  13. package/.agents/skills/gstack/.agents/skills/gstack-investigate/SKILL.md +415 -0
  14. package/.agents/skills/gstack/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
  15. package/.agents/skills/gstack/.agents/skills/gstack-office-hours/SKILL.md +986 -0
  16. package/.agents/skills/gstack/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
  17. package/.agents/skills/gstack/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
  18. package/.agents/skills/gstack/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
  19. package/.agents/skills/gstack/.agents/skills/gstack-qa/SKILL.md +1006 -0
  20. package/.agents/skills/gstack/.agents/skills/gstack-qa-only/SKILL.md +626 -0
  21. package/.agents/skills/gstack/.agents/skills/gstack-retro/SKILL.md +1065 -0
  22. package/.agents/skills/gstack/.agents/skills/gstack-review/SKILL.md +704 -0
  23. package/.agents/skills/gstack/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
  24. package/.agents/skills/gstack/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
  25. package/.agents/skills/gstack/.agents/skills/gstack-ship/SKILL.md +1312 -0
  26. package/.agents/skills/gstack/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
  27. package/.agents/skills/gstack/.agents/skills/gstack-upgrade/SKILL.md +220 -0
  28. package/.agents/skills/gstack/.env.example +5 -0
  29. package/.agents/skills/gstack/.github/workflows/skill-docs.yml +17 -0
  30. package/.agents/skills/gstack/AGENTS.md +49 -0
  31. package/.agents/skills/gstack/ARCHITECTURE.md +359 -0
  32. package/.agents/skills/gstack/BROWSER.md +271 -0
  33. package/.agents/skills/gstack/CHANGELOG.md +800 -0
  34. package/.agents/skills/gstack/CLAUDE.md +284 -0
  35. package/.agents/skills/gstack/CONTRIBUTING.md +370 -0
  36. package/.agents/skills/gstack/ETHOS.md +129 -0
  37. package/.agents/skills/gstack/LICENSE +21 -0
  38. package/.agents/skills/gstack/README.md +228 -0
  39. package/.agents/skills/gstack/SKILL.md +657 -0
  40. package/.agents/skills/gstack/SKILL.md.tmpl +281 -0
  41. package/.agents/skills/gstack/TODOS.md +564 -0
  42. package/.agents/skills/gstack/VERSION +1 -0
  43. package/.agents/skills/gstack/autoplan/SKILL.md +689 -0
  44. package/.agents/skills/gstack/autoplan/SKILL.md.tmpl +416 -0
  45. package/.agents/skills/gstack/benchmark/SKILL.md +489 -0
  46. package/.agents/skills/gstack/benchmark/SKILL.md.tmpl +233 -0
  47. package/.agents/skills/gstack/bin/dev-setup +68 -0
  48. package/.agents/skills/gstack/bin/dev-teardown +56 -0
  49. package/.agents/skills/gstack/bin/gstack-analytics +191 -0
  50. package/.agents/skills/gstack/bin/gstack-community-dashboard +113 -0
  51. package/.agents/skills/gstack/bin/gstack-config +38 -0
  52. package/.agents/skills/gstack/bin/gstack-diff-scope +71 -0
  53. package/.agents/skills/gstack/bin/gstack-global-discover.ts +591 -0
  54. package/.agents/skills/gstack/bin/gstack-repo-mode +93 -0
  55. package/.agents/skills/gstack/bin/gstack-review-log +9 -0
  56. package/.agents/skills/gstack/bin/gstack-review-read +12 -0
  57. package/.agents/skills/gstack/bin/gstack-slug +15 -0
  58. package/.agents/skills/gstack/bin/gstack-telemetry-log +158 -0
  59. package/.agents/skills/gstack/bin/gstack-telemetry-sync +127 -0
  60. package/.agents/skills/gstack/bin/gstack-update-check +196 -0
  61. package/.agents/skills/gstack/browse/SKILL.md +517 -0
  62. package/.agents/skills/gstack/browse/SKILL.md.tmpl +141 -0
  63. package/.agents/skills/gstack/browse/bin/find-browse +21 -0
  64. package/.agents/skills/gstack/browse/bin/remote-slug +14 -0
  65. package/.agents/skills/gstack/browse/scripts/build-node-server.sh +48 -0
  66. package/.agents/skills/gstack/browse/src/browser-manager.ts +634 -0
  67. package/.agents/skills/gstack/browse/src/buffers.ts +137 -0
  68. package/.agents/skills/gstack/browse/src/bun-polyfill.cjs +109 -0
  69. package/.agents/skills/gstack/browse/src/cli.ts +420 -0
  70. package/.agents/skills/gstack/browse/src/commands.ts +111 -0
  71. package/.agents/skills/gstack/browse/src/config.ts +150 -0
  72. package/.agents/skills/gstack/browse/src/cookie-import-browser.ts +417 -0
  73. package/.agents/skills/gstack/browse/src/cookie-picker-routes.ts +207 -0
  74. package/.agents/skills/gstack/browse/src/cookie-picker-ui.ts +541 -0
  75. package/.agents/skills/gstack/browse/src/find-browse.ts +61 -0
  76. package/.agents/skills/gstack/browse/src/meta-commands.ts +269 -0
  77. package/.agents/skills/gstack/browse/src/platform.ts +17 -0
  78. package/.agents/skills/gstack/browse/src/read-commands.ts +335 -0
  79. package/.agents/skills/gstack/browse/src/server.ts +369 -0
  80. package/.agents/skills/gstack/browse/src/snapshot.ts +398 -0
  81. package/.agents/skills/gstack/browse/src/url-validation.ts +91 -0
  82. package/.agents/skills/gstack/browse/src/write-commands.ts +352 -0
  83. package/.agents/skills/gstack/browse/test/bun-polyfill.test.ts +72 -0
  84. package/.agents/skills/gstack/browse/test/commands.test.ts +1836 -0
  85. package/.agents/skills/gstack/browse/test/config.test.ts +250 -0
  86. package/.agents/skills/gstack/browse/test/cookie-import-browser.test.ts +397 -0
  87. package/.agents/skills/gstack/browse/test/cookie-picker-routes.test.ts +205 -0
  88. package/.agents/skills/gstack/browse/test/find-browse.test.ts +50 -0
  89. package/.agents/skills/gstack/browse/test/fixtures/basic.html +33 -0
  90. package/.agents/skills/gstack/browse/test/fixtures/cursor-interactive.html +22 -0
  91. package/.agents/skills/gstack/browse/test/fixtures/dialog.html +15 -0
  92. package/.agents/skills/gstack/browse/test/fixtures/empty.html +2 -0
  93. package/.agents/skills/gstack/browse/test/fixtures/forms.html +55 -0
  94. package/.agents/skills/gstack/browse/test/fixtures/qa-eval-checkout.html +108 -0
  95. package/.agents/skills/gstack/browse/test/fixtures/qa-eval-spa.html +98 -0
  96. package/.agents/skills/gstack/browse/test/fixtures/qa-eval.html +51 -0
  97. package/.agents/skills/gstack/browse/test/fixtures/responsive.html +49 -0
  98. package/.agents/skills/gstack/browse/test/fixtures/snapshot.html +55 -0
  99. package/.agents/skills/gstack/browse/test/fixtures/spa.html +24 -0
  100. package/.agents/skills/gstack/browse/test/fixtures/states.html +17 -0
  101. package/.agents/skills/gstack/browse/test/fixtures/upload.html +25 -0
  102. package/.agents/skills/gstack/browse/test/gstack-config.test.ts +125 -0
  103. package/.agents/skills/gstack/browse/test/gstack-update-check.test.ts +467 -0
  104. package/.agents/skills/gstack/browse/test/handoff.test.ts +235 -0
  105. package/.agents/skills/gstack/browse/test/path-validation.test.ts +63 -0
  106. package/.agents/skills/gstack/browse/test/platform.test.ts +37 -0
  107. package/.agents/skills/gstack/browse/test/snapshot.test.ts +467 -0
  108. package/.agents/skills/gstack/browse/test/test-server.ts +57 -0
  109. package/.agents/skills/gstack/browse/test/url-validation.test.ts +72 -0
  110. package/.agents/skills/gstack/canary/SKILL.md +493 -0
  111. package/.agents/skills/gstack/canary/SKILL.md.tmpl +220 -0
  112. package/.agents/skills/gstack/careful/SKILL.md +59 -0
  113. package/.agents/skills/gstack/careful/SKILL.md.tmpl +57 -0
  114. package/.agents/skills/gstack/careful/bin/check-careful.sh +112 -0
  115. package/.agents/skills/gstack/codex/SKILL.md +677 -0
  116. package/.agents/skills/gstack/codex/SKILL.md.tmpl +356 -0
  117. package/.agents/skills/gstack/conductor.json +6 -0
  118. package/.agents/skills/gstack/cso/SKILL.md +615 -0
  119. package/.agents/skills/gstack/cso/SKILL.md.tmpl +376 -0
  120. package/.agents/skills/gstack/design-consultation/SKILL.md +625 -0
  121. package/.agents/skills/gstack/design-consultation/SKILL.md.tmpl +369 -0
  122. package/.agents/skills/gstack/design-review/SKILL.md +998 -0
  123. package/.agents/skills/gstack/design-review/SKILL.md.tmpl +262 -0
  124. package/.agents/skills/gstack/docs/images/github-2013.png +0 -0
  125. package/.agents/skills/gstack/docs/images/github-2026.png +0 -0
  126. package/.agents/skills/gstack/docs/skills.md +877 -0
  127. package/.agents/skills/gstack/document-release/SKILL.md +613 -0
  128. package/.agents/skills/gstack/document-release/SKILL.md.tmpl +357 -0
  129. package/.agents/skills/gstack/freeze/SKILL.md +82 -0
  130. package/.agents/skills/gstack/freeze/SKILL.md.tmpl +80 -0
  131. package/.agents/skills/gstack/freeze/bin/check-freeze.sh +68 -0
  132. package/.agents/skills/gstack/gstack-upgrade/SKILL.md +226 -0
  133. package/.agents/skills/gstack/gstack-upgrade/SKILL.md.tmpl +224 -0
  134. package/.agents/skills/gstack/guard/SKILL.md +82 -0
  135. package/.agents/skills/gstack/guard/SKILL.md.tmpl +80 -0
  136. package/.agents/skills/gstack/investigate/SKILL.md +435 -0
  137. package/.agents/skills/gstack/investigate/SKILL.md.tmpl +196 -0
  138. package/.agents/skills/gstack/land-and-deploy/SKILL.md +880 -0
  139. package/.agents/skills/gstack/land-and-deploy/SKILL.md.tmpl +575 -0
  140. package/.agents/skills/gstack/office-hours/SKILL.md +996 -0
  141. package/.agents/skills/gstack/office-hours/SKILL.md.tmpl +624 -0
  142. package/.agents/skills/gstack/package.json +55 -0
  143. package/.agents/skills/gstack/plan-ceo-review/SKILL.md +1277 -0
  144. package/.agents/skills/gstack/plan-ceo-review/SKILL.md.tmpl +838 -0
  145. package/.agents/skills/gstack/plan-design-review/SKILL.md +676 -0
  146. package/.agents/skills/gstack/plan-design-review/SKILL.md.tmpl +314 -0
  147. package/.agents/skills/gstack/plan-eng-review/SKILL.md +836 -0
  148. package/.agents/skills/gstack/plan-eng-review/SKILL.md.tmpl +279 -0
  149. package/.agents/skills/gstack/qa/SKILL.md +1016 -0
  150. package/.agents/skills/gstack/qa/SKILL.md.tmpl +316 -0
  151. package/.agents/skills/gstack/qa/references/issue-taxonomy.md +85 -0
  152. package/.agents/skills/gstack/qa/templates/qa-report-template.md +126 -0
  153. package/.agents/skills/gstack/qa-only/SKILL.md +633 -0
  154. package/.agents/skills/gstack/qa-only/SKILL.md.tmpl +101 -0
  155. package/.agents/skills/gstack/retro/SKILL.md +1072 -0
  156. package/.agents/skills/gstack/retro/SKILL.md.tmpl +833 -0
  157. package/.agents/skills/gstack/review/SKILL.md +849 -0
  158. package/.agents/skills/gstack/review/SKILL.md.tmpl +259 -0
  159. package/.agents/skills/gstack/review/TODOS-format.md +62 -0
  160. package/.agents/skills/gstack/review/checklist.md +190 -0
  161. package/.agents/skills/gstack/review/design-checklist.md +132 -0
  162. package/.agents/skills/gstack/review/greptile-triage.md +220 -0
  163. package/.agents/skills/gstack/scripts/analytics.ts +190 -0
  164. package/.agents/skills/gstack/scripts/dev-skill.ts +82 -0
  165. package/.agents/skills/gstack/scripts/eval-compare.ts +96 -0
  166. package/.agents/skills/gstack/scripts/eval-list.ts +116 -0
  167. package/.agents/skills/gstack/scripts/eval-select.ts +86 -0
  168. package/.agents/skills/gstack/scripts/eval-summary.ts +187 -0
  169. package/.agents/skills/gstack/scripts/eval-watch.ts +172 -0
  170. package/.agents/skills/gstack/scripts/gen-skill-docs.ts +2414 -0
  171. package/.agents/skills/gstack/scripts/skill-check.ts +167 -0
  172. package/.agents/skills/gstack/setup +269 -0
  173. package/.agents/skills/gstack/setup-browser-cookies/SKILL.md +330 -0
  174. package/.agents/skills/gstack/setup-browser-cookies/SKILL.md.tmpl +74 -0
  175. package/.agents/skills/gstack/setup-deploy/SKILL.md +459 -0
  176. package/.agents/skills/gstack/setup-deploy/SKILL.md.tmpl +220 -0
  177. package/.agents/skills/gstack/ship/SKILL.md +1457 -0
  178. package/.agents/skills/gstack/ship/SKILL.md.tmpl +528 -0
  179. package/.agents/skills/gstack/supabase/config.sh +10 -0
  180. package/.agents/skills/gstack/supabase/functions/community-pulse/index.ts +59 -0
  181. package/.agents/skills/gstack/supabase/functions/telemetry-ingest/index.ts +135 -0
  182. package/.agents/skills/gstack/supabase/functions/update-check/index.ts +37 -0
  183. package/.agents/skills/gstack/supabase/migrations/001_telemetry.sql +89 -0
  184. package/.agents/skills/gstack/test/analytics.test.ts +277 -0
  185. package/.agents/skills/gstack/test/codex-e2e.test.ts +197 -0
  186. package/.agents/skills/gstack/test/fixtures/coverage-audit-fixture.ts +76 -0
  187. package/.agents/skills/gstack/test/fixtures/eval-baselines.json +7 -0
  188. package/.agents/skills/gstack/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
  189. package/.agents/skills/gstack/test/fixtures/qa-eval-ground-truth.json +43 -0
  190. package/.agents/skills/gstack/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
  191. package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.css +86 -0
  192. package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.html +41 -0
  193. package/.agents/skills/gstack/test/fixtures/review-eval-enum-diff.rb +30 -0
  194. package/.agents/skills/gstack/test/fixtures/review-eval-enum.rb +27 -0
  195. package/.agents/skills/gstack/test/fixtures/review-eval-vuln.rb +14 -0
  196. package/.agents/skills/gstack/test/gemini-e2e.test.ts +173 -0
  197. package/.agents/skills/gstack/test/gen-skill-docs.test.ts +1049 -0
  198. package/.agents/skills/gstack/test/global-discover.test.ts +187 -0
  199. package/.agents/skills/gstack/test/helpers/codex-session-runner.ts +282 -0
  200. package/.agents/skills/gstack/test/helpers/e2e-helpers.ts +239 -0
  201. package/.agents/skills/gstack/test/helpers/eval-store.test.ts +548 -0
  202. package/.agents/skills/gstack/test/helpers/eval-store.ts +689 -0
  203. package/.agents/skills/gstack/test/helpers/gemini-session-runner.test.ts +104 -0
  204. package/.agents/skills/gstack/test/helpers/gemini-session-runner.ts +201 -0
  205. package/.agents/skills/gstack/test/helpers/llm-judge.ts +130 -0
  206. package/.agents/skills/gstack/test/helpers/observability.test.ts +283 -0
  207. package/.agents/skills/gstack/test/helpers/session-runner.test.ts +96 -0
  208. package/.agents/skills/gstack/test/helpers/session-runner.ts +357 -0
  209. package/.agents/skills/gstack/test/helpers/skill-parser.ts +206 -0
  210. package/.agents/skills/gstack/test/helpers/touchfiles.ts +260 -0
  211. package/.agents/skills/gstack/test/hook-scripts.test.ts +373 -0
  212. package/.agents/skills/gstack/test/skill-e2e-browse.test.ts +293 -0
  213. package/.agents/skills/gstack/test/skill-e2e-deploy.test.ts +279 -0
  214. package/.agents/skills/gstack/test/skill-e2e-design.test.ts +614 -0
  215. package/.agents/skills/gstack/test/skill-e2e-plan.test.ts +538 -0
  216. package/.agents/skills/gstack/test/skill-e2e-qa-bugs.test.ts +194 -0
  217. package/.agents/skills/gstack/test/skill-e2e-qa-workflow.test.ts +412 -0
  218. package/.agents/skills/gstack/test/skill-e2e-review.test.ts +535 -0
  219. package/.agents/skills/gstack/test/skill-e2e-workflow.test.ts +586 -0
  220. package/.agents/skills/gstack/test/skill-e2e.test.ts +3325 -0
  221. package/.agents/skills/gstack/test/skill-llm-eval.test.ts +787 -0
  222. package/.agents/skills/gstack/test/skill-parser.test.ts +179 -0
  223. package/.agents/skills/gstack/test/skill-routing-e2e.test.ts +605 -0
  224. package/.agents/skills/gstack/test/skill-validation.test.ts +1520 -0
  225. package/.agents/skills/gstack/test/telemetry.test.ts +278 -0
  226. package/.agents/skills/gstack/test/touchfiles.test.ts +262 -0
  227. package/.agents/skills/gstack/unfreeze/SKILL.md +40 -0
  228. package/.agents/skills/gstack/unfreeze/SKILL.md.tmpl +38 -0
  229. package/package.json +2 -1
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Shared buffers and types — extracted to break circular dependency
3
+ * between server.ts and browser-manager.ts
4
+ *
5
+ * CircularBuffer<T>: O(1) insert ring buffer with fixed capacity.
6
+ *
7
+ * ┌───┬───┬───┬───┬───┬───┐
8
+ * │ 3 │ 4 │ 5 │ │ 1 │ 2 │ capacity=6, head=4, size=5
9
+ * └───┴───┴───┴───┴─▲─┴───┘
10
+ * │
11
+ * head (oldest entry)
12
+ *
13
+ * push() writes at (head+size) % capacity, O(1)
14
+ * toArray() returns entries in insertion order, O(n)
15
+ * totalAdded keeps incrementing past capacity (flush cursor)
16
+ */
17
+
18
+ // ─── CircularBuffer ─────────────────────────────────────────
19
+
20
+ export class CircularBuffer<T> {
21
+ private buffer: (T | undefined)[];
22
+ private head: number = 0;
23
+ private _size: number = 0;
24
+ private _totalAdded: number = 0;
25
+ readonly capacity: number;
26
+
27
+ constructor(capacity: number) {
28
+ this.capacity = capacity;
29
+ this.buffer = new Array(capacity);
30
+ }
31
+
32
+ push(entry: T): void {
33
+ const index = (this.head + this._size) % this.capacity;
34
+ this.buffer[index] = entry;
35
+ if (this._size < this.capacity) {
36
+ this._size++;
37
+ } else {
38
+ // Buffer full — advance head (overwrites oldest)
39
+ this.head = (this.head + 1) % this.capacity;
40
+ }
41
+ this._totalAdded++;
42
+ }
43
+
44
+ /** Return entries in insertion order (oldest first) */
45
+ toArray(): T[] {
46
+ const result: T[] = [];
47
+ for (let i = 0; i < this._size; i++) {
48
+ result.push(this.buffer[(this.head + i) % this.capacity] as T);
49
+ }
50
+ return result;
51
+ }
52
+
53
+ /** Return the last N entries (most recent first → reversed to oldest first) */
54
+ last(n: number): T[] {
55
+ const count = Math.min(n, this._size);
56
+ const result: T[] = [];
57
+ const start = (this.head + this._size - count) % this.capacity;
58
+ for (let i = 0; i < count; i++) {
59
+ result.push(this.buffer[(start + i) % this.capacity] as T);
60
+ }
61
+ return result;
62
+ }
63
+
64
+ get length(): number {
65
+ return this._size;
66
+ }
67
+
68
+ get totalAdded(): number {
69
+ return this._totalAdded;
70
+ }
71
+
72
+ clear(): void {
73
+ this.head = 0;
74
+ this._size = 0;
75
+ // Don't reset totalAdded — flush cursor depends on it
76
+ }
77
+
78
+ /** Get entry by index (0 = oldest) — used by network response matching */
79
+ get(index: number): T | undefined {
80
+ if (index < 0 || index >= this._size) return undefined;
81
+ return this.buffer[(this.head + index) % this.capacity];
82
+ }
83
+
84
+ /** Set entry by index (0 = oldest) — used by network response matching */
85
+ set(index: number, entry: T): void {
86
+ if (index < 0 || index >= this._size) return;
87
+ this.buffer[(this.head + index) % this.capacity] = entry;
88
+ }
89
+ }
90
+
91
+ // ─── Entry Types ────────────────────────────────────────────
92
+
93
+ export interface LogEntry {
94
+ timestamp: number;
95
+ level: string;
96
+ text: string;
97
+ }
98
+
99
+ export interface NetworkEntry {
100
+ timestamp: number;
101
+ method: string;
102
+ url: string;
103
+ status?: number;
104
+ duration?: number;
105
+ size?: number;
106
+ }
107
+
108
+ export interface DialogEntry {
109
+ timestamp: number;
110
+ type: string; // 'alert' | 'confirm' | 'prompt' | 'beforeunload'
111
+ message: string;
112
+ defaultValue?: string;
113
+ action: string; // 'accepted' | 'dismissed'
114
+ response?: string; // text provided for prompt
115
+ }
116
+
117
+ // ─── Buffer Instances ───────────────────────────────────────
118
+
119
+ const HIGH_WATER_MARK = 50_000;
120
+
121
+ export const consoleBuffer = new CircularBuffer<LogEntry>(HIGH_WATER_MARK);
122
+ export const networkBuffer = new CircularBuffer<NetworkEntry>(HIGH_WATER_MARK);
123
+ export const dialogBuffer = new CircularBuffer<DialogEntry>(HIGH_WATER_MARK);
124
+
125
+ // ─── Convenience add functions ──────────────────────────────
126
+
127
+ export function addConsoleEntry(entry: LogEntry) {
128
+ consoleBuffer.push(entry);
129
+ }
130
+
131
+ export function addNetworkEntry(entry: NetworkEntry) {
132
+ networkBuffer.push(entry);
133
+ }
134
+
135
+ export function addDialogEntry(entry: DialogEntry) {
136
+ dialogBuffer.push(entry);
137
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Bun API polyfill for Node.js — Windows compatibility layer.
3
+ *
4
+ * On Windows, Bun can't launch or connect to Playwright's Chromium
5
+ * (oven-sh/bun#4253, #9911). The browse server falls back to running
6
+ * under Node.js with this polyfill providing Bun API equivalents.
7
+ *
8
+ * Loaded via --require before the transpiled server bundle.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const http = require('http');
14
+ const { spawnSync, spawn } = require('child_process');
15
+
16
+ globalThis.Bun = {
17
+ serve(options) {
18
+ const { port, hostname = '127.0.0.1', fetch } = options;
19
+
20
+ const server = http.createServer(async (nodeReq, nodeRes) => {
21
+ try {
22
+ const url = `http://${hostname}:${port}${nodeReq.url}`;
23
+ const headers = new Headers();
24
+ for (const [key, val] of Object.entries(nodeReq.headers)) {
25
+ if (val) headers.set(key, Array.isArray(val) ? val[0] : val);
26
+ }
27
+
28
+ let body = null;
29
+ if (nodeReq.method !== 'GET' && nodeReq.method !== 'HEAD') {
30
+ body = await new Promise((resolve) => {
31
+ const chunks = [];
32
+ nodeReq.on('data', (chunk) => chunks.push(chunk));
33
+ nodeReq.on('end', () => resolve(Buffer.concat(chunks)));
34
+ });
35
+ }
36
+
37
+ const webReq = new Request(url, {
38
+ method: nodeReq.method,
39
+ headers,
40
+ body,
41
+ });
42
+
43
+ const webRes = await fetch(webReq);
44
+
45
+ nodeRes.statusCode = webRes.status;
46
+ webRes.headers.forEach((val, key) => {
47
+ nodeRes.setHeader(key, val);
48
+ });
49
+
50
+ const resBody = await webRes.arrayBuffer();
51
+ nodeRes.end(Buffer.from(resBody));
52
+ } catch (err) {
53
+ nodeRes.statusCode = 500;
54
+ nodeRes.end(JSON.stringify({ error: err.message }));
55
+ }
56
+ });
57
+
58
+ server.listen(port, hostname);
59
+
60
+ return {
61
+ stop() { server.close(); },
62
+ port,
63
+ hostname,
64
+ };
65
+ },
66
+
67
+ spawnSync(cmd, options = {}) {
68
+ const [command, ...args] = cmd;
69
+ const result = spawnSync(command, args, {
70
+ stdio: [
71
+ options.stdin || 'pipe',
72
+ options.stdout === 'pipe' ? 'pipe' : 'ignore',
73
+ options.stderr === 'pipe' ? 'pipe' : 'ignore',
74
+ ],
75
+ timeout: options.timeout,
76
+ env: options.env,
77
+ cwd: options.cwd,
78
+ });
79
+
80
+ return {
81
+ exitCode: result.status,
82
+ stdout: result.stdout || Buffer.from(''),
83
+ stderr: result.stderr || Buffer.from(''),
84
+ };
85
+ },
86
+
87
+ spawn(cmd, options = {}) {
88
+ const [command, ...args] = cmd;
89
+ const stdio = options.stdio || ['pipe', 'pipe', 'pipe'];
90
+ const proc = spawn(command, args, {
91
+ stdio,
92
+ env: options.env,
93
+ cwd: options.cwd,
94
+ });
95
+
96
+ return {
97
+ pid: proc.pid,
98
+ stdout: proc.stdout,
99
+ stderr: proc.stderr,
100
+ stdin: proc.stdin,
101
+ unref() { proc.unref(); },
102
+ kill(signal) { proc.kill(signal); },
103
+ };
104
+ },
105
+
106
+ sleep(ms) {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ },
109
+ };
@@ -0,0 +1,420 @@
1
+ /**
2
+ * gstack CLI — thin wrapper that talks to the persistent server
3
+ *
4
+ * Flow:
5
+ * 1. Read .gstack/browse.json for port + token
6
+ * 2. If missing or stale PID → start server in background
7
+ * 3. Health check + version mismatch detection
8
+ * 4. Send command via HTTP POST
9
+ * 5. Print response to stdout (or stderr for errors)
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { resolveConfig, ensureStateDir, readVersionHash } from './config';
15
+
16
+ const config = resolveConfig();
17
+ const IS_WINDOWS = process.platform === 'win32';
18
+ const MAX_START_WAIT = IS_WINDOWS ? 15000 : 8000; // Node+Chromium takes longer on Windows
19
+
20
+ export function resolveServerScript(
21
+ env: Record<string, string | undefined> = process.env,
22
+ metaDir: string = import.meta.dir,
23
+ execPath: string = process.execPath
24
+ ): string {
25
+ if (env.BROWSE_SERVER_SCRIPT) {
26
+ return env.BROWSE_SERVER_SCRIPT;
27
+ }
28
+
29
+ // Dev mode: cli.ts runs directly from browse/src
30
+ // On macOS/Linux, import.meta.dir starts with /
31
+ // On Windows, it starts with a drive letter (e.g., C:\...)
32
+ if (!metaDir.includes('$bunfs')) {
33
+ const direct = path.resolve(metaDir, 'server.ts');
34
+ if (fs.existsSync(direct)) {
35
+ return direct;
36
+ }
37
+ }
38
+
39
+ // Compiled binary: derive the source tree from browse/dist/browse
40
+ if (execPath) {
41
+ const adjacent = path.resolve(path.dirname(execPath), '..', 'src', 'server.ts');
42
+ if (fs.existsSync(adjacent)) {
43
+ return adjacent;
44
+ }
45
+ }
46
+
47
+ throw new Error(
48
+ 'Cannot find server.ts. Set BROWSE_SERVER_SCRIPT env or run from the browse source tree.'
49
+ );
50
+ }
51
+
52
+ const SERVER_SCRIPT = resolveServerScript();
53
+
54
+ /**
55
+ * On Windows, resolve the Node.js-compatible server bundle.
56
+ * Falls back to null if not found (server will use Bun instead).
57
+ */
58
+ export function resolveNodeServerScript(
59
+ metaDir: string = import.meta.dir,
60
+ execPath: string = process.execPath
61
+ ): string | null {
62
+ // Dev mode
63
+ if (!metaDir.includes('$bunfs')) {
64
+ const distScript = path.resolve(metaDir, '..', 'dist', 'server-node.mjs');
65
+ if (fs.existsSync(distScript)) return distScript;
66
+ }
67
+
68
+ // Compiled binary: browse/dist/browse → browse/dist/server-node.mjs
69
+ if (execPath) {
70
+ const adjacent = path.resolve(path.dirname(execPath), 'server-node.mjs');
71
+ if (fs.existsSync(adjacent)) return adjacent;
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ const NODE_SERVER_SCRIPT = IS_WINDOWS ? resolveNodeServerScript() : null;
78
+
79
+ interface ServerState {
80
+ pid: number;
81
+ port: number;
82
+ token: string;
83
+ startedAt: string;
84
+ serverPath: string;
85
+ binaryVersion?: string;
86
+ }
87
+
88
+ // ─── State File ────────────────────────────────────────────────
89
+ function readState(): ServerState | null {
90
+ try {
91
+ const data = fs.readFileSync(config.stateFile, 'utf-8');
92
+ return JSON.parse(data);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ function isProcessAlive(pid: number): boolean {
99
+ try {
100
+ process.kill(pid, 0);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ // ─── Process Management ─────────────────────────────────────────
108
+ async function killServer(pid: number): Promise<void> {
109
+ if (!isProcessAlive(pid)) return;
110
+
111
+ try { process.kill(pid, 'SIGTERM'); } catch { return; }
112
+
113
+ // Wait up to 2s for graceful shutdown
114
+ const deadline = Date.now() + 2000;
115
+ while (Date.now() < deadline && isProcessAlive(pid)) {
116
+ await Bun.sleep(100);
117
+ }
118
+
119
+ // Force kill if still alive
120
+ if (isProcessAlive(pid)) {
121
+ try { process.kill(pid, 'SIGKILL'); } catch {}
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Clean up legacy /tmp/browse-server*.json files from before project-local state.
127
+ * Verifies PID ownership before sending signals.
128
+ */
129
+ function cleanupLegacyState(): void {
130
+ try {
131
+ const files = fs.readdirSync('/tmp').filter(f => f.startsWith('browse-server') && f.endsWith('.json'));
132
+ for (const file of files) {
133
+ const fullPath = `/tmp/${file}`;
134
+ try {
135
+ const data = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
136
+ if (data.pid && isProcessAlive(data.pid)) {
137
+ // Verify this is actually a browse server before killing
138
+ const check = Bun.spawnSync(['ps', '-p', String(data.pid), '-o', 'command='], {
139
+ stdout: 'pipe', stderr: 'pipe', timeout: 2000,
140
+ });
141
+ const cmd = check.stdout.toString().trim();
142
+ if (cmd.includes('bun') || cmd.includes('server.ts')) {
143
+ try { process.kill(data.pid, 'SIGTERM'); } catch {}
144
+ }
145
+ }
146
+ fs.unlinkSync(fullPath);
147
+ } catch {
148
+ // Best effort — skip files we can't parse or clean up
149
+ }
150
+ }
151
+ // Clean up legacy log files too
152
+ const logFiles = fs.readdirSync('/tmp').filter(f =>
153
+ f.startsWith('browse-console') || f.startsWith('browse-network') || f.startsWith('browse-dialog')
154
+ );
155
+ for (const file of logFiles) {
156
+ try { fs.unlinkSync(`/tmp/${file}`); } catch {}
157
+ }
158
+ } catch {
159
+ // /tmp read failed — skip legacy cleanup
160
+ }
161
+ }
162
+
163
+ // ─── Server Lifecycle ──────────────────────────────────────────
164
+ async function startServer(): Promise<ServerState> {
165
+ ensureStateDir(config);
166
+
167
+ // Clean up stale state file
168
+ try { fs.unlinkSync(config.stateFile); } catch {}
169
+
170
+ // Start server as detached background process.
171
+ // On Windows, Bun can't launch/connect to Playwright's Chromium (oven-sh/bun#4253, #9911).
172
+ // Fall back to running the server under Node.js with Bun API polyfills.
173
+ const useNode = IS_WINDOWS && NODE_SERVER_SCRIPT;
174
+ const serverCmd = useNode
175
+ ? ['node', NODE_SERVER_SCRIPT]
176
+ : ['bun', 'run', SERVER_SCRIPT];
177
+ const proc = Bun.spawn(serverCmd, {
178
+ stdio: ['ignore', 'pipe', 'pipe'],
179
+ env: { ...process.env, BROWSE_STATE_FILE: config.stateFile },
180
+ });
181
+
182
+ // Don't hold the CLI open
183
+ proc.unref();
184
+
185
+ // Wait for state file to appear
186
+ const start = Date.now();
187
+ while (Date.now() - start < MAX_START_WAIT) {
188
+ const state = readState();
189
+ if (state && isProcessAlive(state.pid)) {
190
+ return state;
191
+ }
192
+ await Bun.sleep(100);
193
+ }
194
+
195
+ // If we get here, server didn't start in time
196
+ // Try to read stderr for error message
197
+ const stderr = proc.stderr;
198
+ if (stderr) {
199
+ const reader = stderr.getReader();
200
+ const { value } = await reader.read();
201
+ if (value) {
202
+ const errText = new TextDecoder().decode(value);
203
+ throw new Error(`Server failed to start:\n${errText}`);
204
+ }
205
+ }
206
+ throw new Error(`Server failed to start within ${MAX_START_WAIT / 1000}s`);
207
+ }
208
+
209
+ /**
210
+ * Acquire an exclusive lockfile to prevent concurrent ensureServer() races (TOCTOU).
211
+ * Returns a cleanup function that releases the lock.
212
+ */
213
+ function acquireServerLock(): (() => void) | null {
214
+ const lockPath = `${config.stateFile}.lock`;
215
+ try {
216
+ // O_CREAT | O_EXCL — fails if file already exists (atomic check-and-create)
217
+ const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
218
+ fs.writeSync(fd, `${process.pid}\n`);
219
+ fs.closeSync(fd);
220
+ return () => { try { fs.unlinkSync(lockPath); } catch {} };
221
+ } catch {
222
+ // Lock already held — check if the holder is still alive
223
+ try {
224
+ const holderPid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
225
+ if (holderPid && isProcessAlive(holderPid)) {
226
+ return null; // Another live process holds the lock
227
+ }
228
+ // Stale lock — remove and retry
229
+ fs.unlinkSync(lockPath);
230
+ return acquireServerLock();
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ }
236
+
237
+ async function ensureServer(): Promise<ServerState> {
238
+ const state = readState();
239
+
240
+ if (state && isProcessAlive(state.pid)) {
241
+ // Check for binary version mismatch (auto-restart on update)
242
+ const currentVersion = readVersionHash();
243
+ if (currentVersion && state.binaryVersion && currentVersion !== state.binaryVersion) {
244
+ console.error('[browse] Binary updated, restarting server...');
245
+ await killServer(state.pid);
246
+ return startServer();
247
+ }
248
+
249
+ // Server appears alive — do a health check
250
+ try {
251
+ const resp = await fetch(`http://127.0.0.1:${state.port}/health`, {
252
+ signal: AbortSignal.timeout(2000),
253
+ });
254
+ if (resp.ok) {
255
+ const health = await resp.json() as any;
256
+ if (health.status === 'healthy') {
257
+ return state;
258
+ }
259
+ }
260
+ } catch {
261
+ // Health check failed — server is dead or unhealthy
262
+ }
263
+ }
264
+
265
+ // Acquire lock to prevent concurrent restart races (TOCTOU)
266
+ const releaseLock = acquireServerLock();
267
+ if (!releaseLock) {
268
+ // Another process is starting the server — wait for it
269
+ console.error('[browse] Another instance is starting the server, waiting...');
270
+ const start = Date.now();
271
+ while (Date.now() - start < MAX_START_WAIT) {
272
+ const freshState = readState();
273
+ if (freshState && isProcessAlive(freshState.pid)) return freshState;
274
+ await Bun.sleep(200);
275
+ }
276
+ throw new Error('Timed out waiting for another instance to start the server');
277
+ }
278
+
279
+ try {
280
+ // Re-read state under lock in case another process just started the server
281
+ const freshState = readState();
282
+ if (freshState && isProcessAlive(freshState.pid)) {
283
+ return freshState;
284
+ }
285
+
286
+ // Kill the old server to avoid orphaned chromium processes
287
+ if (state && state.pid) {
288
+ await killServer(state.pid);
289
+ }
290
+ console.error('[browse] Starting server...');
291
+ return await startServer();
292
+ } finally {
293
+ releaseLock();
294
+ }
295
+ }
296
+
297
+ // ─── Command Dispatch ──────────────────────────────────────────
298
+ async function sendCommand(state: ServerState, command: string, args: string[], retries = 0): Promise<void> {
299
+ const body = JSON.stringify({ command, args });
300
+
301
+ try {
302
+ const resp = await fetch(`http://127.0.0.1:${state.port}/command`, {
303
+ method: 'POST',
304
+ headers: {
305
+ 'Content-Type': 'application/json',
306
+ 'Authorization': `Bearer ${state.token}`,
307
+ },
308
+ body,
309
+ signal: AbortSignal.timeout(30000),
310
+ });
311
+
312
+ if (resp.status === 401) {
313
+ // Token mismatch — server may have restarted
314
+ console.error('[browse] Auth failed — server may have restarted. Retrying...');
315
+ const newState = readState();
316
+ if (newState && newState.token !== state.token) {
317
+ return sendCommand(newState, command, args);
318
+ }
319
+ throw new Error('Authentication failed');
320
+ }
321
+
322
+ const text = await resp.text();
323
+
324
+ if (resp.ok) {
325
+ process.stdout.write(text);
326
+ if (!text.endsWith('\n')) process.stdout.write('\n');
327
+ } else {
328
+ // Try to parse as JSON error
329
+ try {
330
+ const err = JSON.parse(text);
331
+ console.error(err.error || text);
332
+ if (err.hint) console.error(err.hint);
333
+ } catch {
334
+ console.error(text);
335
+ }
336
+ process.exit(1);
337
+ }
338
+ } catch (err: any) {
339
+ if (err.name === 'AbortError') {
340
+ console.error('[browse] Command timed out after 30s');
341
+ process.exit(1);
342
+ }
343
+ // Connection error — server may have crashed
344
+ if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET' || err.message?.includes('fetch failed')) {
345
+ if (retries >= 1) throw new Error('[browse] Server crashed twice in a row — aborting');
346
+ console.error('[browse] Server connection lost. Restarting...');
347
+ // Kill the old server to avoid orphaned chromium processes
348
+ const oldState = readState();
349
+ if (oldState && oldState.pid) {
350
+ await killServer(oldState.pid);
351
+ }
352
+ const newState = await startServer();
353
+ return sendCommand(newState, command, args, retries + 1);
354
+ }
355
+ throw err;
356
+ }
357
+ }
358
+
359
+ // ─── Main ──────────────────────────────────────────────────────
360
+ async function main() {
361
+ const args = process.argv.slice(2);
362
+
363
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
364
+ console.log(`gstack browse — Fast headless browser for AI coding agents
365
+
366
+ Usage: browse <command> [args...]
367
+
368
+ Navigation: goto <url> | back | forward | reload | url
369
+ Content: text | html [sel] | links | forms | accessibility
370
+ Interaction: click <sel> | fill <sel> <val> | select <sel> <val>
371
+ hover <sel> | type <text> | press <key>
372
+ scroll [sel] | wait <sel|--networkidle|--load> | viewport <WxH>
373
+ upload <sel> <file1> [file2...]
374
+ cookie-import <json-file>
375
+ cookie-import-browser [browser] [--domain <d>]
376
+ Inspection: js <expr> | eval <file> | css <sel> <prop> | attrs <sel>
377
+ console [--clear|--errors] | network [--clear] | dialog [--clear]
378
+ cookies | storage [set <k> <v>] | perf
379
+ is <prop> <sel> (visible|hidden|enabled|disabled|checked|editable|focused)
380
+ Visual: screenshot [--viewport] [--clip x,y,w,h] [@ref|sel] [path]
381
+ pdf [path] | responsive [prefix]
382
+ Snapshot: snapshot [-i] [-c] [-d N] [-s sel] [-D] [-a] [-o path] [-C]
383
+ -D/--diff: diff against previous snapshot
384
+ -a/--annotate: annotated screenshot with ref labels
385
+ -C/--cursor-interactive: find non-ARIA clickable elements
386
+ Compare: diff <url1> <url2>
387
+ Multi-step: chain (reads JSON from stdin)
388
+ Tabs: tabs | tab <id> | newtab [url] | closetab [id]
389
+ Server: status | cookie <n>=<v> | header <n>:<v>
390
+ useragent <str> | stop | restart
391
+ Dialogs: dialog-accept [text] | dialog-dismiss
392
+
393
+ Refs: After 'snapshot', use @e1, @e2... as selectors:
394
+ click @e3 | fill @e4 "value" | hover @e1
395
+ @c refs from -C: click @c1`);
396
+ process.exit(0);
397
+ }
398
+
399
+ // One-time cleanup of legacy /tmp state files
400
+ cleanupLegacyState();
401
+
402
+ const command = args[0];
403
+ const commandArgs = args.slice(1);
404
+
405
+ // Special case: chain reads from stdin
406
+ if (command === 'chain' && commandArgs.length === 0) {
407
+ const stdin = await Bun.stdin.text();
408
+ commandArgs.push(stdin.trim());
409
+ }
410
+
411
+ const state = await ensureServer();
412
+ await sendCommand(state, command, commandArgs);
413
+ }
414
+
415
+ if (import.meta.main) {
416
+ main().catch((err) => {
417
+ console.error(`[browse] ${err.message}`);
418
+ process.exit(1);
419
+ });
420
+ }