@runchr/gstack-antigravity 0.1.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.
Potentially problematic release.
This version of @runchr/gstack-antigravity might be problematic. Click here for more details.
- package/.agents/rules/ETHOS.md +129 -0
- package/.agents/rules/global-gstack.md +117 -0
- package/.agents/rules/persona-gstack-autoplan.md +14 -0
- package/.agents/rules/persona-gstack-benchmark.md +14 -0
- package/.agents/rules/persona-gstack-browse.md +14 -0
- package/.agents/rules/persona-gstack-canary.md +14 -0
- package/.agents/rules/persona-gstack-careful.md +14 -0
- package/.agents/rules/persona-gstack-codex.md +14 -0
- package/.agents/rules/persona-gstack-cso.md +14 -0
- package/.agents/rules/persona-gstack-design-consultation.md +14 -0
- package/.agents/rules/persona-gstack-design-review.md +14 -0
- package/.agents/rules/persona-gstack-document-release.md +14 -0
- package/.agents/rules/persona-gstack-freeze.md +14 -0
- package/.agents/rules/persona-gstack-gstack-upgrade.md +14 -0
- package/.agents/rules/persona-gstack-guard.md +14 -0
- package/.agents/rules/persona-gstack-investigate.md +14 -0
- package/.agents/rules/persona-gstack-land-and-deploy.md +14 -0
- package/.agents/rules/persona-gstack-office-hours.md +14 -0
- package/.agents/rules/persona-gstack-plan-ceo-review.md +14 -0
- package/.agents/rules/persona-gstack-plan-design-review.md +14 -0
- package/.agents/rules/persona-gstack-plan-eng-review.md +14 -0
- package/.agents/rules/persona-gstack-qa-only.md +14 -0
- package/.agents/rules/persona-gstack-qa.md +14 -0
- package/.agents/rules/persona-gstack-retro.md +14 -0
- package/.agents/rules/persona-gstack-review.md +14 -0
- package/.agents/rules/persona-gstack-setup-browser-cookies.md +14 -0
- package/.agents/rules/persona-gstack-setup-deploy.md +14 -0
- package/.agents/rules/persona-gstack-ship.md +14 -0
- package/.agents/rules/persona-gstack-unfreeze.md +14 -0
- package/.agents/rules/persona-gstack.md +40 -0
- package/.agents/rules/recursive-identities.md +22 -0
- package/.agents/workflows/autoplan.md +30 -0
- package/.agents/workflows/benchmark.md +31 -0
- package/.agents/workflows/browse.md +26 -0
- package/.agents/workflows/canary.md +33 -0
- package/.agents/workflows/careful.md +22 -0
- package/.agents/workflows/codex.md +36 -0
- package/.agents/workflows/cso.md +29 -0
- package/.agents/workflows/design-consultation.md +28 -0
- package/.agents/workflows/design-review.md +28 -0
- package/.agents/workflows/document-release.md +32 -0
- package/.agents/workflows/freeze.md +17 -0
- package/.agents/workflows/gstack-upgrade.md +54 -0
- package/.agents/workflows/gstack.md +56 -0
- package/.agents/workflows/guard.md +18 -0
- package/.agents/workflows/investigate.md +37 -0
- package/.agents/workflows/land-and-deploy.md +35 -0
- package/.agents/workflows/office-hours.md +27 -0
- package/.agents/workflows/plan-ceo-review.md +34 -0
- package/.agents/workflows/plan-design-review.md +31 -0
- package/.agents/workflows/plan-eng-review.md +28 -0
- package/.agents/workflows/qa-only.md +28 -0
- package/.agents/workflows/qa.md +73 -0
- package/.agents/workflows/retro.md +34 -0
- package/.agents/workflows/review.md +30 -0
- package/.agents/workflows/setup-browser-cookies.md +15 -0
- package/.agents/workflows/setup-cookies.md +8 -0
- package/.agents/workflows/setup-deploy.md +21 -0
- package/.agents/workflows/ship.md +93 -0
- package/.agents/workflows/unfreeze.md +12 -0
- package/LICENSE +22 -0
- package/README.md +189 -0
- package/README_KO.md +191 -0
- package/bin/install.js +105 -0
- package/gstack-origin/.agents/skills/gstack/SKILL.md +651 -0
- package/gstack-origin/.agents/skills/gstack-autoplan/SKILL.md +678 -0
- package/gstack-origin/.agents/skills/gstack-benchmark/SKILL.md +482 -0
- package/gstack-origin/.agents/skills/gstack-browse/SKILL.md +511 -0
- package/gstack-origin/.agents/skills/gstack-canary/SKILL.md +486 -0
- package/gstack-origin/.agents/skills/gstack-careful/SKILL.md +50 -0
- package/gstack-origin/.agents/skills/gstack-cso/SKILL.md +607 -0
- package/gstack-origin/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
- package/gstack-origin/.agents/skills/gstack-design-review/SKILL.md +988 -0
- package/gstack-origin/.agents/skills/gstack-document-release/SKILL.md +604 -0
- package/gstack-origin/.agents/skills/gstack-freeze/SKILL.md +67 -0
- package/gstack-origin/.agents/skills/gstack-guard/SKILL.md +62 -0
- package/gstack-origin/.agents/skills/gstack-investigate/SKILL.md +415 -0
- package/gstack-origin/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
- package/gstack-origin/.agents/skills/gstack-office-hours/SKILL.md +986 -0
- package/gstack-origin/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
- package/gstack-origin/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
- package/gstack-origin/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
- package/gstack-origin/.agents/skills/gstack-qa/SKILL.md +1006 -0
- package/gstack-origin/.agents/skills/gstack-qa-only/SKILL.md +626 -0
- package/gstack-origin/.agents/skills/gstack-retro/SKILL.md +1065 -0
- package/gstack-origin/.agents/skills/gstack-review/SKILL.md +704 -0
- package/gstack-origin/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
- package/gstack-origin/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
- package/gstack-origin/.agents/skills/gstack-ship/SKILL.md +1312 -0
- package/gstack-origin/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
- package/gstack-origin/.agents/skills/gstack-upgrade/SKILL.md +220 -0
- package/gstack-origin/.env.example +5 -0
- package/gstack-origin/.github/workflows/skill-docs.yml +17 -0
- package/gstack-origin/AGENTS.md +49 -0
- package/gstack-origin/ARCHITECTURE.md +359 -0
- package/gstack-origin/BROWSER.md +271 -0
- package/gstack-origin/CHANGELOG.md +800 -0
- package/gstack-origin/CLAUDE.md +284 -0
- package/gstack-origin/CONTRIBUTING.md +370 -0
- package/gstack-origin/ETHOS.md +129 -0
- package/gstack-origin/LICENSE +21 -0
- package/gstack-origin/README.md +228 -0
- package/gstack-origin/SKILL.md +657 -0
- package/gstack-origin/SKILL.md.tmpl +281 -0
- package/gstack-origin/TODOS.md +564 -0
- package/gstack-origin/VERSION +1 -0
- package/gstack-origin/autoplan/SKILL.md +689 -0
- package/gstack-origin/autoplan/SKILL.md.tmpl +416 -0
- package/gstack-origin/benchmark/SKILL.md +489 -0
- package/gstack-origin/benchmark/SKILL.md.tmpl +233 -0
- package/gstack-origin/bin/dev-setup +68 -0
- package/gstack-origin/bin/dev-teardown +56 -0
- package/gstack-origin/bin/gstack-analytics +191 -0
- package/gstack-origin/bin/gstack-community-dashboard +113 -0
- package/gstack-origin/bin/gstack-config +38 -0
- package/gstack-origin/bin/gstack-diff-scope +71 -0
- package/gstack-origin/bin/gstack-global-discover.ts +591 -0
- package/gstack-origin/bin/gstack-repo-mode +93 -0
- package/gstack-origin/bin/gstack-review-log +9 -0
- package/gstack-origin/bin/gstack-review-read +12 -0
- package/gstack-origin/bin/gstack-slug +15 -0
- package/gstack-origin/bin/gstack-telemetry-log +158 -0
- package/gstack-origin/bin/gstack-telemetry-sync +127 -0
- package/gstack-origin/bin/gstack-update-check +196 -0
- package/gstack-origin/browse/SKILL.md +517 -0
- package/gstack-origin/browse/SKILL.md.tmpl +141 -0
- package/gstack-origin/browse/bin/find-browse +21 -0
- package/gstack-origin/browse/bin/remote-slug +14 -0
- package/gstack-origin/browse/scripts/build-node-server.sh +48 -0
- package/gstack-origin/browse/src/browser-manager.ts +634 -0
- package/gstack-origin/browse/src/buffers.ts +137 -0
- package/gstack-origin/browse/src/bun-polyfill.cjs +109 -0
- package/gstack-origin/browse/src/cli.ts +420 -0
- package/gstack-origin/browse/src/commands.ts +111 -0
- package/gstack-origin/browse/src/config.ts +150 -0
- package/gstack-origin/browse/src/cookie-import-browser.ts +417 -0
- package/gstack-origin/browse/src/cookie-picker-routes.ts +207 -0
- package/gstack-origin/browse/src/cookie-picker-ui.ts +541 -0
- package/gstack-origin/browse/src/find-browse.ts +61 -0
- package/gstack-origin/browse/src/meta-commands.ts +269 -0
- package/gstack-origin/browse/src/platform.ts +17 -0
- package/gstack-origin/browse/src/read-commands.ts +335 -0
- package/gstack-origin/browse/src/server.ts +369 -0
- package/gstack-origin/browse/src/snapshot.ts +398 -0
- package/gstack-origin/browse/src/url-validation.ts +91 -0
- package/gstack-origin/browse/src/write-commands.ts +352 -0
- package/gstack-origin/browse/test/bun-polyfill.test.ts +72 -0
- package/gstack-origin/browse/test/commands.test.ts +1836 -0
- package/gstack-origin/browse/test/config.test.ts +250 -0
- package/gstack-origin/browse/test/cookie-import-browser.test.ts +397 -0
- package/gstack-origin/browse/test/cookie-picker-routes.test.ts +205 -0
- package/gstack-origin/browse/test/find-browse.test.ts +50 -0
- package/gstack-origin/browse/test/fixtures/basic.html +33 -0
- package/gstack-origin/browse/test/fixtures/cursor-interactive.html +22 -0
- package/gstack-origin/browse/test/fixtures/dialog.html +15 -0
- package/gstack-origin/browse/test/fixtures/empty.html +2 -0
- package/gstack-origin/browse/test/fixtures/forms.html +55 -0
- package/gstack-origin/browse/test/fixtures/qa-eval-checkout.html +108 -0
- package/gstack-origin/browse/test/fixtures/qa-eval-spa.html +98 -0
- package/gstack-origin/browse/test/fixtures/qa-eval.html +51 -0
- package/gstack-origin/browse/test/fixtures/responsive.html +49 -0
- package/gstack-origin/browse/test/fixtures/snapshot.html +55 -0
- package/gstack-origin/browse/test/fixtures/spa.html +24 -0
- package/gstack-origin/browse/test/fixtures/states.html +17 -0
- package/gstack-origin/browse/test/fixtures/upload.html +25 -0
- package/gstack-origin/browse/test/gstack-config.test.ts +125 -0
- package/gstack-origin/browse/test/gstack-update-check.test.ts +467 -0
- package/gstack-origin/browse/test/handoff.test.ts +235 -0
- package/gstack-origin/browse/test/path-validation.test.ts +63 -0
- package/gstack-origin/browse/test/platform.test.ts +37 -0
- package/gstack-origin/browse/test/snapshot.test.ts +467 -0
- package/gstack-origin/browse/test/test-server.ts +57 -0
- package/gstack-origin/browse/test/url-validation.test.ts +72 -0
- package/gstack-origin/canary/SKILL.md +493 -0
- package/gstack-origin/canary/SKILL.md.tmpl +220 -0
- package/gstack-origin/careful/SKILL.md +59 -0
- package/gstack-origin/careful/SKILL.md.tmpl +57 -0
- package/gstack-origin/careful/bin/check-careful.sh +112 -0
- package/gstack-origin/codex/SKILL.md +677 -0
- package/gstack-origin/codex/SKILL.md.tmpl +356 -0
- package/gstack-origin/conductor.json +6 -0
- package/gstack-origin/cso/SKILL.md +615 -0
- package/gstack-origin/cso/SKILL.md.tmpl +376 -0
- package/gstack-origin/design-consultation/SKILL.md +625 -0
- package/gstack-origin/design-consultation/SKILL.md.tmpl +369 -0
- package/gstack-origin/design-review/SKILL.md +998 -0
- package/gstack-origin/design-review/SKILL.md.tmpl +262 -0
- package/gstack-origin/docs/images/github-2013.png +0 -0
- package/gstack-origin/docs/images/github-2026.png +0 -0
- package/gstack-origin/docs/skills.md +877 -0
- package/gstack-origin/document-release/SKILL.md +613 -0
- package/gstack-origin/document-release/SKILL.md.tmpl +357 -0
- package/gstack-origin/freeze/SKILL.md +82 -0
- package/gstack-origin/freeze/SKILL.md.tmpl +80 -0
- package/gstack-origin/freeze/bin/check-freeze.sh +68 -0
- package/gstack-origin/gstack-upgrade/SKILL.md +226 -0
- package/gstack-origin/gstack-upgrade/SKILL.md.tmpl +224 -0
- package/gstack-origin/guard/SKILL.md +82 -0
- package/gstack-origin/guard/SKILL.md.tmpl +80 -0
- package/gstack-origin/investigate/SKILL.md +435 -0
- package/gstack-origin/investigate/SKILL.md.tmpl +196 -0
- package/gstack-origin/land-and-deploy/SKILL.md +880 -0
- package/gstack-origin/land-and-deploy/SKILL.md.tmpl +575 -0
- package/gstack-origin/office-hours/SKILL.md +996 -0
- package/gstack-origin/office-hours/SKILL.md.tmpl +624 -0
- package/gstack-origin/package.json +55 -0
- package/gstack-origin/plan-ceo-review/SKILL.md +1277 -0
- package/gstack-origin/plan-ceo-review/SKILL.md.tmpl +838 -0
- package/gstack-origin/plan-design-review/SKILL.md +676 -0
- package/gstack-origin/plan-design-review/SKILL.md.tmpl +314 -0
- package/gstack-origin/plan-eng-review/SKILL.md +836 -0
- package/gstack-origin/plan-eng-review/SKILL.md.tmpl +279 -0
- package/gstack-origin/qa/SKILL.md +1016 -0
- package/gstack-origin/qa/SKILL.md.tmpl +316 -0
- package/gstack-origin/qa/references/issue-taxonomy.md +85 -0
- package/gstack-origin/qa/templates/qa-report-template.md +126 -0
- package/gstack-origin/qa-only/SKILL.md +633 -0
- package/gstack-origin/qa-only/SKILL.md.tmpl +101 -0
- package/gstack-origin/retro/SKILL.md +1072 -0
- package/gstack-origin/retro/SKILL.md.tmpl +833 -0
- package/gstack-origin/review/SKILL.md +849 -0
- package/gstack-origin/review/SKILL.md.tmpl +259 -0
- package/gstack-origin/review/TODOS-format.md +62 -0
- package/gstack-origin/review/checklist.md +190 -0
- package/gstack-origin/review/design-checklist.md +132 -0
- package/gstack-origin/review/greptile-triage.md +220 -0
- package/gstack-origin/scripts/analytics.ts +190 -0
- package/gstack-origin/scripts/dev-skill.ts +82 -0
- package/gstack-origin/scripts/eval-compare.ts +96 -0
- package/gstack-origin/scripts/eval-list.ts +116 -0
- package/gstack-origin/scripts/eval-select.ts +86 -0
- package/gstack-origin/scripts/eval-summary.ts +187 -0
- package/gstack-origin/scripts/eval-watch.ts +172 -0
- package/gstack-origin/scripts/gen-skill-docs.ts +2414 -0
- package/gstack-origin/scripts/skill-check.ts +167 -0
- package/gstack-origin/setup +269 -0
- package/gstack-origin/setup-browser-cookies/SKILL.md +330 -0
- package/gstack-origin/setup-browser-cookies/SKILL.md.tmpl +74 -0
- package/gstack-origin/setup-deploy/SKILL.md +459 -0
- package/gstack-origin/setup-deploy/SKILL.md.tmpl +220 -0
- package/gstack-origin/ship/SKILL.md +1457 -0
- package/gstack-origin/ship/SKILL.md.tmpl +528 -0
- package/gstack-origin/supabase/config.sh +10 -0
- package/gstack-origin/supabase/functions/community-pulse/index.ts +59 -0
- package/gstack-origin/supabase/functions/telemetry-ingest/index.ts +135 -0
- package/gstack-origin/supabase/functions/update-check/index.ts +37 -0
- package/gstack-origin/supabase/migrations/001_telemetry.sql +89 -0
- package/gstack-origin/test/analytics.test.ts +277 -0
- package/gstack-origin/test/codex-e2e.test.ts +197 -0
- package/gstack-origin/test/fixtures/coverage-audit-fixture.ts +76 -0
- package/gstack-origin/test/fixtures/eval-baselines.json +7 -0
- package/gstack-origin/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
- package/gstack-origin/test/fixtures/qa-eval-ground-truth.json +43 -0
- package/gstack-origin/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
- package/gstack-origin/test/fixtures/review-eval-design-slop.css +86 -0
- package/gstack-origin/test/fixtures/review-eval-design-slop.html +41 -0
- package/gstack-origin/test/fixtures/review-eval-enum-diff.rb +30 -0
- package/gstack-origin/test/fixtures/review-eval-enum.rb +27 -0
- package/gstack-origin/test/fixtures/review-eval-vuln.rb +14 -0
- package/gstack-origin/test/gemini-e2e.test.ts +173 -0
- package/gstack-origin/test/gen-skill-docs.test.ts +1049 -0
- package/gstack-origin/test/global-discover.test.ts +187 -0
- package/gstack-origin/test/helpers/codex-session-runner.ts +282 -0
- package/gstack-origin/test/helpers/e2e-helpers.ts +239 -0
- package/gstack-origin/test/helpers/eval-store.test.ts +548 -0
- package/gstack-origin/test/helpers/eval-store.ts +689 -0
- package/gstack-origin/test/helpers/gemini-session-runner.test.ts +104 -0
- package/gstack-origin/test/helpers/gemini-session-runner.ts +201 -0
- package/gstack-origin/test/helpers/llm-judge.ts +130 -0
- package/gstack-origin/test/helpers/observability.test.ts +283 -0
- package/gstack-origin/test/helpers/session-runner.test.ts +96 -0
- package/gstack-origin/test/helpers/session-runner.ts +357 -0
- package/gstack-origin/test/helpers/skill-parser.ts +206 -0
- package/gstack-origin/test/helpers/touchfiles.ts +260 -0
- package/gstack-origin/test/hook-scripts.test.ts +373 -0
- package/gstack-origin/test/skill-e2e-browse.test.ts +293 -0
- package/gstack-origin/test/skill-e2e-deploy.test.ts +279 -0
- package/gstack-origin/test/skill-e2e-design.test.ts +614 -0
- package/gstack-origin/test/skill-e2e-plan.test.ts +538 -0
- package/gstack-origin/test/skill-e2e-qa-bugs.test.ts +194 -0
- package/gstack-origin/test/skill-e2e-qa-workflow.test.ts +412 -0
- package/gstack-origin/test/skill-e2e-review.test.ts +535 -0
- package/gstack-origin/test/skill-e2e-workflow.test.ts +586 -0
- package/gstack-origin/test/skill-e2e.test.ts +3325 -0
- package/gstack-origin/test/skill-llm-eval.test.ts +787 -0
- package/gstack-origin/test/skill-parser.test.ts +179 -0
- package/gstack-origin/test/skill-routing-e2e.test.ts +605 -0
- package/gstack-origin/test/skill-validation.test.ts +1520 -0
- package/gstack-origin/test/telemetry.test.ts +278 -0
- package/gstack-origin/test/touchfiles.test.ts +262 -0
- package/gstack-origin/unfreeze/SKILL.md +40 -0
- package/gstack-origin/unfreeze/SKILL.md.tmpl +38 -0
- package/package.json +38 -0
- package/scripts/install-antigravity-skill.ps1 +33 -0
- package/scripts/install-antigravity-skill.sh +41 -0
- package/scripts/sync-gstack-origin.ps1 +37 -0
- package/scripts/sync-gstack-origin.sh +35 -0
|
@@ -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
|
+
}
|