opengstack 0.13.7 → 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.
- package/bin/opengstack.js +35 -90
- package/package.json +2 -3
- package/scripts/install-skills.js +29 -58
- package/skills/browse/bin/find-browse +21 -0
- package/skills/browse/bin/remote-slug +14 -0
- package/skills/browse/scripts/build-node-server.sh +48 -0
- package/skills/browse/src/activity.ts +208 -0
- package/skills/browse/src/browser-manager.ts +959 -0
- package/skills/browse/src/buffers.ts +137 -0
- package/skills/browse/src/bun-polyfill.cjs +109 -0
- package/skills/browse/src/cli.ts +678 -0
- package/skills/browse/src/commands.ts +128 -0
- package/skills/browse/src/config.ts +150 -0
- package/skills/browse/src/cookie-import-browser.ts +625 -0
- package/skills/browse/src/cookie-picker-routes.ts +230 -0
- package/skills/browse/src/cookie-picker-ui.ts +688 -0
- package/skills/browse/src/find-browse.ts +61 -0
- package/skills/browse/src/meta-commands.ts +550 -0
- package/skills/browse/src/platform.ts +17 -0
- package/skills/browse/src/read-commands.ts +358 -0
- package/skills/browse/src/server.ts +1192 -0
- package/skills/browse/src/sidebar-agent.ts +280 -0
- package/skills/browse/src/sidebar-utils.ts +21 -0
- package/skills/browse/src/snapshot.ts +407 -0
- package/skills/browse/src/url-validation.ts +95 -0
- package/skills/browse/src/write-commands.ts +364 -0
- package/skills/browse/test/activity.test.ts +120 -0
- package/skills/browse/test/adversarial-security.test.ts +32 -0
- package/skills/browse/test/browser-manager-unit.test.ts +17 -0
- package/skills/browse/test/bun-polyfill.test.ts +72 -0
- package/skills/browse/test/commands.test.ts +2075 -0
- package/skills/browse/test/compare-board.test.ts +342 -0
- package/skills/browse/test/config.test.ts +316 -0
- package/skills/browse/test/cookie-import-browser.test.ts +519 -0
- package/skills/browse/test/cookie-picker-routes.test.ts +260 -0
- package/skills/browse/test/file-drop.test.ts +271 -0
- package/skills/browse/test/find-browse.test.ts +50 -0
- package/skills/browse/test/findport.test.ts +191 -0
- package/skills/browse/test/fixtures/basic.html +33 -0
- package/skills/browse/test/fixtures/cursor-interactive.html +22 -0
- package/skills/browse/test/fixtures/dialog.html +15 -0
- package/skills/browse/test/fixtures/empty.html +2 -0
- package/skills/browse/test/fixtures/forms.html +55 -0
- package/skills/browse/test/fixtures/iframe.html +30 -0
- package/skills/browse/test/fixtures/network-idle.html +30 -0
- package/skills/browse/test/fixtures/qa-eval-checkout.html +108 -0
- package/skills/browse/test/fixtures/qa-eval-spa.html +98 -0
- package/skills/browse/test/fixtures/qa-eval.html +51 -0
- package/skills/browse/test/fixtures/responsive.html +49 -0
- package/skills/browse/test/fixtures/snapshot.html +55 -0
- package/skills/browse/test/fixtures/spa.html +24 -0
- package/skills/browse/test/fixtures/states.html +17 -0
- package/skills/browse/test/fixtures/upload.html +25 -0
- package/skills/browse/test/gstack-config.test.ts +138 -0
- package/skills/browse/test/gstack-update-check.test.ts +514 -0
- package/skills/browse/test/handoff.test.ts +235 -0
- package/skills/browse/test/path-validation.test.ts +91 -0
- package/skills/browse/test/platform.test.ts +37 -0
- package/skills/browse/test/server-auth.test.ts +65 -0
- package/skills/browse/test/sidebar-agent-roundtrip.test.ts +226 -0
- package/skills/browse/test/sidebar-agent.test.ts +199 -0
- package/skills/browse/test/sidebar-integration.test.ts +320 -0
- package/skills/browse/test/sidebar-unit.test.ts +96 -0
- package/skills/browse/test/snapshot.test.ts +467 -0
- package/skills/browse/test/state-ttl.test.ts +35 -0
- package/skills/browse/test/test-server.ts +57 -0
- package/skills/browse/test/url-validation.test.ts +72 -0
- package/skills/browse/test/watch.test.ts +129 -0
- package/skills/careful/bin/check-careful.sh +112 -0
- package/skills/cso/ACKNOWLEDGEMENTS.md +14 -0
- package/skills/freeze/bin/check-freeze.sh +79 -0
- package/skills/qa/references/issue-taxonomy.md +85 -0
- package/skills/qa/templates/qa-report-template.md +126 -0
- package/skills/review/TODOS-format.md +62 -0
- package/skills/review/checklist.md +220 -0
- package/skills/review/design-checklist.md +132 -0
- package/skills/review/greptile-triage.md +220 -0
- /package/{autoplan → skills/autoplan}/SKILL.md +0 -0
- /package/{autoplan → skills/autoplan}/SKILL.md.tmpl +0 -0
- /package/{benchmark → skills/benchmark}/SKILL.md +0 -0
- /package/{benchmark → skills/benchmark}/SKILL.md.tmpl +0 -0
- /package/{browse → skills/browse}/SKILL.md +0 -0
- /package/{browse → skills/browse}/SKILL.md.tmpl +0 -0
- /package/{canary → skills/canary}/SKILL.md +0 -0
- /package/{canary → skills/canary}/SKILL.md.tmpl +0 -0
- /package/{careful → skills/careful}/SKILL.md +0 -0
- /package/{careful → skills/careful}/SKILL.md.tmpl +0 -0
- /package/{codex → skills/codex}/SKILL.md +0 -0
- /package/{codex → skills/codex}/SKILL.md.tmpl +0 -0
- /package/{connect-chrome → skills/connect-chrome}/SKILL.md +0 -0
- /package/{connect-chrome → skills/connect-chrome}/SKILL.md.tmpl +0 -0
- /package/{cso → skills/cso}/SKILL.md +0 -0
- /package/{cso → skills/cso}/SKILL.md.tmpl +0 -0
- /package/{design-consultation → skills/design-consultation}/SKILL.md +0 -0
- /package/{design-consultation → skills/design-consultation}/SKILL.md.tmpl +0 -0
- /package/{design-review → skills/design-review}/SKILL.md +0 -0
- /package/{design-review → skills/design-review}/SKILL.md.tmpl +0 -0
- /package/{design-shotgun → skills/design-shotgun}/SKILL.md +0 -0
- /package/{design-shotgun → skills/design-shotgun}/SKILL.md.tmpl +0 -0
- /package/{document-release → skills/document-release}/SKILL.md +0 -0
- /package/{document-release → skills/document-release}/SKILL.md.tmpl +0 -0
- /package/{freeze → skills/freeze}/SKILL.md +0 -0
- /package/{freeze → skills/freeze}/SKILL.md.tmpl +0 -0
- /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md +0 -0
- /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md.tmpl +0 -0
- /package/{guard → skills/guard}/SKILL.md +0 -0
- /package/{guard → skills/guard}/SKILL.md.tmpl +0 -0
- /package/{investigate → skills/investigate}/SKILL.md +0 -0
- /package/{investigate → skills/investigate}/SKILL.md.tmpl +0 -0
- /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md +0 -0
- /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md.tmpl +0 -0
- /package/{office-hours → skills/office-hours}/SKILL.md +0 -0
- /package/{office-hours → skills/office-hours}/SKILL.md.tmpl +0 -0
- /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md +0 -0
- /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md.tmpl +0 -0
- /package/{plan-design-review → skills/plan-design-review}/SKILL.md +0 -0
- /package/{plan-design-review → skills/plan-design-review}/SKILL.md.tmpl +0 -0
- /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md +0 -0
- /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md.tmpl +0 -0
- /package/{qa → skills/qa}/SKILL.md +0 -0
- /package/{qa → skills/qa}/SKILL.md.tmpl +0 -0
- /package/{qa-only → skills/qa-only}/SKILL.md +0 -0
- /package/{qa-only → skills/qa-only}/SKILL.md.tmpl +0 -0
- /package/{retro → skills/retro}/SKILL.md +0 -0
- /package/{retro → skills/retro}/SKILL.md.tmpl +0 -0
- /package/{review → skills/review}/SKILL.md +0 -0
- /package/{review → skills/review}/SKILL.md.tmpl +0 -0
- /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md +0 -0
- /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md.tmpl +0 -0
- /package/{setup-deploy → skills/setup-deploy}/SKILL.md +0 -0
- /package/{setup-deploy → skills/setup-deploy}/SKILL.md.tmpl +0 -0
- /package/{ship → skills/ship}/SKILL.md +0 -0
- /package/{ship → skills/ship}/SKILL.md.tmpl +0 -0
- /package/{unfreeze → skills/unfreeze}/SKILL.md +0 -0
- /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 -
|
|
5
|
-
* Usage: opengstack
|
|
6
|
-
* Example: opengstack
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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 ~/.
|
|
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
|
-
|
|
98
|
-
|
|
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 (
|
|
101
|
-
|
|
102
|
-
|
|
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(` ${
|
|
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
|
-
|
|
138
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
7
|
-
|
|
8
|
-
path.join(
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
TARGET_DIRS.forEach(target => {
|
|
20
|
+
if (!fs.existsSync(target)) {
|
|
21
|
+
fs.mkdirSync(target, { recursive: true });
|
|
22
|
+
}
|
|
37
23
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
skipped++;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
28
|
+
// Skip if not a directory
|
|
29
|
+
if (!fs.statSync(src).isDirectory()) return;
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|