Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions .github/scripts/sync-bb-sites.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
#!/usr/bin/env node
/**
* Parse bb-sites scripts and POST them to the tap web API batch endpoint.
* Parse site scripts and POST them to the tap web API batch endpoint.
* Accepts one or more directories; later directories override earlier ones
* when script names collide (matching the CLI registry priority).
*
* Usage: node sync-bb-sites.mjs <bb-sites-dir>
* Usage: node sync-bb-sites.mjs <dir> [<dir> ...]
*
* Env:
* TAP_SCRIPTS_SECRET - shared secret for X-Tap-Secret header
* TAP_API_URL - batch endpoint (default: https://tap.vaayne.com/api/batch)
*/

import { readdir, readFile } from "node:fs/promises"
import { join, basename, dirname } from "node:path"
import { join } from "node:path"
import { createHash } from "node:crypto"

const sitesDir = process.argv[2]
if (!sitesDir) {
console.error("Usage: node sync-bb-sites.mjs <bb-sites-dir>")
const sitesDirs = process.argv.slice(2)
if (sitesDirs.length === 0) {
console.error("Usage: node sync-bb-sites.mjs <dir> [<dir> ...]")
process.exit(1)
}

Expand All @@ -42,19 +44,23 @@ function parseMeta(content) {
}

/**
* Discover all .js files under sitesDir organized as site/action.js.
* Discover all .js files under dir organized as site/action.js.
*/
async function discoverScripts() {
async function discoverScripts(dir) {
const scripts = []
const entries = await readdir(sitesDir, { withFileTypes: true })
let entries
try {
entries = await readdir(dir, { withFileTypes: true })
} catch {
return scripts
}

for (const entry of entries) {
if (!entry.isDirectory()) continue
const site = entry.name
// Skip hidden dirs and common non-script dirs
if (site.startsWith(".") || site === "node_modules") continue

const siteDir = join(sitesDir, site)
const siteDir = join(dir, site)
const files = await readdir(siteDir)

for (const file of files) {
Expand All @@ -68,7 +74,6 @@ async function discoverScripts() {
}

const hash = createHash("sha256").update(content).digest("hex")
const action = basename(file, ".js")

scripts.push({
name: meta.name,
Expand All @@ -89,13 +94,21 @@ async function discoverScripts() {
}

async function main() {
const scripts = await discoverScripts()
const byName = new Map()
for (const dir of sitesDirs) {
const found = await discoverScripts(dir)
for (const s of found) {
byName.set(s.name, s)
}
console.log(`${dir}: ${found.length} scripts`)
}
const scripts = [...byName.values()]
if (scripts.length === 0) {
console.error("No scripts found")
process.exit(1)
}

console.log(`Found ${scripts.length} scripts, posting to ${apiUrl}`)
console.log(`Total: ${scripts.length} scripts, posting to ${apiUrl}`)

const resp = await fetch(apiUrl, {
method: "POST",
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/sync-sites.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ jobs:

- uses: actions/checkout@v4
with:
repository: vaayne/bb-sites
repository: epiral/bb-sites
path: bb-sites

- uses: actions/setup-node@v4
with:
node-version: "22"
node-version: "24"

- name: Build payload and sync
env:
TAP_SCRIPTS_SECRET: ${{ secrets.TAP_SCRIPTS_SECRET }}
TAP_API_URL: https://tap.vaayne.com/api/batch
run: node .github/scripts/sync-bb-sites.mjs bb-sites
run: node .github/scripts/sync-bb-sites.mjs bb-sites sites
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ Thumbs.db
.env
!.env.example
/tap

.agents/sessions/
32 changes: 4 additions & 28 deletions cmd/tap/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package main

import (
"context"
"errors"
"fmt"
"os"
"strings"

"github.com/urfave/cli/v3"
"github.com/vaayne/tap"
"github.com/vaayne/tap/script"
)

Expand Down Expand Up @@ -86,36 +86,12 @@ func loadCompletionRegistry(cmd *cli.Command) (*script.Registry, error) {
if dir == "" {
dir = defaultSitesDir()
}

overrideDir := defaultLocalOverrideDir()
if cmd.Bool("local-only") {
return loadRegistryDir(overrideDir)
}

reg, err := script.NewRegistryWithOverride(dir, overrideDir)
if err == nil {
return reg, nil
}
if !errors.Is(err, os.ErrNotExist) {
return nil, err
}

return loadRegistryDir(overrideDir)
}

func loadRegistryDir(dir string) (*script.Registry, error) {
reg, err := script.NewRegistry(dir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return emptyRegistry(), nil
}
return nil, err
if cmd.Bool("local-only") {
return tap.DefaultRegistry(overrideDir, "")
}
return reg, nil
}

func emptyRegistry() *script.Registry {
return &script.Registry{}
return tap.DefaultRegistry(dir, overrideDir)
}

func printCompletion(cmd *cli.Command, value, description string) {
Expand Down
51 changes: 50 additions & 1 deletion cmd/tap/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,14 @@ func siteListCmd() *cli.Command {
if _, after, ok := strings.Cut(actionName, "/"); ok {
actionName = after
}
fmt.Printf(" %-24s %s%s\n",
runtimeBadge := ""
if s.Meta.Runtime != "" && s.Meta.Runtime != "auto" {
runtimeBadge = dim(color, " ["+s.Meta.Runtime+"]")
}
fmt.Printf(" %-24s %s%s%s\n",
green(color, actionName),
s.Meta.Description,
runtimeBadge,
argHints,
)
}
Expand Down Expand Up @@ -191,10 +196,54 @@ func siteInfoCmd() *cli.Command {

fmt.Printf(" %s %s\n", bold(color, "Domain:"), s.Meta.Domain)

if s.Meta.Runtime != "" && s.Meta.Runtime != "auto" {
fmt.Printf(" %s %s\n", bold(color, "Runtime:"), s.Meta.Runtime)
}

if s.Meta.Example != "" {
fmt.Printf(" %s %s\n", bold(color, "Example:"), s.Meta.Example)
}

if len(s.Meta.Env) > 0 {
fmt.Printf("\n %s\n", bold(color, "Env:"))
envNames := make([]string, 0, len(s.Meta.Env))
for name := range s.Meta.Env {
envNames = append(envNames, name)
}
sort.Strings(envNames)
for _, envName := range envNames {
def := s.Meta.Env[envName]
req := dim(color, "optional")
if def.Required {
req = yellow(color, "required")
}
fmt.Printf(" %-16s %s %s\n",
green(color, envName),
dim(color, "("+req+")"),
def.Description,
)
}
}

if len(s.Meta.Headers) > 0 {
fmt.Printf("\n %s\n", bold(color, "Headers:"))
headerKeys := make([]string, 0, len(s.Meta.Headers))
for k := range s.Meta.Headers {
headerKeys = append(headerKeys, k)
}
sort.Strings(headerKeys)
for _, k := range headerKeys {
v := s.Meta.Headers[k]
if strings.Contains(v, "${") {
v = dim(color, "(from env)")
}
fmt.Printf(" %-16s %s\n",
green(color, k),
v,
)
}
}

if len(s.Meta.Args) > 0 {
fmt.Printf("\n %s\n", bold(color, "Arguments:"))
// Sort args for consistent output
Expand Down
4 changes: 2 additions & 2 deletions engine/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewBrowser(tp *transport.Transport, pauseFn transport.PauseFunc) *Browser {
func (b *Browser) Name() string { return "Browser" }
func (b *Browser) Close() error { return nil }

func (b *Browser) Run(ctx context.Context, s *script.Script, args map[string]string) (any, error) {
func (b *Browser) Run(ctx context.Context, s *script.Script, args map[string]string, opts RunOpts) (any, error) {
argsJSON, err := json.Marshal(args)
if err != nil {
return nil, fmt.Errorf("marshal args: %w", err)
Expand All @@ -36,5 +36,5 @@ func (b *Browser) Run(ctx context.Context, s *script.Script, args map[string]str
navURL = "https://" + s.Meta.Domain
}

return b.transport.BrowseEvalWithPause(ctx, navURL, js, b.pauseFn)
return b.transport.BrowseEvalWithPause(ctx, navURL, js, b.pauseFn, opts.Headers)
}
11 changes: 8 additions & 3 deletions engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import (
"github.com/vaayne/tap/script"
)

// RunOpts holds per-run configuration for script execution.
type RunOpts struct {
Headers map[string]string // resolved meta headers (env vars interpolated)
}

// Engine can execute a site script with arguments and return a JSON-compatible result.
type Engine interface {
// Run executes a script with the given arguments.
Run(ctx context.Context, s *script.Script, args map[string]string) (any, error)
Run(ctx context.Context, s *script.Script, args map[string]string, opts RunOpts) (any, error)

// Name returns the engine name for logging.
Name() string
Expand All @@ -26,10 +31,10 @@ type Engine interface {
// If a result contains an "error" field (e.g. {"error":"HTTP 400"}), it is
// treated as a failure and the next engine is tried.
// If all engines fail, returns the last error.
func RunScript(ctx context.Context, engines []Engine, s *script.Script, args map[string]string) (any, error) {
func RunScript(ctx context.Context, engines []Engine, s *script.Script, args map[string]string, opts RunOpts) (any, error) {
var lastErr error
for _, e := range engines {
result, err := e.Run(ctx, s, args)
result, err := e.Run(ctx, s, args, opts)
if err != nil {
lastErr = err
log.Printf("%s failed: %v", e.Name(), err)
Expand Down
8 changes: 4 additions & 4 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type mockEngine struct {

func (m *mockEngine) Name() string { return m.name }
func (m *mockEngine) Close() error { return nil }
func (m *mockEngine) Run(_ context.Context, _ *script.Script, _ map[string]string) (any, error) {
func (m *mockEngine) Run(_ context.Context, _ *script.Script, _ map[string]string, _ RunOpts) (any, error) {
return m.result, m.err
}

Expand All @@ -27,7 +27,7 @@ func TestRunScript_FirstEngineSucceeds(t *testing.T) {
&mockEngine{name: "slow", result: map[string]any{"ok": true}},
}

result, err := RunScript(context.Background(), engines, &script.Script{}, nil)
result, err := RunScript(context.Background(), engines, &script.Script{}, nil, RunOpts{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -43,7 +43,7 @@ func TestRunScript_FallbackToSecond(t *testing.T) {
&mockEngine{name: "slow", result: map[string]any{"fallback": true}},
}

result, err := RunScript(context.Background(), engines, &script.Script{}, nil)
result, err := RunScript(context.Background(), engines, &script.Script{}, nil, RunOpts{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -59,7 +59,7 @@ func TestRunScript_AllFail(t *testing.T) {
&mockEngine{name: "e2", err: fmt.Errorf("fail2")},
}

_, err := RunScript(context.Background(), engines, &script.Script{}, nil)
_, err := RunScript(context.Background(), engines, &script.Script{}, nil, RunOpts{})
if err == nil {
t.Fatal("expected error, got nil")
}
Expand Down
10 changes: 7 additions & 3 deletions engine/quickjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func NewQuickJS(tp *transport.Transport) *QuickJS {
func (q *QuickJS) Name() string { return "QuickJS" }
func (q *QuickJS) Close() error { return nil }

func (q *QuickJS) Run(_ context.Context, s *script.Script, args map[string]string) (result any, err error) {
func (q *QuickJS) Run(_ context.Context, s *script.Script, args map[string]string, opts RunOpts) (result any, err error) {
// The QJS WASM runtime can panic on certain async patterns (e.g. out of
// bounds memory access). Recover so the engine fallback chain continues.
defer func() {
Expand All @@ -44,7 +44,7 @@ func (q *QuickJS) Run(_ context.Context, s *script.Script, args map[string]strin
defer rt.Close()
ctx := rt.Context()

injectFetch(ctx, q.transport)
injectFetch(ctx, q.transport, opts.Headers)

argsJSON, qErr := json.Marshal(args)
if qErr != nil {
Expand Down Expand Up @@ -83,7 +83,8 @@ func stringify(ctx *qjs.Context, val *qjs.Value) string {
}

// injectFetch adds a fetch() function backed by the shared transport's HTTP client.
func injectFetch(ctx *qjs.Context, tp *transport.Transport) {
// metaHeaders are set first; JS-level headers override them.
func injectFetch(ctx *qjs.Context, tp *transport.Transport, metaHeaders map[string]string) {
ctx.SetAsyncFunc("fetch", func(this *qjs.This) {
c := this.Context()

Expand Down Expand Up @@ -127,6 +128,9 @@ func injectFetch(ctx *qjs.Context, tp *transport.Transport) {

req.Header.Set("User-Agent", transport.UserAgent)
req.Header.Set("Accept", "application/json, text/plain, */*")
for k, v := range metaHeaders {
req.Header.Set(k, v)
}
for k, v := range headers {
req.Header.Set(k, v)
}
Expand Down
Loading
Loading