Add `homelab browser run|open` so agents can drive the cluster's headful Chrome (chrome-service) over CDP from the devvm. The headless playwright/mcp browser can load anti-bot sites and fill their forms, but the gated submit silently fails — e.g. the Stirling Ackroyd Fixflo tenant portal returned net::ERR_FILE_NOT_FOUND on its pre-submit check and hung, creating nothing. Driving the real headful Chrome submits first try. That capability already existed but was undiscoverable, so it cost ~40 min + redundant form re-runs to find; now it is one command, versioned, test-covered, and `browser --help` carries the when-to-use signature + an error-code cheat-sheet so the right tool is reached at the right moment (the failure was judgment, not setup). - port-forward svc/chrome-service:9222 (tunnels API-server->pod, so it bypasses the :9222 NetworkPolicy), assert non-headless via /json/version, connect_over_cdp, inject the same vendored stealth.js the in-cluster callers use; the port-forward is always torn down, on success and on error. - node CDP client pinned to playwright-core@1.48.2 to match the v1.48.0-noble image (Chromium 130); self-provisioned lazily into ~/.cache/homelab, no per-user setup. - default is a fresh incognito context (safe for the shared browser + concurrent callers); --shared-context reuses the warmed persistent profile. - TDD: cmd_browser_test.go covers arg parsing, headless detection, the version pin, the help cheat-sheet, and a stealth.js drift guard. Verified end-to-end against bot.sannysoft.com (real Chrome UA, webdriver hidden, plugins/WebGL spoofed) and `browser open`. - docs: README v0.8 section, ADR-0013, and a chrome-service.md "driving from outside the cluster" section. Closes: code-nepg Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
2.5 KiB
Go
97 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// version is stamped at build time via -ldflags "-X main.version=vX.Y.Z".
|
|
var version = "dev"
|
|
|
|
// buildRegistry returns every homelab verb. New verb-groups append here.
|
|
func buildRegistry() []Command {
|
|
var reg []Command
|
|
reg = append(reg, claimCommands()...)
|
|
reg = append(reg, tfCommands()...)
|
|
reg = append(reg, workCommands()...)
|
|
reg = append(reg, k8sCommands()...)
|
|
reg = append(reg, memoryCommands()...)
|
|
reg = append(reg, ciCommands()...)
|
|
reg = append(reg, deployCommands()...)
|
|
reg = append(reg, netCommands()...)
|
|
reg = append(reg, obsCommands()...)
|
|
reg = append(reg, usageCommands()...)
|
|
reg = append(reg, haCommands()...)
|
|
reg = append(reg, browserCommands()...)
|
|
return reg
|
|
}
|
|
|
|
// dispatchTop handles the homelab verb surface. handled=false means the args are
|
|
// not a homelab verb, so main() falls back to the legacy -use-case path.
|
|
func dispatchTop(args []string) (handled bool, err error) {
|
|
if len(args) == 0 {
|
|
fmt.Print(usage())
|
|
return true, nil
|
|
}
|
|
switch args[0] {
|
|
case "help", "-h", "--help":
|
|
fmt.Print(usage())
|
|
return true, nil
|
|
case "version", "--version":
|
|
fmt.Println("homelab " + version)
|
|
return true, nil
|
|
case "manifest":
|
|
reg := buildRegistry()
|
|
if containsArg(args[1:], "--json") {
|
|
out, err := manifestJSON(reg)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
fmt.Println(out)
|
|
return true, nil
|
|
}
|
|
fmt.Print(manifestText(reg))
|
|
return true, nil
|
|
}
|
|
if strings.HasPrefix(args[0], "-") {
|
|
return false, nil
|
|
}
|
|
reg := buildRegistry()
|
|
if !isCommandGroup(reg, args[0]) {
|
|
return false, nil
|
|
}
|
|
return true, dispatch(reg, args)
|
|
}
|
|
|
|
func isCommandGroup(reg []Command, group string) bool {
|
|
for _, c := range reg {
|
|
if len(c.Path) > 0 && c.Path[0] == group {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func containsArg(args []string, want string) bool {
|
|
for _, a := range args {
|
|
if a == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func usage() string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "homelab %s — unified homelab operations CLI\n\n", version)
|
|
b.WriteString("Usage:\n homelab <command> [args]\n\nCommands:\n")
|
|
for _, line := range strings.Split(strings.TrimRight(manifestText(buildRegistry()), "\n"), "\n") {
|
|
if line != "" {
|
|
b.WriteString(" " + line + "\n")
|
|
}
|
|
}
|
|
b.WriteString("\n manifest [--json] list all commands (machine-readable with --json)\n")
|
|
b.WriteString(" version print version\n")
|
|
b.WriteString("\nLegacy webhook use-cases remain available via -use-case=<name>.\n")
|
|
return b.String()
|
|
}
|