opengstack 0.13.6 → 0.13.8

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 (135) hide show
  1. package/bin/opengstack.js +35 -90
  2. package/package.json +2 -3
  3. package/scripts/install-skills.js +29 -58
  4. package/skills/browse/bin/find-browse +21 -0
  5. package/skills/browse/bin/remote-slug +14 -0
  6. package/skills/browse/scripts/build-node-server.sh +48 -0
  7. package/skills/browse/src/activity.ts +208 -0
  8. package/skills/browse/src/browser-manager.ts +959 -0
  9. package/skills/browse/src/buffers.ts +137 -0
  10. package/skills/browse/src/bun-polyfill.cjs +109 -0
  11. package/skills/browse/src/cli.ts +678 -0
  12. package/skills/browse/src/commands.ts +128 -0
  13. package/skills/browse/src/config.ts +150 -0
  14. package/skills/browse/src/cookie-import-browser.ts +625 -0
  15. package/skills/browse/src/cookie-picker-routes.ts +230 -0
  16. package/skills/browse/src/cookie-picker-ui.ts +688 -0
  17. package/skills/browse/src/find-browse.ts +61 -0
  18. package/skills/browse/src/meta-commands.ts +550 -0
  19. package/skills/browse/src/platform.ts +17 -0
  20. package/skills/browse/src/read-commands.ts +358 -0
  21. package/skills/browse/src/server.ts +1192 -0
  22. package/skills/browse/src/sidebar-agent.ts +280 -0
  23. package/skills/browse/src/sidebar-utils.ts +21 -0
  24. package/skills/browse/src/snapshot.ts +407 -0
  25. package/skills/browse/src/url-validation.ts +95 -0
  26. package/skills/browse/src/write-commands.ts +364 -0
  27. package/skills/browse/test/activity.test.ts +120 -0
  28. package/skills/browse/test/adversarial-security.test.ts +32 -0
  29. package/skills/browse/test/browser-manager-unit.test.ts +17 -0
  30. package/skills/browse/test/bun-polyfill.test.ts +72 -0
  31. package/skills/browse/test/commands.test.ts +2075 -0
  32. package/skills/browse/test/compare-board.test.ts +342 -0
  33. package/skills/browse/test/config.test.ts +316 -0
  34. package/skills/browse/test/cookie-import-browser.test.ts +519 -0
  35. package/skills/browse/test/cookie-picker-routes.test.ts +260 -0
  36. package/skills/browse/test/file-drop.test.ts +271 -0
  37. package/skills/browse/test/find-browse.test.ts +50 -0
  38. package/skills/browse/test/findport.test.ts +191 -0
  39. package/skills/browse/test/fixtures/basic.html +33 -0
  40. package/skills/browse/test/fixtures/cursor-interactive.html +22 -0
  41. package/skills/browse/test/fixtures/dialog.html +15 -0
  42. package/skills/browse/test/fixtures/empty.html +2 -0
  43. package/skills/browse/test/fixtures/forms.html +55 -0
  44. package/skills/browse/test/fixtures/iframe.html +30 -0
  45. package/skills/browse/test/fixtures/network-idle.html +30 -0
  46. package/skills/browse/test/fixtures/qa-eval-checkout.html +108 -0
  47. package/skills/browse/test/fixtures/qa-eval-spa.html +98 -0
  48. package/skills/browse/test/fixtures/qa-eval.html +51 -0
  49. package/skills/browse/test/fixtures/responsive.html +49 -0
  50. package/skills/browse/test/fixtures/snapshot.html +55 -0
  51. package/skills/browse/test/fixtures/spa.html +24 -0
  52. package/skills/browse/test/fixtures/states.html +17 -0
  53. package/skills/browse/test/fixtures/upload.html +25 -0
  54. package/skills/browse/test/gstack-config.test.ts +138 -0
  55. package/skills/browse/test/gstack-update-check.test.ts +514 -0
  56. package/skills/browse/test/handoff.test.ts +235 -0
  57. package/skills/browse/test/path-validation.test.ts +91 -0
  58. package/skills/browse/test/platform.test.ts +37 -0
  59. package/skills/browse/test/server-auth.test.ts +65 -0
  60. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +226 -0
  61. package/skills/browse/test/sidebar-agent.test.ts +199 -0
  62. package/skills/browse/test/sidebar-integration.test.ts +320 -0
  63. package/skills/browse/test/sidebar-unit.test.ts +96 -0
  64. package/skills/browse/test/snapshot.test.ts +467 -0
  65. package/skills/browse/test/state-ttl.test.ts +35 -0
  66. package/skills/browse/test/test-server.ts +57 -0
  67. package/skills/browse/test/url-validation.test.ts +72 -0
  68. package/skills/browse/test/watch.test.ts +129 -0
  69. package/skills/careful/bin/check-careful.sh +112 -0
  70. package/skills/cso/ACKNOWLEDGEMENTS.md +14 -0
  71. package/skills/freeze/bin/check-freeze.sh +79 -0
  72. package/skills/qa/references/issue-taxonomy.md +85 -0
  73. package/skills/qa/templates/qa-report-template.md +126 -0
  74. package/skills/review/TODOS-format.md +62 -0
  75. package/skills/review/checklist.md +220 -0
  76. package/skills/review/design-checklist.md +132 -0
  77. package/skills/review/greptile-triage.md +220 -0
  78. /package/{autoplan → skills/autoplan}/SKILL.md +0 -0
  79. /package/{autoplan → skills/autoplan}/SKILL.md.tmpl +0 -0
  80. /package/{benchmark → skills/benchmark}/SKILL.md +0 -0
  81. /package/{benchmark → skills/benchmark}/SKILL.md.tmpl +0 -0
  82. /package/{browse → skills/browse}/SKILL.md +0 -0
  83. /package/{browse → skills/browse}/SKILL.md.tmpl +0 -0
  84. /package/{canary → skills/canary}/SKILL.md +0 -0
  85. /package/{canary → skills/canary}/SKILL.md.tmpl +0 -0
  86. /package/{careful → skills/careful}/SKILL.md +0 -0
  87. /package/{careful → skills/careful}/SKILL.md.tmpl +0 -0
  88. /package/{codex → skills/codex}/SKILL.md +0 -0
  89. /package/{codex → skills/codex}/SKILL.md.tmpl +0 -0
  90. /package/{connect-chrome → skills/connect-chrome}/SKILL.md +0 -0
  91. /package/{connect-chrome → skills/connect-chrome}/SKILL.md.tmpl +0 -0
  92. /package/{cso → skills/cso}/SKILL.md +0 -0
  93. /package/{cso → skills/cso}/SKILL.md.tmpl +0 -0
  94. /package/{design-consultation → skills/design-consultation}/SKILL.md +0 -0
  95. /package/{design-consultation → skills/design-consultation}/SKILL.md.tmpl +0 -0
  96. /package/{design-review → skills/design-review}/SKILL.md +0 -0
  97. /package/{design-review → skills/design-review}/SKILL.md.tmpl +0 -0
  98. /package/{design-shotgun → skills/design-shotgun}/SKILL.md +0 -0
  99. /package/{design-shotgun → skills/design-shotgun}/SKILL.md.tmpl +0 -0
  100. /package/{document-release → skills/document-release}/SKILL.md +0 -0
  101. /package/{document-release → skills/document-release}/SKILL.md.tmpl +0 -0
  102. /package/{freeze → skills/freeze}/SKILL.md +0 -0
  103. /package/{freeze → skills/freeze}/SKILL.md.tmpl +0 -0
  104. /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md +0 -0
  105. /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md.tmpl +0 -0
  106. /package/{guard → skills/guard}/SKILL.md +0 -0
  107. /package/{guard → skills/guard}/SKILL.md.tmpl +0 -0
  108. /package/{investigate → skills/investigate}/SKILL.md +0 -0
  109. /package/{investigate → skills/investigate}/SKILL.md.tmpl +0 -0
  110. /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md +0 -0
  111. /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md.tmpl +0 -0
  112. /package/{office-hours → skills/office-hours}/SKILL.md +0 -0
  113. /package/{office-hours → skills/office-hours}/SKILL.md.tmpl +0 -0
  114. /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md +0 -0
  115. /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md.tmpl +0 -0
  116. /package/{plan-design-review → skills/plan-design-review}/SKILL.md +0 -0
  117. /package/{plan-design-review → skills/plan-design-review}/SKILL.md.tmpl +0 -0
  118. /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md +0 -0
  119. /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md.tmpl +0 -0
  120. /package/{qa → skills/qa}/SKILL.md +0 -0
  121. /package/{qa → skills/qa}/SKILL.md.tmpl +0 -0
  122. /package/{qa-only → skills/qa-only}/SKILL.md +0 -0
  123. /package/{qa-only → skills/qa-only}/SKILL.md.tmpl +0 -0
  124. /package/{retro → skills/retro}/SKILL.md +0 -0
  125. /package/{retro → skills/retro}/SKILL.md.tmpl +0 -0
  126. /package/{review → skills/review}/SKILL.md +0 -0
  127. /package/{review → skills/review}/SKILL.md.tmpl +0 -0
  128. /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md +0 -0
  129. /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md.tmpl +0 -0
  130. /package/{setup-deploy → skills/setup-deploy}/SKILL.md +0 -0
  131. /package/{setup-deploy → skills/setup-deploy}/SKILL.md.tmpl +0 -0
  132. /package/{ship → skills/ship}/SKILL.md +0 -0
  133. /package/{ship → skills/ship}/SKILL.md.tmpl +0 -0
  134. /package/{unfreeze → skills/unfreeze}/SKILL.md +0 -0
  135. /package/{unfreeze → skills/unfreeze}/SKILL.md.tmpl +0 -0
package/bin/opengstack.js CHANGED
@@ -1,91 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * OpenGStack CLI - Run skills directly from command line
5
- * Usage: opengstack <skill-name> [args...]
6
- * Example: opengstack ship
4
+ * OpenGStack CLI - Install and manage AI workflow skills
5
+ * Usage: opengstack [options]
6
+ * Example: opengstack --install
7
7
  */
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
12
  const PKG_DIR = path.dirname(__dirname);
13
- const SKILLS_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'skills');
14
-
15
- // Map of skill names to their directories
16
- const skillMap = {
17
- 'ship': 'ship',
18
- 'qa': 'qa',
19
- 'qa-only': 'qa-only',
20
- 'review': 'review',
21
- 'investigate': 'investigate',
22
- 'design-review': 'design-review',
23
- 'plan-ceo-review': 'plan-ceo-review',
24
- 'plan-eng-review': 'plan-eng-review',
25
- 'plan-design-review': 'plan-design-review',
26
- 'office-hours': 'office-hours',
27
- 'design-consultation': 'design-consultation',
28
- 'design-shotgun': 'design-shotgun',
29
- 'document-release': 'document-release',
30
- 'retro': 'retro',
31
- 'browse': 'browse',
32
- 'setup-browser-cookies': 'setup-browser-cookies',
33
- 'setup-deploy': 'setup-deploy',
34
- 'careful': 'careful',
35
- 'freeze': 'freeze',
36
- 'guard': 'guard',
37
- 'unfreeze': 'unfreeze',
38
- 'autoplan': 'autoplan',
39
- 'codex': 'codex',
40
- 'canary': 'canary',
41
- 'benchmark': 'benchmark',
42
- 'cso': 'cso',
43
- 'connect-chrome': 'connect-chrome',
44
- 'land-and-deploy': 'land-and-deploy',
45
- 'gstack-upgrade': 'gstack-upgrade'
46
- };
13
+ const SKILLS_SOURCE = path.join(PKG_DIR, 'skills');
14
+
15
+ function getSkillDescription(skillName) {
16
+ const skillPath = path.join(SKILLS_SOURCE, skillName, 'SKILL.md');
17
+ if (!fs.existsSync(skillPath)) return '';
18
+
19
+ const content = fs.readFileSync(skillPath, 'utf8');
20
+ const match = content.match(/description:\s*\|?\s*([^\n]+)/);
21
+ return match ? match[1].trim() : '';
22
+ }
47
23
 
48
24
  function showHelp() {
49
25
  console.log(`
50
26
  OpenGStack - AI Engineering Workflow Skills
51
27
 
52
- Usage: opengstack <command> [options]
53
-
54
- Commands:
55
- ship Ship workflow: test, review, push, PR
56
- qa Open browser, find bugs, fix, verify
57
- qa-only QA report only — no code changes
58
- review Pre-landing PR review
59
- investigate Root-cause debugging
60
- design-review Design audit + fix loop
61
- plan-ceo-review CEO-level strategic review
62
- plan-eng-review Lock architecture & edge cases
63
- plan-design-review Rate design decisions 0-10
64
- office-hours Brainstorm before building
65
- design-consultation Build a design system from scratch
66
- design-shotgun Generate multiple AI design variants
67
- document-release Update docs post-ship
68
- retro Weekly engineering retrospective
69
- browse Headless browser (real Chromium)
70
- setup-browser-cookies Import cookies for auth testing
71
- setup-deploy Configure deployment settings
72
- careful Warn before destructive ops
73
- freeze Lock edits to one directory
74
- guard Activate careful + freeze
75
- unfreeze Remove directory restrictions
76
- autoplan Run all reviews auto-decisioned
77
- codex OpenAI Codex CLI wrapper
78
- canary Post-deploy monitoring
79
- benchmark Performance regression detection
80
- cso Security audit
81
- connect-chrome Launch Chrome with Side Panel
82
- land-and-deploy Merge PR, deploy, verify health
83
- gstack-upgrade Upgrade gstack to latest version
28
+ Usage: opengstack [options]
84
29
 
85
30
  Options:
86
31
  -h, --help Show this help message
87
32
  -l, --list List all available skills
88
- -i, --install Install skills to ~/.claude/skills/
33
+ -i, --install Install skills to ~/.config/opencode/skills/,
34
+ ~/.claude/skills/, and ~/.agents/skills/
89
35
 
90
36
  In opencode/Claude, use /slash commands:
91
37
  /ship, /qa, /review, etc.
@@ -94,18 +40,24 @@ In opencode/Claude, use /slash commands:
94
40
 
95
41
  function listSkills() {
96
42
  console.log('\nAvailable skills:\n');
97
- Object.entries(skillMap).forEach(([cmd, dir]) => {
98
- const skillPath = path.join(SKILLS_DIR, dir, 'SKILL.md');
43
+
44
+ if (!fs.existsSync(SKILLS_SOURCE)) {
45
+ console.error('❌ No skills/ folder found');
46
+ process.exit(1);
47
+ }
48
+
49
+ fs.readdirSync(SKILLS_SOURCE).forEach(skillName => {
50
+ const skillPath = path.join(SKILLS_SOURCE, skillName, 'SKILL.md');
51
+ if (!fs.existsSync(skillPath)) return;
52
+
53
+ const content = fs.readFileSync(skillPath, 'utf8');
54
+ const match = content.match(/description:\s*\|?\s*([^\n]+)/);
99
55
  let description = '';
100
- if (fs.existsSync(skillPath)) {
101
- const content = fs.readFileSync(skillPath, 'utf8');
102
- const match = content.match(/description:\s*\|?\s*([^\n]+)/);
103
- if (match) {
104
- description = match[1].trim().substring(0, 60);
105
- if (description.length === 60) description += '...';
106
- }
56
+ if (match) {
57
+ description = match[1].trim().substring(0, 60);
58
+ if (description.length === 60) description += '...';
107
59
  }
108
- console.log(` ${cmd.padEnd(20)} ${description}`);
60
+ console.log(` ${skillName.padEnd(20)} ${description}`);
109
61
  });
110
62
  console.log('');
111
63
  }
@@ -134,21 +86,14 @@ function main() {
134
86
  process.exit(0);
135
87
  }
136
88
 
137
- const skillDir = skillMap[command];
138
- if (!skillDir) {
89
+ // Check if skill exists in package
90
+ const skillPath = path.join(SKILLS_SOURCE, command, 'SKILL.md');
91
+ if (!fs.existsSync(skillPath)) {
139
92
  console.error(`Unknown command: ${command}`);
140
93
  console.error('Run "opengstack --help" for available commands');
141
94
  process.exit(1);
142
95
  }
143
96
 
144
- // Check if skill is installed
145
- const skillPath = path.join(SKILLS_DIR, skillDir, 'SKILL.md');
146
- if (!fs.existsSync(skillPath)) {
147
- console.error(`Skill "${command}" not installed.`);
148
- console.error('Run: opengstack --install');
149
- process.exit(1);
150
- }
151
-
152
97
  // Print instructions for using the skill
153
98
  console.log(`\n🎯 Skill: ${command}`);
154
99
  console.log(`📍 Location: ${skillPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opengstack",
3
- "version": "0.13.6",
3
+ "version": "0.13.8",
4
4
  "private": false,
5
5
  "description": "AI Engineering Workflow - SKILL.md files that give AI agents structured roles for software development. Forked from gstack but scrubbed clean of all the YC/Garry Tan cruft and telemetry.",
6
6
  "keywords": [
@@ -35,8 +35,7 @@
35
35
  "CLAUDE.md",
36
36
  "AGENTS.md",
37
37
  "README.md",
38
- "**/SKILL.md",
39
- "**/SKILL.md.tmpl",
38
+ "skills/",
40
39
  "scripts/",
41
40
  "docs/",
42
41
  "bin/"
@@ -1,74 +1,45 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  const fs = require('fs');
4
3
  const path = require('path');
4
+ const os = require('os');
5
5
 
6
- const SKILLS_DIRS = [
7
- path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'skills'),
8
- path.join(process.env.HOME || process.env.USERPROFILE, '.opencode')
9
- ];
10
- const PKG_DIR = path.dirname(__dirname);
11
-
12
- // List of skill directories
13
- const skills = [
14
- 'autoplan', 'benchmark', 'browse', 'canary', 'careful', 'codex',
15
- 'connect-chrome', 'cso', 'design-consultation', 'design-review',
16
- 'design-shotgun', 'document-release', 'freeze', 'gstack-upgrade',
17
- 'guard', 'investigate', 'land-and-deploy', 'office-hours',
18
- 'plan-ceo-review', 'plan-design-review', 'plan-eng-review',
19
- 'qa', 'qa-only', 'retro', 'review', 'setup-browser-cookies',
20
- 'setup-deploy', 'ship', 'unfreeze'
6
+ const SKILLS_SOURCE = path.join(__dirname, '..', 'skills');
7
+ const TARGET_DIRS = [
8
+ path.join(os.homedir(), '.config', 'opencode', 'skills'), // OpenCode native
9
+ path.join(os.homedir(), '.claude', 'skills'), // Claude compat
10
+ path.join(os.homedir(), '.agents', 'skills') // other agents
21
11
  ];
22
12
 
23
- console.log('🔗 Installing OpenGStack skills...');
24
-
25
- // Install to all skill directories
26
- for (const SKILLS_DIR of SKILLS_DIRS) {
27
- console.log(`\n📁 Installing to ${SKILLS_DIR}...`);
28
-
29
- // Ensure skills directory exists
30
- if (!fs.existsSync(SKILLS_DIR)) {
31
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
13
+ function copySkills() {
14
+ if (!fs.existsSync(SKILLS_SOURCE)) {
15
+ console.error('❌ No skills/ folder found in package');
16
+ process.exit(1);
32
17
  }
33
18
 
34
- // Create symlinks for each skill
35
- let installed = 0;
36
- let skipped = 0;
19
+ TARGET_DIRS.forEach(target => {
20
+ if (!fs.existsSync(target)) {
21
+ fs.mkdirSync(target, { recursive: true });
22
+ }
37
23
 
38
- for (const skill of skills) {
39
- const srcPath = path.join(PKG_DIR, skill);
40
- const destPath = path.join(SKILLS_DIR, skill);
24
+ fs.readdirSync(SKILLS_SOURCE).forEach(skillName => {
25
+ const src = path.join(SKILLS_SOURCE, skillName);
26
+ const dest = path.join(target, skillName);
41
27
 
42
- if (!fs.existsSync(srcPath)) {
43
- console.warn(`⚠️ Skill not found: ${skill}`);
44
- skipped++;
45
- continue;
46
- }
28
+ // Skip if not a directory
29
+ if (!fs.statSync(src).isDirectory()) return;
47
30
 
48
- try {
49
- // Remove existing if it's a symlink
50
- if (fs.existsSync(destPath)) {
51
- const stat = fs.lstatSync(destPath);
52
- if (stat.isSymbolicLink()) {
53
- fs.unlinkSync(destPath);
54
- } else {
55
- console.log(`⏭️ Skipping ${skill} (already exists)`);
56
- skipped++;
57
- continue;
58
- }
31
+ if (fs.existsSync(dest)) {
32
+ console.log(`⚠️ Skill ${skillName} already exists skipping`);
33
+ return;
59
34
  }
60
35
 
61
- // Create symlink
62
- fs.symlinkSync(srcPath, destPath, 'dir');
63
- console.log(`✓ ${skill}`);
64
- installed++;
65
- } catch (err) {
66
- console.error(`✗ ${skill}: ${err.message}`);
67
- skipped++;
68
- }
69
- }
36
+ fs.cpSync(src, dest, { recursive: true, force: true });
37
+ console.log(`✅ Installed skill: /${skillName}`);
38
+ });
39
+ });
70
40
 
71
- console.log(`\n Installed ${installed} skills, skipped ${skipped}`);
41
+ console.log('\n🎉 Skills installed! Restart OpenCode (just quit and restart the TUI).');
42
+ console.log('Now just type /qa directly — no /skills menu needed.');
72
43
  }
73
44
 
74
- console.log('\n🎯 Skills are now available in opencode/Claude');
45
+ copySkills();
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # Shim: delegates to compiled find-browse binary, falls back to basic discovery.
3
+ # The compiled binary handles git root detection for workspace-local installs.
4
+ DIR="$(cd "$(dirname "$0")/.." && pwd)/dist"
5
+ if test -x "$DIR/find-browse"; then
6
+ exec "$DIR/find-browse" "$@"
7
+ fi
8
+ # Fallback: basic discovery with priority chain
9
+ ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
10
+ for MARKER in .codex .agents .claude; do
11
+ if [ -n "$ROOT" ] && test -x "$ROOT/$MARKER/skills/gstack/browse/dist/browse"; then
12
+ echo "$ROOT/$MARKER/skills/gstack/browse/dist/browse"
13
+ exit 0
14
+ fi
15
+ if test -x "$HOME/$MARKER/skills/gstack/browse/dist/browse"; then
16
+ echo "$HOME/$MARKER/skills/gstack/browse/dist/browse"
17
+ exit 0
18
+ fi
19
+ done
20
+ echo "ERROR: browse binary not found. Run: cd <skill-dir> && ./setup" >&2
21
+ exit 1
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # Output the remote slug (owner-repo) for the current git repo.
3
+ # Used by SKILL.md files to derive project-specific paths in ~/.gstack/projects/.
4
+ set -e
5
+ URL=$(git remote get-url origin 2>/dev/null || true)
6
+ if [ -n "$URL" ]; then
7
+ # Strip trailing .git if present, then extract owner/repo
8
+ URL="${URL%.git}"
9
+ # Handle both SSH (git@host:owner/repo) and HTTPS (https://host/owner/repo)
10
+ OWNER_REPO=$(echo "$URL" | sed -E 's#.*[:/]([^/]+)/([^/]+)$#\1-\2#')
11
+ echo "$OWNER_REPO"
12
+ else
13
+ basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
14
+ fi
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Build a Node.js-compatible server bundle for Windows.
3
+ #
4
+ # On Windows, Bun can't launch or connect to Playwright's Chromium
5
+ # (oven-sh/bun#4253, #9911). This script produces a server bundle
6
+ # that runs under Node.js with Bun API polyfills.
7
+
8
+ set -e
9
+
10
+ GSTACK_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
11
+ SRC_DIR="$GSTACK_DIR/browse/src"
12
+ DIST_DIR="$GSTACK_DIR/browse/dist"
13
+
14
+ echo "Building Node-compatible server bundle..."
15
+
16
+ # Step 1: Transpile server.ts to a single .mjs bundle (externalize runtime deps)
17
+ bun build "$SRC_DIR/server.ts" \
18
+ --target=node \
19
+ --outfile "$DIST_DIR/server-node.mjs" \
20
+ --external playwright \
21
+ --external playwright-core \
22
+ --external diff \
23
+ --external "bun:sqlite"
24
+
25
+ # Step 2: Post-process
26
+ # Replace import.meta.dir with a resolvable reference
27
+ perl -pi -e 's/import\.meta\.dir/__browseNodeSrcDir/g' "$DIST_DIR/server-node.mjs"
28
+ # Stub out bun:sqlite (macOS-only cookie import, not needed on Windows)
29
+ perl -pi -e 's|import { Database } from "bun:sqlite";|const Database = null; // bun:sqlite stubbed on Node|g' "$DIST_DIR/server-node.mjs"
30
+
31
+ # Step 3: Create the final file with polyfill header injected after the first line
32
+ {
33
+ head -1 "$DIST_DIR/server-node.mjs"
34
+ echo '// ── Windows Node.js compatibility (auto-generated) ──'
35
+ echo 'import { fileURLToPath as _ftp } from "node:url";'
36
+ echo 'import { dirname as _dn } from "node:path";'
37
+ echo 'const __browseNodeSrcDir = _dn(_dn(_ftp(import.meta.url))) + "/src";'
38
+ echo '{ const _r = createRequire(import.meta.url); _r("./bun-polyfill.cjs"); }'
39
+ echo '// ── end compatibility ──'
40
+ tail -n +2 "$DIST_DIR/server-node.mjs"
41
+ } > "$DIST_DIR/server-node.tmp.mjs"
42
+
43
+ mv "$DIST_DIR/server-node.tmp.mjs" "$DIST_DIR/server-node.mjs"
44
+
45
+ # Step 4: Copy polyfill to dist/
46
+ cp "$SRC_DIR/bun-polyfill.cjs" "$DIST_DIR/bun-polyfill.cjs"
47
+
48
+ echo "Node server bundle ready: $DIST_DIR/server-node.mjs"
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Activity streaming — real-time feed of browse commands for the Chrome extension Side Panel
3
+ *
4
+ * Architecture:
5
+ * handleCommand() ──► emitActivity(command_start)
6
+ * ──► emitActivity(command_end)
7
+ * wirePageEvents() ──► emitActivity(navigation)
8
+ *
9
+ * GET /activity/stream?after=ID ──► SSE via ReadableStream
10
+ * GET /activity/history?limit=N ──► REST fallback
11
+ *
12
+ * Privacy: filterArgs() redacts passwords, auth tokens, and sensitive query params.
13
+ * Backpressure: subscribers notified via queueMicrotask (never blocks command path).
14
+ * Gap detection: client sends ?after=ID, server detects if ring buffer overflowed.
15
+ */
16
+
17
+ import { CircularBuffer } from './buffers';
18
+
19
+ // ─── Types ──────────────────────────────────────────────────────
20
+
21
+ export interface ActivityEntry {
22
+ id: number;
23
+ timestamp: number;
24
+ type: 'command_start' | 'command_end' | 'navigation' | 'error';
25
+ command?: string;
26
+ args?: string[];
27
+ url?: string;
28
+ duration?: number;
29
+ status?: 'ok' | 'error';
30
+ error?: string;
31
+ result?: string;
32
+ tabs?: number;
33
+ mode?: string;
34
+ }
35
+
36
+ // ─── Buffer & Subscribers ───────────────────────────────────────
37
+
38
+ const BUFFER_CAPACITY = 1000;
39
+ const activityBuffer = new CircularBuffer<ActivityEntry>(BUFFER_CAPACITY);
40
+ let nextId = 1;
41
+
42
+ type ActivitySubscriber = (entry: ActivityEntry) => void;
43
+ const subscribers = new Set<ActivitySubscriber>();
44
+
45
+ // ─── Privacy Filtering ─────────────────────────────────────────
46
+
47
+ const SENSITIVE_COMMANDS = new Set(['fill', 'type', 'cookie', 'header']);
48
+ const SENSITIVE_PARAM_PATTERN = /\b(password|token|secret|key|auth|bearer|api[_-]?key)\b/i;
49
+
50
+ /**
51
+ * Redact sensitive data from command args before streaming.
52
+ */
53
+ export function filterArgs(command: string, args: string[]): string[] {
54
+ if (!args || args.length === 0) return args;
55
+
56
+ // fill: redact the value (last arg) for password-type fields
57
+ if (command === 'fill' && args.length >= 2) {
58
+ const selector = args[0];
59
+ // If the selector suggests a password field, redact the value
60
+ if (/password|passwd|secret|token/i.test(selector)) {
61
+ return [selector, '[REDACTED]'];
62
+ }
63
+ return args;
64
+ }
65
+
66
+ // header: redact Authorization and other sensitive headers
67
+ if (command === 'header' && args.length >= 1) {
68
+ const headerLine = args[0];
69
+ if (/^(authorization|x-api-key|cookie|set-cookie)/i.test(headerLine)) {
70
+ const colonIdx = headerLine.indexOf(':');
71
+ if (colonIdx > 0) {
72
+ return [headerLine.substring(0, colonIdx + 1) + '[REDACTED]'];
73
+ }
74
+ }
75
+ return args;
76
+ }
77
+
78
+ // cookie: redact cookie values
79
+ if (command === 'cookie' && args.length >= 1) {
80
+ const cookieStr = args[0];
81
+ const eqIdx = cookieStr.indexOf('=');
82
+ if (eqIdx > 0) {
83
+ return [cookieStr.substring(0, eqIdx + 1) + '[REDACTED]'];
84
+ }
85
+ return args;
86
+ }
87
+
88
+ // type: always redact (could be a password field)
89
+ if (command === 'type') {
90
+ return ['[REDACTED]'];
91
+ }
92
+
93
+ // URL args: redact sensitive query params
94
+ return args.map(arg => {
95
+ if (arg.startsWith('http://') || arg.startsWith('https://')) {
96
+ try {
97
+ const url = new URL(arg);
98
+ let redacted = false;
99
+ for (const key of url.searchParams.keys()) {
100
+ if (SENSITIVE_PARAM_PATTERN.test(key)) {
101
+ url.searchParams.set(key, '[REDACTED]');
102
+ redacted = true;
103
+ }
104
+ }
105
+ return redacted ? url.toString() : arg;
106
+ } catch {
107
+ return arg;
108
+ }
109
+ }
110
+ return arg;
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Truncate result text for streaming (max 200 chars).
116
+ */
117
+ function truncateResult(result: string | undefined): string | undefined {
118
+ if (!result) return undefined;
119
+ if (result.length <= 200) return result;
120
+ return result.substring(0, 200) + '...';
121
+ }
122
+
123
+ // ─── Public API ─────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Emit an activity event. Backpressure-safe: subscribers notified asynchronously.
127
+ */
128
+ export function emitActivity(entry: Omit<ActivityEntry, 'id' | 'timestamp'>): ActivityEntry {
129
+ const full: ActivityEntry = {
130
+ ...entry,
131
+ id: nextId++,
132
+ timestamp: Date.now(),
133
+ args: entry.args ? filterArgs(entry.command || '', entry.args) : undefined,
134
+ result: truncateResult(entry.result),
135
+ };
136
+ activityBuffer.push(full);
137
+
138
+ // Notify subscribers asynchronously — never block the command path
139
+ for (const notify of subscribers) {
140
+ queueMicrotask(() => {
141
+ try { notify(full); } catch { /* subscriber error — don't crash */ }
142
+ });
143
+ }
144
+
145
+ return full;
146
+ }
147
+
148
+ /**
149
+ * Subscribe to live activity events. Returns unsubscribe function.
150
+ */
151
+ export function subscribe(fn: ActivitySubscriber): () => void {
152
+ subscribers.add(fn);
153
+ return () => subscribers.delete(fn);
154
+ }
155
+
156
+ /**
157
+ * Get recent activity entries after the given cursor ID.
158
+ * Returns entries and gap info if the buffer has overflowed.
159
+ */
160
+ export function getActivityAfter(afterId: number): {
161
+ entries: ActivityEntry[];
162
+ gap: boolean;
163
+ gapFrom?: number;
164
+ availableFrom?: number;
165
+ totalAdded: number;
166
+ } {
167
+ const total = activityBuffer.totalAdded;
168
+ const allEntries = activityBuffer.toArray();
169
+
170
+ if (afterId === 0) {
171
+ return { entries: allEntries, gap: false, totalAdded: total };
172
+ }
173
+
174
+ // Check for gap: if afterId is too old and has been evicted
175
+ const oldestId = allEntries.length > 0 ? allEntries[0].id : nextId;
176
+ if (afterId < oldestId) {
177
+ return {
178
+ entries: allEntries,
179
+ gap: true,
180
+ gapFrom: afterId + 1,
181
+ availableFrom: oldestId,
182
+ totalAdded: total,
183
+ };
184
+ }
185
+
186
+ // Filter to entries after the cursor
187
+ const filtered = allEntries.filter(e => e.id > afterId);
188
+ return { entries: filtered, gap: false, totalAdded: total };
189
+ }
190
+
191
+ /**
192
+ * Get the N most recent activity entries.
193
+ */
194
+ export function getActivityHistory(limit: number = 50): {
195
+ entries: ActivityEntry[];
196
+ totalAdded: number;
197
+ } {
198
+ const allEntries = activityBuffer.toArray();
199
+ const sliced = limit < allEntries.length ? allEntries.slice(-limit) : allEntries;
200
+ return { entries: sliced, totalAdded: activityBuffer.totalAdded };
201
+ }
202
+
203
+ /**
204
+ * Get subscriber count (for debugging/health).
205
+ */
206
+ export function getSubscriberCount(): number {
207
+ return subscribers.size;
208
+ }