diff --git a/.booth/tools/coding-booth.lock b/.booth/tools/coding-booth.lock deleted file mode 100644 index 9b2e7db8..00000000 --- a/.booth/tools/coding-booth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.14.0 -downloaded_at=2026-02-01T20:03:18Z -cache=shared diff --git a/.github/workflows/release-binary-and-wrapper.yaml b/.github/workflows/release-binary-and-wrapper.yaml index d3983d80..0cb2807d 100644 --- a/.github/workflows/release-binary-and-wrapper.yaml +++ b/.github/workflows/release-binary-and-wrapper.yaml @@ -80,17 +80,23 @@ jobs: - name: Generate SHA256 checksums for all binaries run: | cd bin + echo "BIN" ls -la for binary in codingbooth-*; do sha256sum "$binary" > "${binary}.sha256" done cd .. + echo "ROOT" + ls -la # --------------------------------------------------------- # Build example artifacts # --------------------------------------------------------- - name: Update booth wrappers - run: ./examples/update-booth.sh + run: | + cd ./examples + ./update-booth.sh + cd .. - name: Create example-list.txt run: | diff --git a/.gitignore b/.gitignore index 6d6ab731..09e27f79 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,8 @@ tests/basic/test*.sh.log tests/basic/test--dockerfile tests/dryrun/test--.env tests/dryrun/test--config.sh +tests/dryrun/test--config.toml +**/.Dockerfile.generated playground/ @@ -121,3 +123,5 @@ variants/base/_stage/ !/build/ /codingbooth +**/booth +!/booth diff --git a/README.md b/README.md index 073786c6..64f503f8 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ CodingBooth provides a command-line interface with the following structure: | `--pull` | Force pull latest image | | `--dind` | Enable Docker-in-Docker mode | | `--keep-alive` | Keep container after exit | +| `--writable-booth` | Allow writing to `.booth/` inside the container (read-only by default) | | `--silence-build` | Suppress build/startup output | | `--dryrun` | Print docker commands without executing | | `--verbose` | Enable debug output | @@ -415,6 +416,8 @@ my-project/ > 💡 **Tip:** When both `Boothfile` and `Dockerfile` exist, Boothfile takes precedence. Use `--dockerfile` to force using the Dockerfile. +> 🔒 **Read-only by default:** The `.booth/` folder is mounted **read-only** inside the container to prevent accidental or malicious modifications to your configuration. Use `--writable-booth` if you need to edit `.booth/` files from inside the container (e.g., during development). + > ⚠️ **Note on `cmds`:** When you pass commands via CLI (`-- `), they **override** the `cmds` in config.toml (they don't append). --- @@ -453,17 +456,17 @@ env APP_ENV=production ### Boothfile Commands -| Command | Example | Compiles to | -|---------|---------|-------------| -| `run` | `run apt-get update` | `RUN apt-get update` | -| `setup` | `setup python 3.12` | `RUN python--setup.sh 3.12` | -| `install` | `install pip django` | `RUN pip--install.sh django` | -| `copy` | `copy ./config /opt` | `COPY ./config /opt` | -| `env` | `env DEBUG=true` | `ENV DEBUG=true` | -| `arg` | `arg VERSION=1.0` | `ARG VERSION=1.0` | -| `workdir` | `workdir /app` | `WORKDIR /app` | -| `expose` | `expose 8080` | `EXPOSE 8080` | -| `label` | `label maintainer="me"` | `LABEL maintainer="me"` | +| Command | Example | Compiles to | +|-----------|-------------------------|------------------------------| +| `run` | `run apt-get update` | `RUN apt-get update` | +| `setup` | `setup python 3.12` | `RUN python--setup.sh 3.12` | +| `install` | `install pip django` | `RUN pip--install.sh django` | +| `copy` | `copy ./config /opt` | `COPY ./config /opt` | +| `env` | `env DEBUG=true` | `ENV DEBUG=true` | +| `arg` | `arg VERSION=1.0` | `ARG VERSION=1.0` | +| `workdir` | `workdir /app` | `WORKDIR /app` | +| `expose` | `expose 8080` | `EXPOSE 8080` | +| `label` | `label maintainer="me"` | `LABEL maintainer="me"` | ### Multi-line Commands (Heredocs) @@ -525,11 +528,11 @@ The compiler automatically adds a `COPY` to bring your script into the image. ### CLI Flags -| Flag | Description | -|------|-------------| -| `--boothfile ` | Use a specific Boothfile | -| `--emit-dockerfile` | Print generated Dockerfile without building | -| `--strict` | Treat warnings as errors | +| Flag | Description | +|----------------------|---------------------------------------------| +| `--boothfile ` | Use a specific Boothfile | +| `--emit-dockerfile` | Print generated Dockerfile without building | +| `--strict` | Treat warnings as errors | ### File Precedence @@ -575,13 +578,14 @@ env DJANGO_SETTINGS_MODULE=myproject.settings CodingBooth is designed for development environments, not production workloads. Key security aspects: -| Aspect | Behavior | -|---------------------|------------------------------------------------------------------------| -| **User privileges** | Processes run as unprivileged `coder` user, not root | -| **Sudo access** | `coder` has passwordless sudo (for installing packages) | -| **File ownership** | Files match your host UID/GID — no root-owned files | -| **Network** | Full network access by default; use Network Whitelist for restrictions | -| **DinD mode** | Requires `--privileged` flag (elevated permissions) | +| Aspect | Behavior | +|----------------------|---------------------------------------------------------------------------| +| **User privileges** | Processes run as unprivileged `coder` user, not root | +| **Sudo access** | `coder` has passwordless sudo (for installing packages) | +| **File ownership** | Files match your host UID/GID — no root-owned files | +| **`.booth/` config** | Read-only inside the container by default (`--writable-booth` to opt out) | +| **Network** | Full network access by default; use Network Whitelist for restrictions | +| **DinD mode** | Requires `--privileged` flag (elevated permissions) | **Best practices:** - Don't run untrusted code in CodingBooth containers @@ -619,41 +623,41 @@ The `booth` wrapper script is **location-based**: it operates relative to its ow ``` -host # your machine - ├── ~/.cache/codingbooth/ # shared binary cache +host # your machine + ├── ~/.cache/codingbooth/ # shared binary cache | └── versions/ - | └── 0.13.0/ # version-specific binaries + | └── 0.13.0/ # version-specific binaries | ├── codingbooth.sha256 - | └── codingbooth-* # platform binaries - ├── project/ # your project folder on the host - | ├── booth # booth wrapper script - | ├── .booth # booth internal folder + | └── codingbooth-* # platform binaries + ├── project/ # your project folder on the host + | ├── booth # booth wrapper script + | ├── .booth # booth internal folder | | └── tools/ - | | └── codingbooth.lock # version reference - | ├── ... # other project files + | | └── codingbooth.lock # version reference + | ├── ... # other project files ... container ├── home/ | ├── coder/ - | | ├── code/ # your project folder inside the container - | | | ├── booth # booth wrapper script - | | | ├── .booth # booth internal folder + | | ├── code/ # your project folder inside the container + | | | ├── booth # booth wrapper script + | | | ├── .booth # booth internal folder | | | | └── tools/ | | | | └── codingbooth.lock # version reference - | | ├── ... # other project files - | ├── ... # other home files + | | ├── ... # other project files + | ├── ... # other home files ├── etc/ - | ├── profile.d/ # profile script folder + | ├── profile.d/ # profile script folder ├── opt/ | ├── codingbooth/ - | | ├── setups/ # setup script folder - | | | ├── ... # setup scripts + | | ├── setups/ # setup script folder + | | | ├── ... # setup scripts ├── usr/ | ├── local/ - | | ├── bin/ # program file folder + | | ├── bin/ # program file folder | ├── share/ - | | ├── startup.d/ # startup script folder + | | ├── startup.d/ # startup script folder ... ``` @@ -1204,10 +1208,55 @@ This allows the booth to run Docker commands that execute inside the isolated Di - The sidecar approach offers stronger isolation but can be slower and more complex to manage. > 💡 **Tip:** -> See `examples/dind-example` for basic DinD usage, or `examples/kind-example` for running Kubernetes with KinD inside the booth. +> See `examples/workspaces/dind-example` for basic DinD usage, `examples/workspaces/kind-example` for KinD, and `examples/workspaces/firewall-example` for `--sandboxed` egress enforcement. +> **Security note (2026-02-06):** `--sandboxed` with `--dind` is **not supported**. The DinD sidecar can bypass the egress firewall by running a privileged container in the shared network namespace. Use `--sandboxed` **without** `--dind` until further research. + + +### 12. Egress Sandbox (`--sandboxed`) + +CodingBooth includes an **egress sandbox** that restricts outbound traffic to an allowlist. +When enabled, traffic must pass through an Envoy proxy sidecar and is enforced with firewall rules. + +**How It Works** +- Envoy forward proxy enforces a domain allowlist. +- iptables rules force all HTTP/HTTPS traffic through the proxy. +- Default policy is **deny**; only allowlisted domains are reachable. + +**Configuration** +- Enable with: + ```bash + ./booth --sandboxed + ``` +- Policy is provided by **one** of: + - `.booth/sandbox/allowlist.txt` (simple allowlist), or + - `.booth/sandbox/envoy.yaml` (advanced/custom). +- These are **mutually exclusive**; if both exist, startup fails with a clear error. + - Optional: add extra domains with `sandbox-allowlist` in `.booth/config.toml`: + ```toml + sandbox-allowlist = [ + "example.com", + "registry.npmjs.org" + ] + ``` + This list is merged into the active allowlist (default or file-based). It cannot be used with `sandbox-policy-file`. + +**Default Allowlist (embedded)** +- If `--sandboxed` is enabled and no policy files exist, CodingBooth materializes a default allowlist at: + - `.booth/sandbox/allowlist.txt` +- This default content is embedded in the CLI binary and is based on `docs/implementations/example-allowlist.txt`. +**Important Security Note (2026-02-06)** +- `--sandboxed` with `--dind` is **not supported** due to a known firewall bypass via privileged DinD containers. +- Use `--sandboxed` **without** `--dind` until further research. + +**Quick Example** +```bash +mkdir -p .booth/sandbox +cp docs/implementations/example-allowlist.txt .booth/sandbox/allowlist.txt +./booth --sandboxed +``` -### 12. Network Whitelist +### 13. Network Whitelist CodingBooth includes a **network whitelist** feature that restricts container internet access to only approved domains. This is useful for: - Security-conscious environments @@ -1520,7 +1569,3 @@ Stay in touch or follow updates, insights, and development notes: > 🙏 Every issue, idea, and pull request — big or small — helps make CodingBooth better for everyone. > Thank you for being part of the community! - - - - diff --git a/cli/src/cmd/codingbooth/help.go b/cli/src/cmd/codingbooth/help.go index 7400c6a8..cb7f5b8d 100644 --- a/cli/src/cmd/codingbooth/help.go +++ b/cli/src/cmd/codingbooth/help.go @@ -31,6 +31,7 @@ USAGE: %s prune [--yes] (remove stopped booth containers) %s example (manage examples) %s emit-dockerfile [options] (compile Boothfile to Dockerfile) + %s print-default-allowlist.txt (print built-in egress allowlist) BOOTSTRAP OPTIONS (CLI or defaults; evaluated before environmental variable and config file): --code Host code path to mount at /home/coder/code @@ -81,7 +82,9 @@ RUNTIME OPTIONS: CONTAINER MODE: --daemon Run the booth container in the background --dind Enable a Docker-in-Docker sidecar and set DOCKER_HOST + --sandboxed Enable egress sandbox defaults (proxy + enforcement setup) --keep-alive Do not remove the container when stopped + --writable-booth Allow writing to .booth/ inside the container (read-only by default) COMMANDS: All arguments after '--' are executed *inside* the container instead of starting @@ -102,6 +105,8 @@ NOTES: - With --dind, a docker:dind sidecar runs on a private network and the main container uses DOCKER_HOST=tcp://:2375. + - With --sandboxed, booth enables egress policy defaults. If --dind is also set, + the existing DinD sidecar network namespace is reused. EXAMPLES: # Prebuilt, foreground @@ -139,5 +144,6 @@ EXAMPLES: scriptName, scriptName, scriptName, + scriptName, ) } diff --git a/cli/src/cmd/codingbooth/main.go b/cli/src/cmd/codingbooth/main.go index 44370f65..a90cbc05 100644 --- a/cli/src/cmd/codingbooth/main.go +++ b/cli/src/cmd/codingbooth/main.go @@ -52,6 +52,9 @@ func main() { case "emit-dockerfile": emitDockerfile() return + case "print-default-allowlist.txt": + printDefaultAllowlist() + return default: // If it starts with --, treat as run with options if len(command) > 0 && command[0] == '-' { diff --git a/cli/src/cmd/codingbooth/print_default_allowlist.go b/cli/src/cmd/codingbooth/print_default_allowlist.go new file mode 100644 index 00000000..ebcccd4e --- /dev/null +++ b/cli/src/cmd/codingbooth/print_default_allowlist.go @@ -0,0 +1,15 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package main + +import ( + "fmt" + + "github.com/nawaman/codingbooth/src/pkg/defaults" +) + +func printDefaultAllowlist() { + fmt.Print(defaults.ExampleAllowlist) +} diff --git a/cli/src/pkg/appctx/app_config.go b/cli/src/pkg/appctx/app_config.go index b28bdd53..7f6994ab 100644 --- a/cli/src/pkg/appctx/app_config.go +++ b/cli/src/pkg/appctx/app_config.go @@ -33,6 +33,11 @@ type AppConfig struct { Daemon bool `toml:"daemon,omitempty" envconfig:"CB_DAEMON" default:"false"` Pull bool `toml:"pull,omitempty" envconfig:"CB_PULL" default:"false"` Dind bool `toml:"dind,omitempty" envconfig:"CB_DIND" default:"false"` + Sandbox bool `toml:"sandboxed,omitempty" envconfig:"CB_SANDBOX" default:"false"` + WritableBooth bool `toml:"writable-booth,omitempty" envconfig:"CB_WRITABLE_BOOTH" default:"false"` + SandboxAllowlistFile string `toml:"sandbox-allowlist-file,omitempty" envconfig:"CB_SANDBOX_ALLOWLIST_FILE"` + SandboxPolicyFile string `toml:"sandbox-policy-file,omitempty" envconfig:"CB_SANDBOX_POLICY_FILE"` + SandboxAllowlist []string `toml:"sandbox-allowlist,omitempty" envconfig:"CB_SANDBOX_ALLOWLIST"` // -------------------- // Image configuration @@ -67,6 +72,11 @@ type AppConfig struct { BuildArgs ilist.SemicolonStringList `toml:"build-args,omitempty" envconfig:"CB_BUILD_ARGS"` RunArgs ilist.SemicolonStringList `toml:"run-args,omitempty" envconfig:"CB_RUN_ARGS"` Cmds ilist.SemicolonStringList `toml:"cmds,omitempty" envconfig:"CB_CMDS"` + + // -------------------- + // Nested TOML configuration + // -------------------- + Egress EgressConfig `toml:"egress,omitempty" ignored:"true"` } // Clone the content of the app config. @@ -113,6 +123,11 @@ func (config AppConfig) String() string { fmt.Fprintf(&str, " Daemon: %t\n", config.Daemon) fmt.Fprintf(&str, " Pull: %t\n", config.Pull) fmt.Fprintf(&str, " Dind: %t\n", config.Dind) + fmt.Fprintf(&str, " Sandbox: %t\n", config.Sandbox) + fmt.Fprintf(&str, " WritableBooth: %t\n", config.WritableBooth) + fmt.Fprintf(&str, " SandboxAllowlist: %q\n", config.SandboxAllowlistFile) + fmt.Fprintf(&str, " SandboxPolicy: %q\n", config.SandboxPolicyFile) + fmt.Fprintf(&str, " SandboxAllowlist+: %v\n", config.SandboxAllowlist) fmt.Fprintf(&str, "# Image Configuration -----------\n") fmt.Fprintf(&str, " Dockerfile: %q\n", config.Dockerfile) @@ -140,6 +155,13 @@ func (config AppConfig) String() string { formatList(&str, "RunArgs", config.RunArgs.List, " ") formatList(&str, "Cmds", config.Cmds.List, " ") + fmt.Fprintf(&str, "# Egress Configuration ----------\n") + fmt.Fprintf(&str, " Egress.Mode: %q\n", config.Egress.Mode) + fmt.Fprintf(&str, " Egress.Enforcement:%q\n", config.Egress.Enforcement) + fmt.Fprintf(&str, " Egress.Default: %q\n", config.Egress.Default) + fmt.Fprintf(&str, " Egress.Allowlist: %q\n", config.Egress.AllowlistFile) + fmt.Fprintf(&str, " Egress.Policy: %q\n", config.Egress.PolicyFile) + str.WriteString("==================================================================\n") return str.String() diff --git a/cli/src/pkg/appctx/app_context.go b/cli/src/pkg/appctx/app_context.go index a57600b4..3cdd8558 100644 --- a/cli/src/pkg/appctx/app_context.go +++ b/cli/src/pkg/appctx/app_context.go @@ -68,6 +68,9 @@ func (ctx AppContext) LibDir() string { return ctx.values.LibDir } // derived from DinD func (ctx AppContext) CreatedDindNet() bool { return ctx.values.CreatedDindNet } +func (ctx AppContext) CreatedSandboxNet() bool { + return ctx.values.CreatedSandboxNet +} // derived from image determination of image func (ctx AppContext) RunMode() string { return ctx.values.RunMode } @@ -84,6 +87,13 @@ func (ctx AppContext) SilenceBuild() bool { return ctx.values.Config.SilenceBuil func (ctx AppContext) Daemon() bool { return ctx.values.Config.Daemon } func (ctx AppContext) Pull() bool { return ctx.values.Config.Pull } func (ctx AppContext) Dind() bool { return ctx.values.Config.Dind } +func (ctx AppContext) Sandbox() bool { return ctx.values.Config.Sandbox } +func (ctx AppContext) WritableBooth() bool { return ctx.values.Config.WritableBooth } +func (ctx AppContext) SandboxAllowlistFile() string { + return ctx.values.Config.SandboxAllowlistFile +} +func (ctx AppContext) SandboxPolicyFile() string { return ctx.values.Config.SandboxPolicyFile } +func (ctx AppContext) SandboxAllowlist() []string { return ctx.values.Config.SandboxAllowlist } // Image Configuration func (ctx AppContext) Dockerfile() string { return ctx.values.Config.Dockerfile } @@ -105,6 +115,13 @@ func (ctx AppContext) Port() string { return ctx.values.Config.Port } func (ctx AppContext) EnvFile() string { return ctx.values.Config.EnvFile } func (ctx AppContext) Startup() string { return ctx.values.Config.Startup } +// Egress Configuration +func (ctx AppContext) EgressMode() string { return ctx.values.Config.Egress.Mode } +func (ctx AppContext) EgressEnforcement() string { return ctx.values.Config.Egress.Enforcement } +func (ctx AppContext) EgressDefault() string { return ctx.values.Config.Egress.Default } +func (ctx AppContext) EgressAllowlistFile() string { return ctx.values.Config.Egress.AllowlistFile } +func (ctx AppContext) EgressPolicyFile() string { return ctx.values.Config.Egress.PolicyFile } + // derived from all the context processing (IMMUTABLE SNAPSHOTS) func (ctx AppContext) CommonArgs() ilist.List[ilist.List[string]] { return ctx.commonArgs } func (ctx AppContext) BuildArgs() ilist.List[ilist.List[string]] { return ctx.buildArgs } @@ -141,6 +158,7 @@ func (ctx AppContext) String() string { fmt.Fprintf(&str, "# DinD --------------------------\n") fmt.Fprintf(&str, " CreatedDindNet: %t\n", ctx.CreatedDindNet()) + fmt.Fprintf(&str, " CreatedSandboxNet:%t\n", ctx.CreatedSandboxNet()) fmt.Fprintf(&str, "# Image -------------------------\n") fmt.Fprintf(&str, " RunMode: %q\n", ctx.RunMode()) @@ -164,6 +182,8 @@ func (ctx AppContext) String() string { fmt.Fprintf(&str, " Daemon: %t\n", ctx.Daemon()) fmt.Fprintf(&str, " Pull: %t\n", ctx.Pull()) fmt.Fprintf(&str, " Dind: %t\n", ctx.Dind()) + fmt.Fprintf(&str, " Sandbox: %t\n", ctx.Sandbox()) + fmt.Fprintf(&str, " WritableBooth: %t\n", ctx.WritableBooth()) fmt.Fprintf(&str, "# Image Configuration -----------\n") fmt.Fprintf(&str, " Dockerfile: %q\n", ctx.Dockerfile()) @@ -182,6 +202,16 @@ func (ctx AppContext) String() string { fmt.Fprintf(&str, " EnvFile: %q\n", ctx.EnvFile()) fmt.Fprintf(&str, " Startup: %q\n", ctx.Startup()) + fmt.Fprintf(&str, "# Egress Configuration ----------\n") + fmt.Fprintf(&str, " EgressMode: %q\n", ctx.EgressMode()) + fmt.Fprintf(&str, " EgressEnforcement:%q\n", ctx.EgressEnforcement()) + fmt.Fprintf(&str, " EgressDefault: %q\n", ctx.EgressDefault()) + fmt.Fprintf(&str, " EgressAllowlist: %q\n", ctx.EgressAllowlistFile()) + fmt.Fprintf(&str, " EgressPolicy: %q\n", ctx.EgressPolicyFile()) + fmt.Fprintf(&str, " SandboxAllowlist: %q\n", ctx.SandboxAllowlistFile()) + fmt.Fprintf(&str, " SandboxPolicy: %q\n", ctx.SandboxPolicyFile()) + fmt.Fprintf(&str, " SandboxAllowlist+: %v\n", ctx.SandboxAllowlist()) + fmt.Fprintf(&str, "# Lists (Immutable) -------------\n") formatList(&str, "CommonArgs", ctx.CommonArgs(), " ") formatList(&str, "BuildArgs", ctx.BuildArgs(), " ") diff --git a/cli/src/pkg/appctx/app_context_builder.go b/cli/src/pkg/appctx/app_context_builder.go index 7da9a340..ab3d2c9b 100644 --- a/cli/src/pkg/appctx/app_context_builder.go +++ b/cli/src/pkg/appctx/app_context_builder.go @@ -28,7 +28,8 @@ type AppContextBuilder struct { HasDesktop bool // derived from DinD - CreatedDindNet bool + CreatedDindNet bool + CreatedSandboxNet bool // derived from image determination of image RunMode string diff --git a/cli/src/pkg/appctx/config_integration_test.go b/cli/src/pkg/appctx/config_integration_test.go index aa68b198..5e3e7442 100644 --- a/cli/src/pkg/appctx/config_integration_test.go +++ b/cli/src/pkg/appctx/config_integration_test.go @@ -44,6 +44,8 @@ func TestIntegration_ReadFromToml(t *testing.T) { verbose = true project-name = "toml-test-project" host-uid = "1002" + +sandbox-allowlist-file = ".booth/sandbox/allowlist.txt" ` tmpfile, err := os.CreateTemp("", "config-*.toml") assert.NoError(t, err) @@ -64,4 +66,5 @@ host-uid = "1002" assert.True(t, config.Verbose.ValueOr(false)) assert.Equal(t, "toml-test-project", config.ProjectName) assert.Equal(t, "1002", config.HostUID) + assert.Equal(t, ".booth/sandbox/allowlist.txt", config.SandboxAllowlistFile) } diff --git a/cli/src/pkg/appctx/egress_config.go b/cli/src/pkg/appctx/egress_config.go new file mode 100644 index 00000000..6b507639 --- /dev/null +++ b/cli/src/pkg/appctx/egress_config.go @@ -0,0 +1,16 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package appctx + +// EgressConfig controls outbound network policy orchestration. +// +// It is intentionally TOML-only for now; runtime wiring comes in later phases. +type EgressConfig struct { + Mode string `toml:"mode,omitempty"` + Enforcement string `toml:"enforcement,omitempty"` + Default string `toml:"default,omitempty"` + AllowlistFile string `toml:"allowlist-file,omitempty"` + PolicyFile string `toml:"policy-file,omitempty"` +} diff --git a/cli/src/pkg/booth/booth.go b/cli/src/pkg/booth/booth.go index ef8ca484..044bceb3 100644 --- a/cli/src/pkg/booth/booth.go +++ b/cli/src/pkg/booth/booth.go @@ -84,14 +84,17 @@ func (booth *Booth) runAsCommand() error { // Execute the docker run command err := docker.Docker(flags, "run", args) - // Cleanup DinD resources if enabled + // Cleanup sandbox/DinD sidecars after command exits. + cleanupFlags := flags + cleanupFlags.Silent = true + cleanupFlags.Verbose = false + cleanupSandboxResources(booth.ctx, &cleanupFlags) if booth.ctx.Dind() { - flags.Silent = true dindName := getDindName(booth.ctx) dindNet := getDindNet(booth.ctx) - _ = docker.Docker(flags, "stop", ilist.NewList(ilist.NewList(dindName))) + _ = docker.Docker(cleanupFlags, "stop", ilist.NewList(ilist.NewList(dindName))) if booth.ctx.CreatedDindNet() { - _ = docker.Docker(flags, "network", ilist.NewList(ilist.NewList("rm", dindNet))) + _ = docker.Docker(cleanupFlags, "network", ilist.NewList(ilist.NewList("rm", dindNet))) } } @@ -163,6 +166,15 @@ func (booth *Booth) runAsDaemon() error { err := docker.Docker(flags, "run", args) // If DinD is enabled in daemon mode, inform user how to stop it + if booth.ctx.Sandbox() { + fmt.Printf("🛡️ Sandbox sidecar running: %s\n", getSandboxProxyName(booth.ctx)) + if booth.ctx.Dind() { + fmt.Printf(" Reusing DinD netns owner: %s\n", getDindName(booth.ctx)) + } else { + fmt.Printf(" Netns owner sidecar: %s (network: %s)\n", getSandboxNetnsName(booth.ctx), getSandboxNet(booth.ctx)) + } + } + if booth.ctx.Dind() { dindName := getDindName(booth.ctx) dindNet := getDindNet(booth.ctx) @@ -212,14 +224,17 @@ func (booth *Booth) runAsForeground() error { // Execute the docker run command err := docker.Docker(flags, "run", args) - // Cleanup DinD resources if enabled + // Cleanup sandbox/DinD sidecars after foreground exits. + cleanupFlags := flags + cleanupFlags.Silent = true + cleanupFlags.Verbose = false + cleanupSandboxResources(booth.ctx, &cleanupFlags) if booth.ctx.Dind() { dindName := getDindName(booth.ctx) dindNet := getDindNet(booth.ctx) - flags.Silent = true - _ = docker.Docker(flags, "stop", ilist.NewList(ilist.NewList(dindName))) + _ = docker.Docker(cleanupFlags, "stop", ilist.NewList(ilist.NewList(dindName))) if booth.ctx.CreatedDindNet() { - _ = docker.Docker(flags, "network", ilist.NewList(ilist.NewList("rm", dindNet))) + _ = docker.Docker(cleanupFlags, "network", ilist.NewList(ilist.NewList("rm", dindNet))) } } @@ -265,6 +280,10 @@ func PrepareCommonArgs(ctx appctx.AppContext) appctx.AppContext { builder.CommonArgs.Append(ilist.NewList[string]("-v", codePath+":/home/coder/code")) builder.CommonArgs.Append(ilist.NewList[string]("-w", "/home/coder/code")) + if !ctx.WritableBooth() { + addReadOnlyBoothDir(builder, codePath) + } + // Lifecycle management labels used by list/start/stop/restart/remove commands. builder.CommonArgs.Append(ilist.NewList[string]("--label", "cb.managed=true")) builder.CommonArgs.Append(ilist.NewList[string]("--label", "cb.project="+ctx.ProjectName())) @@ -274,8 +293,8 @@ func PrepareCommonArgs(ctx appctx.AppContext) appctx.AppContext { builder.CommonArgs.Append(ilist.NewList[string]("--label", "cb.version="+ctx.CbVersion())) builder.CommonArgs.Append(ilist.NewList[string]("--label", fmt.Sprintf("cb.keep-alive=%t", ctx.KeepAlive()))) - // Skip port mapping when using DinD (port is exposed on DinD container instead) - if !ctx.Dind() { + // Skip port mapping when using shared network namespace sidecars. + if !ctx.Dind() && !ctx.Sandbox() { builder.CommonArgs.Append(ilist.NewList[string]("-p", fmt.Sprintf("%d:10000", ctx.PortNumber()))) } @@ -302,6 +321,7 @@ func PrepareCommonArgs(ctx appctx.AppContext) appctx.AppContext { builder.CommonArgs.Append(ilist.NewList[string]("-e", fmt.Sprintf("BOOTH_SILENCE_BUILD=%t", ctx.SilenceBuild()))) builder.CommonArgs.Append(ilist.NewList[string]("-e", fmt.Sprintf("BOOTH_PULL=%t", ctx.Pull()))) builder.CommonArgs.Append(ilist.NewList[string]("-e", fmt.Sprintf("BOOTH_DIND=%t", ctx.Dind()))) + builder.CommonArgs.Append(ilist.NewList[string]("-e", fmt.Sprintf("BOOTH_SANDBOX=%t", ctx.Sandbox()))) builder.CommonArgs.Append(ilist.NewList[string]("-e", "BOOTH_DOCKERFILE="+ctx.Dockerfile())) builder.CommonArgs.Append(ilist.NewList[string]("-e", "BOOTH_PROJECT_NAME="+ctx.ProjectName())) builder.CommonArgs.Append(ilist.NewList[string]("-e", "BOOTH_TIMEZONE="+ctx.Timezone())) @@ -333,6 +353,18 @@ func normalizeCodePath(path string) string { return absPath } +func addReadOnlyBoothDir(builder *appctx.AppContextBuilder, codePath string) { + if codePath == "" { + return + } + hostPath := filepath.Join(codePath, ".booth") + info, err := os.Stat(hostPath) + if err != nil || !info.IsDir() { + return + } + builder.CommonArgs.Append(ilist.NewList[string]("-v", hostPath+":/home/coder/code/.booth:ro")) +} + func flattenArgs(argsList ilist.List[ilist.List[string]]) []string { var flattened []string argsList.Range(func(_ int, group ilist.List[string]) bool { diff --git a/cli/src/pkg/booth/booth_runner.go b/cli/src/pkg/booth/booth_runner.go index a289aed4..c4f4ef7e 100644 --- a/cli/src/pkg/booth/booth_runner.go +++ b/cli/src/pkg/booth/booth_runner.go @@ -36,6 +36,7 @@ func (runner *BoothRunner) Run() error { ctx = PortDetermination(ctx) ctx = ShowDebugBanner(ctx) ctx = SetupDind(ctx) + ctx = SetupSandbox(ctx) ctx = PrepareRunMode(ctx) ctx = PrepareCommonArgs(ctx) if err := ensureContainerNameAvailable(ctx); err != nil { diff --git a/cli/src/pkg/booth/debug_banner.go b/cli/src/pkg/booth/debug_banner.go index adf8742a..7f15daff 100644 --- a/cli/src/pkg/booth/debug_banner.go +++ b/cli/src/pkg/booth/debug_banner.go @@ -47,6 +47,20 @@ func ShowDebugBanner(ctx appctx.AppContext) appctx.AppContext { fmt.Printf("PORT_GENERATED: %t\n", ctx.PortGenerated()) fmt.Println() fmt.Printf("DIND: %t\n", ctx.Dind()) + fmt.Printf("SANDBOX: %t\n", ctx.Sandbox()) + if ctx.Sandbox() { + entries, note := effectiveSandboxAllowlist(ctx) + fmt.Printf("SANDBOX_ALLOWLIST_FILE: %s\n", ctx.SandboxAllowlistFile()) + fmt.Printf("SANDBOX_ALLOWLIST_EXTRA: %v\n", ctx.SandboxAllowlist()) + if len(entries) > 0 { + fmt.Printf("SANDBOX_ALLOWLIST: %s\n", strings.Join(entries, ", ")) + } else { + fmt.Printf("SANDBOX_ALLOWLIST: (empty)\n") + } + if note != "" { + fmt.Println(note) + } + } fmt.Println() fmt.Printf("CONTAINER_ENV_FILE: %s\n", ctx.EnvFile()) fmt.Println() diff --git a/cli/src/pkg/booth/ensure_docker_image.go b/cli/src/pkg/booth/ensure_docker_image.go index e87a8c17..04a2e3dc 100644 --- a/cli/src/pkg/booth/ensure_docker_image.go +++ b/cli/src/pkg/booth/ensure_docker_image.go @@ -198,8 +198,13 @@ func compileBoothfile(ctx appctx.AppContext, boothfilePath string) string { } } - // Write to a temporary file in .booth/ - generatedPath := filepath.Join(ctx.Code(), ".booth", ".Dockerfile.generated") + // Write to a temporary file (avoid polluting .booth/) + tempDir, err := os.MkdirTemp("", "codingbooth-dockerfile-") + if err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to create temp dir for Dockerfile: %v\n", err) + os.Exit(1) + } + generatedPath := filepath.Join(tempDir, "Dockerfile.generated") err = os.WriteFile(generatedPath, []byte(compileResult.Dockerfile), 0644) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to write generated Dockerfile: %v\n", err) diff --git a/cli/src/pkg/booth/init/initialize_app_context.go b/cli/src/pkg/booth/init/initialize_app_context.go index b57e2216..da1951bc 100644 --- a/cli/src/pkg/booth/init/initialize_app_context.go +++ b/cli/src/pkg/booth/init/initialize_app_context.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/nawaman/codingbooth/src/pkg/appctx" + "github.com/nawaman/codingbooth/src/pkg/defaults" "github.com/nawaman/codingbooth/src/pkg/ilist" "github.com/nawaman/codingbooth/src/pkg/nillable" ) @@ -56,6 +57,7 @@ func InitializeAppContext(version string, boundary InitializeAppContextBoundary) readFromEnvVars(boundary, &context) readFromToml(boundary, &context, configExplicitlySet) readFromArgs(boundary, &context, ilist.NewListFromSlice(args.Slice()[1:])) + validateConfig(&context.Config) if context.Config.ProjectName == "" { context.Config.ProjectName = getProjectName(context.Config.Code.ValueOr(".")) @@ -98,6 +100,152 @@ func InitializeAppContext(version string, boundary InitializeAppContextBoundary) return context.Build() } +func validateConfig(config *appctx.AppConfig) { + if err := validateEgressConfig(config); err != nil { + panic(err) + } +} + +const ( + defaultSandboxAllowlistPath = ".booth/sandbox/allowlist.txt" + defaultSandboxPolicyPath = ".booth/sandbox/envoy.yaml" +) + +func validateEgressConfig(config *appctx.AppConfig) error { + // --sandboxed is a shorthand for egress guardrails. + if config.Sandbox { + if config.Egress.Mode == "" { + config.Egress.Mode = "envoy" + } + if config.Egress.Enforcement == "" { + config.Egress.Enforcement = "iptables" + } + if config.Egress.Default == "" { + config.Egress.Default = "deny" + } + } + + config.Egress.Mode = strings.ToLower(strings.TrimSpace(config.Egress.Mode)) + config.Egress.Enforcement = strings.ToLower(strings.TrimSpace(config.Egress.Enforcement)) + config.Egress.Default = strings.ToLower(strings.TrimSpace(config.Egress.Default)) + config.Egress.AllowlistFile = strings.TrimSpace(config.Egress.AllowlistFile) + config.Egress.PolicyFile = strings.TrimSpace(config.Egress.PolicyFile) + config.SandboxAllowlistFile = strings.TrimSpace(config.SandboxAllowlistFile) + config.SandboxPolicyFile = strings.TrimSpace(config.SandboxPolicyFile) + for i := range config.SandboxAllowlist { + config.SandboxAllowlist[i] = strings.TrimSpace(config.SandboxAllowlist[i]) + } + + if err := applySandboxPolicyDefaults(config); err != nil { + return err + } + + if config.Egress.Mode != "" && + config.Egress.Mode != "none" && + config.Egress.Mode != "envoy" && + config.Egress.Mode != "squid" && + config.Egress.Mode != "tinyproxy" { + return fmt.Errorf("invalid egress.mode %q (supported: none, envoy, squid, tinyproxy)", config.Egress.Mode) + } + + if config.Egress.Enforcement != "" && + config.Egress.Enforcement != "none" && + config.Egress.Enforcement != "iptables" && + config.Egress.Enforcement != "nftables" { + return fmt.Errorf("invalid egress.enforcement %q (supported: none, iptables, nftables)", config.Egress.Enforcement) + } + + if config.Egress.Default != "" && + config.Egress.Default != "deny" && + config.Egress.Default != "allow" { + return fmt.Errorf("invalid egress.default %q (supported: deny, allow)", config.Egress.Default) + } + + if config.Egress.AllowlistFile != "" || config.Egress.PolicyFile != "" { + return fmt.Errorf("egress.* is no longer supported; use sandbox-allowlist-file or sandbox-policy-file") + } + + if config.SandboxPolicyFile != "" && len(config.SandboxAllowlist) > 0 { + return fmt.Errorf("sandbox-allowlist cannot be used with sandbox-policy-file") + } + + if config.SandboxAllowlistFile != "" && config.SandboxPolicyFile != "" { + return fmt.Errorf("sandbox-allowlist-file and sandbox-policy-file are mutually exclusive") + } + + if config.SandboxAllowlistFile != "" { + if err := ensureRegularFile(resolvePathFromCodeDir(config.Code.ValueOr(""), config.SandboxAllowlistFile)); err != nil { + return fmt.Errorf("invalid sandbox-allowlist-file: %w", err) + } + } + + if config.SandboxPolicyFile != "" { + if err := ensureRegularFile(resolvePathFromCodeDir(config.Code.ValueOr(""), config.SandboxPolicyFile)); err != nil { + return fmt.Errorf("invalid sandbox-policy-file: %w", err) + } + } + + return nil +} + +func applySandboxPolicyDefaults(config *appctx.AppConfig) error { + if !config.Sandbox { + return nil + } + if config.SandboxAllowlistFile != "" || config.SandboxPolicyFile != "" { + return nil + } + + codeDir := config.Code.ValueOr("") + if codeDir == "" { + return fmt.Errorf("sandbox requires a code directory to resolve egress policy") + } + + allowlistPath := filepath.Join(codeDir, defaultSandboxAllowlistPath) + policyPath := filepath.Join(codeDir, defaultSandboxPolicyPath) + allowlistExists := fileExists(allowlistPath) + policyExists := fileExists(policyPath) + + if allowlistExists && policyExists { + return fmt.Errorf("both %q and %q exist; choose only one", defaultSandboxAllowlistPath, defaultSandboxPolicyPath) + } + if allowlistExists { + config.SandboxAllowlistFile = defaultSandboxAllowlistPath + return nil + } + if policyExists { + config.SandboxPolicyFile = defaultSandboxPolicyPath + return nil + } + + if err := os.MkdirAll(filepath.Dir(allowlistPath), 0o755); err != nil { + return fmt.Errorf("failed to create default allowlist dir: %w", err) + } + if err := os.WriteFile(allowlistPath, []byte(defaults.ExampleAllowlist), 0o644); err != nil { + return fmt.Errorf("failed to write default allowlist: %w", err) + } + config.SandboxAllowlistFile = defaultSandboxAllowlistPath + return nil +} + +func resolvePathFromCodeDir(codeDir, path string) string { + if filepath.IsAbs(path) || codeDir == "" { + return path + } + return filepath.Join(codeDir, path) +} + +func ensureRegularFile(path string) error { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("file %q does not exist", path) + } + if info.IsDir() { + return fmt.Errorf("%q is a directory", path) + } + return nil +} + // getProjectName extracts a sanitized project name from the code path func getProjectName(codePath string) string { // Resolve to absolute path to handle relative paths like ".." @@ -210,6 +358,14 @@ func parseArgs(args ilist.List[string], cfg *appctx.AppConfig) error { cfg.Dind = true i++ + case "--sandboxed": + cfg.Sandbox = true + i++ + case "--sandbox": + // Backward compatibility alias. + cfg.Sandbox = true + i++ + case "--pull": cfg.Pull = true i++ @@ -218,6 +374,10 @@ func parseArgs(args ilist.List[string], cfg *appctx.AppConfig) error { cfg.SilenceBuild = true i++ + case "--writable-booth": + cfg.WritableBooth = true + i++ + // Image selection case "--image": v, err := needValue(args, i, arg) diff --git a/cli/src/pkg/booth/init/initialize_app_context_egress_integration_test.go b/cli/src/pkg/booth/init/initialize_app_context_egress_integration_test.go new file mode 100644 index 00000000..14497cea --- /dev/null +++ b/cli/src/pkg/booth/init/initialize_app_context_egress_integration_test.go @@ -0,0 +1,98 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package init + +import "testing" + +func TestIntegration_InitializeAppContext_Egress_ValidAllowlist(t *testing.T) { + res := RunInitializeAppContext(t, TestInput{ + TomlFiles: []TomlFile{ + { + Path: ".booth/config.toml", + Content: ` +sandboxed = true +sandbox-allowlist-file = ".booth/sandbox/allowlist.txt" +`, + }, + { + Path: ".booth/sandbox/allowlist.txt", + Content: "pypi.org\n", + }, + }, + }) + + if got := res.Ctx.EgressMode(); got != "envoy" { + t.Fatalf("expected normalized egress mode %q, got %q", "envoy", got) + } + if got := res.Ctx.EgressEnforcement(); got != "iptables" { + t.Fatalf("expected egress enforcement %q, got %q", "iptables", got) + } + if got := res.Ctx.EgressDefault(); got != "deny" { + t.Fatalf("expected egress default %q, got %q", "deny", got) + } +} + +func TestIntegration_InitializeAppContext_EgressBlock_Panics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic when egress.* is set, but did not panic") + } + }() + + _ = RunInitializeAppContext(t, TestInput{ + TomlFiles: []TomlFile{ + { + Path: ".booth/config.toml", + Content: ` +[egress] +allowlist-file = ".booth/sandbox/allowlist.txt" +`, + }, + }, + }) +} + +func TestIntegration_InitializeAppContext_Sandbox_AllowlistAndPolicy_Panics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic when both allowlist and policy are set, but did not panic") + } + }() + + _ = RunInitializeAppContext(t, TestInput{ + TomlFiles: []TomlFile{ + { + Path: ".booth/config.toml", + Content: ` +sandboxed = true +sandbox-allowlist-file = ".booth/sandbox/allowlist.txt" +sandbox-policy-file = ".booth/sandbox/envoy.yaml" +`, + }, + {Path: ".booth/sandbox/allowlist.txt", Content: "pypi.org\n"}, + {Path: ".booth/sandbox/envoy.yaml", Content: "static_resources: {}\n"}, + }, + }) +} + +func TestIntegration_InitializeAppContext_Egress_SandboxFlagSetsDefaults(t *testing.T) { + res := RunInitializeAppContext(t, TestInput{ + Args: []string{"--sandboxed"}, + CurrentPath: ".", + }) + + if !res.Ctx.Sandbox() { + t.Fatalf("expected sandbox flag to be true") + } + if got := res.Ctx.EgressMode(); got != "envoy" { + t.Fatalf("expected sandbox egress mode %q, got %q", "envoy", got) + } + if got := res.Ctx.EgressEnforcement(); got != "iptables" { + t.Fatalf("expected sandbox enforcement %q, got %q", "iptables", got) + } + if got := res.Ctx.EgressDefault(); got != "deny" { + t.Fatalf("expected sandbox default %q, got %q", "deny", got) + } +} diff --git a/cli/src/pkg/booth/port_determination.go b/cli/src/pkg/booth/port_determination.go index a90e6a4e..88b78c27 100644 --- a/cli/src/pkg/booth/port_determination.go +++ b/cli/src/pkg/booth/port_determination.go @@ -26,6 +26,11 @@ func PortDetermination(ctx appctx.AppContext) appctx.AppContext { switch upperPort { case "RANDOM": + // In dryrun mode, avoid binding sockets so tests remain deterministic in restricted environments. + if ctx.Dryrun() { + portNumber, portGenerated = 10000, true + break + } // Generate random ports in increments of 1000 (10000, 11000, 12000, etc.) portNumber, portGenerated = findRandomPort() if !portGenerated { @@ -34,6 +39,11 @@ func PortDetermination(ctx appctx.AppContext) appctx.AppContext { } case "NEXT": + // In dryrun mode, avoid binding sockets so tests remain deterministic in restricted environments. + if ctx.Dryrun() { + portNumber, portGenerated = 10000, true + break + } // Find next available port starting from 10000 in increments of 1000 portNumber, portGenerated = findNextPort() if !portGenerated { diff --git a/cli/src/pkg/booth/sandbox_setup.go b/cli/src/pkg/booth/sandbox_setup.go new file mode 100644 index 00000000..34d0ebdb --- /dev/null +++ b/cli/src/pkg/booth/sandbox_setup.go @@ -0,0 +1,463 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package booth + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/nawaman/codingbooth/src/pkg/appctx" + "github.com/nawaman/codingbooth/src/pkg/docker" + "github.com/nawaman/codingbooth/src/pkg/ilist" +) + +const ( + sandboxProxyImage = "envoyproxy/envoy:v1.31-latest" + sandboxProxyUID = 101 + sandboxProxyPort = 15001 + sandboxProxyConfigPath = "/etc/envoy/envoy.yaml" +) + +// SetupSandbox sets up egress sandbox sidecars and firewall policy. +// +// Behavior: +// - If --sandboxed is off: no-op. +// - If --sandboxed + --dind: reuse DinD sidecar network namespace. +// - If --sandboxed only: create a dedicated netns-owner sidecar and share its namespace. +func SetupSandbox(ctx appctx.AppContext) appctx.AppContext { + if !ctx.Sandbox() { + return ctx + } + + if mode := strings.TrimSpace(strings.ToLower(ctx.EgressMode())); mode != "" && mode != "envoy" { + fmt.Fprintf(os.Stderr, "❌ egress mode %q is not implemented yet in sandbox mode.\n", mode) + os.Exit(1) + } + + builder := ctx.ToBuilder() + netnsOwner := getSandboxNetnsOwnerName(ctx) + + // If not using DinD, create dedicated network + owner sidecar. + if !ctx.Dind() { + netName := getSandboxNet(ctx) + createdNet := createDindNetwork(ctx, netName) + builder.CreatedSandboxNet = createdNet + + extraPorts := extractPortFlags(ctx.RunArgs()) + if err := startSandboxNetnsOwner(ctx, netnsOwner, netName, ctx.PortNumber(), extraPorts); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to start sandbox netns sidecar: %v\n", err) + os.Exit(1) + } + + builder.RunArgs = stripNetworkAndPortFlags(ctx.RunArgs()) + builder.CommonArgs.Append(ilist.NewList[string]("--network", fmt.Sprintf("container:%s", netnsOwner))) + } + + ctx = builder.Build() + + // Resolve and materialize proxy config. + configPath, err := resolveSandboxProxyConfig(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to prepare sandbox proxy config: %v\n", err) + os.Exit(1) + } + + // Start Envoy sidecar in the shared namespace. + proxyName := getSandboxProxyName(ctx) + if err := startSandboxProxy(ctx, proxyName, netnsOwner, configPath); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to start sandbox proxy sidecar: %v\n", err) + os.Exit(1) + } + + // Wait for proxy and then enforce firewall. + if err := waitForSandboxProxyReady(ctx, netnsOwner); err != nil { + fmt.Fprintf(os.Stderr, "❌ Sandbox proxy did not become ready: %v\n", err) + os.Exit(1) + } + if err := applySandboxFirewall(ctx, netnsOwner); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to apply sandbox firewall rules: %v\n", err) + os.Exit(1) + } + + // Set proxy env for the workspace container. + builder = ctx.ToBuilder() + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", sandboxProxyPort) + builder.CommonArgs.Append(ilist.NewList[string]("-e", "http_proxy="+proxyURL)) + builder.CommonArgs.Append(ilist.NewList[string]("-e", "HTTP_PROXY="+proxyURL)) + builder.CommonArgs.Append(ilist.NewList[string]("-e", "https_proxy="+proxyURL)) + builder.CommonArgs.Append(ilist.NewList[string]("-e", "HTTPS_PROXY="+proxyURL)) + builder.CommonArgs.Append(ilist.NewList[string]("-e", "no_proxy=127.0.0.1,localhost")) + builder.CommonArgs.Append(ilist.NewList[string]("-e", "NO_PROXY=127.0.0.1,localhost")) + return builder.Build() +} + +func getSandboxNet(ctx appctx.AppContext) string { + return ctx.Name() + "-" + fmt.Sprintf("%d", ctx.PortNumber()) + "-sandbox-net" +} + +func getSandboxNetnsName(ctx appctx.AppContext) string { + return ctx.Name() + "-" + fmt.Sprintf("%d", ctx.PortNumber()) + "-sandbox-netns" +} + +func getSandboxProxyName(ctx appctx.AppContext) string { + return ctx.Name() + "-" + fmt.Sprintf("%d", ctx.PortNumber()) + "-sandbox-proxy" +} + +func getSandboxNetnsOwnerName(ctx appctx.AppContext) string { + if ctx.Dind() { + return getDindName(ctx) + } + return getSandboxNetnsName(ctx) +} + +func startSandboxNetnsOwner(ctx appctx.AppContext, ownerName, netName string, hostPort int, extraPorts []string) error { + flags := docker.DockerFlags{ + Dryrun: ctx.Dryrun(), + Verbose: ctx.Verbose(), + Silent: true, + } + + output, err := docker.DockerOutput(flags, "ps", ilist.NewList( + ilist.NewList("--filter", fmt.Sprintf("name=^/%s$", ownerName), "--format", "{{.Names}}"), + )) + if err == nil && strings.TrimSpace(output) == ownerName { + return nil + } + + portMapping := fmt.Sprintf("%d:10000", hostPort) + args := []string{ + "run", "-d", "--rm", + "--cap-add=NET_ADMIN", + "--name", ownerName, + "--network", netName, + "-p", portMapping, + } + for _, port := range extraPorts { + args = append(args, "-p", port) + } + args = append(args, "docker:dind", "sleep", "infinity") + + flags.Silent = false + return docker.Docker(flags, args[0], ilist.NewList(ilist.NewListFromSlice(args[1:]))) +} + +func resolveSandboxProxyConfig(ctx appctx.AppContext) (string, error) { + policyFile := strings.TrimSpace(ctx.SandboxPolicyFile()) + if policyFile != "" { + return resolvePolicyPath(ctx, policyFile), nil + } + return renderSandboxEnvoyConfigFromAllowlist(ctx) +} + +func resolvePolicyPath(ctx appctx.AppContext, path string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(ctx.Code(), path) +} + +func renderSandboxEnvoyConfigFromAllowlist(ctx appctx.AppContext) (string, error) { + allowlistPath := strings.TrimSpace(ctx.SandboxAllowlistFile()) + var patterns []string + if allowlistPath != "" { + content, err := os.ReadFile(resolvePolicyPath(ctx, allowlistPath)) + if err != nil { + return "", fmt.Errorf("failed to read allowlist file: %w", err) + } + patterns = parseAllowlistPatterns(mergeAllowlistContent(string(content), ctx.SandboxAllowlist())) + } else if len(ctx.SandboxAllowlist()) > 0 { + patterns = parseAllowlistPatterns(mergeAllowlistContent("", ctx.SandboxAllowlist())) + } + + outDir := filepath.Join(ctx.Code(), ".booth", "tools", "sandbox") + if err := os.MkdirAll(outDir, 0o755); err != nil { + return "", fmt.Errorf("failed to create egress output dir: %w", err) + } + + outPath := filepath.Join(outDir, "envoy.generated.yaml") + if err := os.WriteFile(outPath, []byte(buildEnvoyConfigFromPatterns(patterns)), 0o644); err != nil { + return "", fmt.Errorf("failed to write generated envoy config: %w", err) + } + if ctx.Verbose() { + fmt.Printf("SANDBOX_ENVOY_CONFIG: %s\n", outPath) + fmt.Println("SANDBOX_ENVOY_CONFIG_CONTENT:") + fmt.Println(buildEnvoyConfigFromPatterns(patterns)) + } + return outPath, nil +} + +func parseAllowlistPatterns(content string) []string { + lines := strings.Split(content, "\n") + patterns := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + domain := line + if idx := strings.Index(domain, "#"); idx >= 0 { + domain = strings.TrimSpace(domain[:idx]) + } + if domain == "" { + continue + } + escaped := regexp.QuoteMeta(domain) + patterns = append(patterns, fmt.Sprintf("(^|.*\\.)%s(:[0-9]+)?$", escaped)) + } + return patterns +} + +func parseAllowlistEntries(content string) []string { + lines := strings.Split(content, "\n") + entries := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + domain := line + if idx := strings.Index(domain, "#"); idx >= 0 { + domain = strings.TrimSpace(domain[:idx]) + } + if domain == "" { + continue + } + entries = append(entries, domain) + } + return entries +} + +func mergeAllowlistContent(content string, extra []string) string { + if len(extra) == 0 { + return content + } + var builder strings.Builder + builder.WriteString(content) + if content != "" && !strings.HasSuffix(content, "\n") { + builder.WriteString("\n") + } + for _, entry := range extra { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + builder.WriteString(entry) + builder.WriteString("\n") + } + return builder.String() +} + +func effectiveSandboxAllowlist(ctx appctx.AppContext) (entries []string, note string) { + allowlistPath := strings.TrimSpace(ctx.SandboxAllowlistFile()) + content := "" + if allowlistPath != "" { + path := resolvePolicyPath(ctx, allowlistPath) + b, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Sprintf("SANDBOX_ALLOWLIST_ERROR: %v", err) + } + content = string(b) + } + if content == "" && len(ctx.SandboxAllowlist()) == 0 { + return nil, "" + } + merged := mergeAllowlistContent(content, ctx.SandboxAllowlist()) + return parseAllowlistEntries(merged), "" +} + +func buildEnvoyConfigFromPatterns(patterns []string) string { + var policyBuilder strings.Builder + for i, pattern := range patterns { + fmt.Fprintf(&policyBuilder, " allow_%d:\n", i) + policyBuilder.WriteString(" permissions:\n") + policyBuilder.WriteString(" - header:\n") + policyBuilder.WriteString(" name: \":authority\"\n") + policyBuilder.WriteString(" string_match:\n") + policyBuilder.WriteString(" safe_regex:\n") + policyBuilder.WriteString(" google_re2: {}\n") + fmt.Fprintf(&policyBuilder, " regex: %q\n", pattern) + policyBuilder.WriteString(" principals:\n") + policyBuilder.WriteString(" - any: true\n") + } + + if policyBuilder.Len() == 0 { + policyBuilder.WriteString(" deny_all:\n") + policyBuilder.WriteString(" permissions:\n") + policyBuilder.WriteString(" - header:\n") + policyBuilder.WriteString(" name: \":authority\"\n") + policyBuilder.WriteString(" string_match:\n") + policyBuilder.WriteString(" exact: \"__deny_all__\"\n") + policyBuilder.WriteString(" principals:\n") + policyBuilder.WriteString(" - any: true\n") + } + + return fmt.Sprintf(`static_resources: + listeners: + - name: egress_proxy + address: + socket_address: + address: 0.0.0.0 + port_value: %d + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + codec_type: AUTO + route_config: + name: proxy_routes + virtual_hosts: + - name: forward_proxy + domains: ["*"] + routes: + - match: + connect_matcher: {} + route: + cluster: dynamic_forward_proxy_cluster + upgrade_configs: + - upgrade_type: CONNECT + connect_config: {} + - match: + prefix: "/" + redirect: + https_redirect: true + http_filters: + - name: envoy.filters.http.rbac + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: ALLOW + policies: +%s + - name: envoy.filters.http.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + upgrade_configs: + - upgrade_type: CONNECT + + clusters: + - name: dynamic_forward_proxy_cluster + connect_timeout: 5s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + +admin: + address: + socket_address: + address: 127.0.0.1 + port_value: 9901 +`, sandboxProxyPort, policyBuilder.String()) +} + +func startSandboxProxy(ctx appctx.AppContext, proxyName, netnsOwnerName, configPath string) error { + flags := docker.DockerFlags{ + Dryrun: ctx.Dryrun(), + Verbose: ctx.Verbose(), + Silent: true, + } + output, err := docker.DockerOutput(flags, "ps", ilist.NewList( + ilist.NewList("--filter", fmt.Sprintf("name=^/%s$", proxyName), "--format", "{{.Names}}"), + )) + if err == nil && strings.TrimSpace(output) == proxyName { + return nil + } + + args := []string{ + "run", "-d", "--rm", + "--name", proxyName, + "--network", fmt.Sprintf("container:%s", netnsOwnerName), + "-v", fmt.Sprintf("%s:%s:ro", configPath, sandboxProxyConfigPath), + sandboxProxyImage, + "-c", sandboxProxyConfigPath, + "--log-level", "warning", + } + flags.Silent = false + return docker.Docker(flags, args[0], ilist.NewList(ilist.NewListFromSlice(args[1:]))) +} + +func waitForSandboxProxyReady(ctx appctx.AppContext, netnsOwnerName string) error { + if ctx.Dryrun() { + return nil + } + + flags := docker.DockerFlags{ + Dryrun: false, + Verbose: ctx.Verbose(), + Silent: true, + } + checkCmd := fmt.Sprintf("nc -z 127.0.0.1 %d", sandboxProxyPort) + for i := 0; i < 30; i++ { + _, err := docker.DockerOutput(flags, "exec", ilist.NewList( + ilist.NewList(netnsOwnerName, "sh", "-lc", checkCmd), + )) + if err == nil { + return nil + } + time.Sleep(250 * time.Millisecond) + } + return fmt.Errorf("proxy port %d did not open in time", sandboxProxyPort) +} + +func applySandboxFirewall(ctx appctx.AppContext, netnsOwnerName string) error { + if strings.ToLower(strings.TrimSpace(ctx.EgressEnforcement())) != "iptables" { + return nil + } + + cmd := fmt.Sprintf(` +set -eu +iptables -F OUTPUT +iptables -A OUTPUT -o lo -j ACCEPT +iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT +iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport %d -j ACCEPT +iptables -A OUTPUT -m owner --uid-owner %d -p tcp --dport 80 -j ACCEPT +iptables -A OUTPUT -m owner --uid-owner %d -p tcp --dport 443 -j ACCEPT +iptables -P OUTPUT DROP +`, sandboxProxyPort, sandboxProxyUID, sandboxProxyUID) + + flags := docker.DockerFlags{ + Dryrun: ctx.Dryrun(), + Verbose: ctx.Verbose(), + Silent: true, + } + return docker.Docker(flags, "exec", ilist.NewList( + ilist.NewList(netnsOwnerName, "sh", "-lc", cmd), + )) +} + +func cleanupSandboxResources(ctx appctx.AppContext, flags *docker.DockerFlags) { + if !ctx.Sandbox() { + return + } + + _ = docker.Docker(*flags, "stop", ilist.NewList(ilist.NewList(getSandboxProxyName(ctx)))) + + if ctx.Dind() { + return + } + + _ = docker.Docker(*flags, "stop", ilist.NewList(ilist.NewList(getSandboxNetnsName(ctx)))) + if ctx.CreatedSandboxNet() { + _ = docker.Docker(*flags, "network", ilist.NewList(ilist.NewList("rm", getSandboxNet(ctx)))) + } +} diff --git a/cli/src/pkg/booth/sandbox_setup_test.go b/cli/src/pkg/booth/sandbox_setup_test.go new file mode 100644 index 00000000..223f2533 --- /dev/null +++ b/cli/src/pkg/booth/sandbox_setup_test.go @@ -0,0 +1,42 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package booth + +import ( + "strings" + "testing" +) + +func TestParseAllowlistPatterns(t *testing.T) { + content := ` +# comment +pypi.org +api.example.com # trailing + +` + patterns := parseAllowlistPatterns(content) + if len(patterns) != 2 { + t.Fatalf("expected 2 patterns, got %d", len(patterns)) + } + if patterns[0] != `(^|.*\.)pypi\.org(:[0-9]+)?$` { + t.Fatalf("unexpected first pattern: %q", patterns[0]) + } + if patterns[1] != `(^|.*\.)api\.example\.com(:[0-9]+)?$` { + t.Fatalf("unexpected second pattern: %q", patterns[1]) + } +} + +func TestBuildEnvoyConfigFromPatterns(t *testing.T) { + config := buildEnvoyConfigFromPatterns([]string{`(^|.*\.)pypi\.org(:[0-9]+)?$`}) + if !strings.Contains(config, "envoy.filters.http.rbac") { + t.Fatalf("generated config missing RBAC filter") + } + if !strings.Contains(config, "pypi") { + t.Fatalf("generated config missing allowlist pattern") + } + if !strings.Contains(config, "connect_config") { + t.Fatalf("generated config missing CONNECT route configuration") + } +} diff --git a/cli/src/pkg/defaults/allowlist.go b/cli/src/pkg/defaults/allowlist.go new file mode 100644 index 00000000..4e953f68 --- /dev/null +++ b/cli/src/pkg/defaults/allowlist.go @@ -0,0 +1,10 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package defaults + +import _ "embed" + +//go:embed example-allowlist.txt +var ExampleAllowlist string diff --git a/cli/src/pkg/defaults/example-allowlist.txt b/cli/src/pkg/defaults/example-allowlist.txt new file mode 100644 index 00000000..9d0182af --- /dev/null +++ b/cli/src/pkg/defaults/example-allowlist.txt @@ -0,0 +1,407 @@ +# ============================================================ +# Common software development egress allowlist +# ============================================================ +# NOTE: This list is inevitably incomplete. Any new dependency, +# tool, or framework may introduce domains not listed here. +# Review and maintain regularly. +# +# Put this file at: .booth/sandbox/allowlist.txt +# +# ============================================================ + +# ---------------------------------------------------------- +# Source control & collaboration +# ---------------------------------------------------------- +github.com +api.github.com +raw.githubusercontent.com +codeload.github.com +objects.githubusercontent.com +github-releases.githubusercontent.com +gist.github.com +gitlab.com +gitlab-static.net +registry.gitlab.com +bitbucket.org +codeberg.org +sr.ht + +# ---------------------------------------------------------- +# Container registries +# ---------------------------------------------------------- +docker.io +registry-1.docker.io +auth.docker.io +production.cloudflare.docker.com +hub.docker.com +ghcr.io +quay.io +mcr.microsoft.com +gallery.ecr.aws +gcr.io + +# ---------------------------------------------------------- +# JavaScript / Node.js +# ---------------------------------------------------------- +registry.npmjs.org +npmjs.org +yarnpkg.com +nodejs.org +deno.land + +# ---------------------------------------------------------- +# Python +# ---------------------------------------------------------- +pypi.org +files.pythonhosted.org +python.org +docs.python.org +conda.anaconda.org +repo.anaconda.com + +# ---------------------------------------------------------- +# Go +# ---------------------------------------------------------- +proxy.golang.org +sum.golang.org +golang.org +go.dev +pkg.go.dev + +# ---------------------------------------------------------- +# Rust +# ---------------------------------------------------------- +crates.io +index.crates.io +static.crates.io +static.rust-lang.org +rust-lang.org +docs.rs + +# ---------------------------------------------------------- +# Java / JVM / Kotlin / Scala +# ---------------------------------------------------------- +repo.maven.apache.org +repo1.maven.org +plugins.gradle.org +services.gradle.org +search.maven.org +dl.google.com +kotlinlang.org +scala-lang.org + +# ---------------------------------------------------------- +# .NET / NuGet +# ---------------------------------------------------------- +nuget.org +api.nuget.org +dotnet.microsoft.com + +# ---------------------------------------------------------- +# Ruby +# ---------------------------------------------------------- +rubygems.org +api.rubygems.org +ruby-lang.org + +# ---------------------------------------------------------- +# PHP +# ---------------------------------------------------------- +packagist.org +repo.packagist.org +getcomposer.org +php.net + +# ---------------------------------------------------------- +# Elixir / Erlang +# ---------------------------------------------------------- +hex.pm +repo.hex.pm +elixir-lang.org +erlang.org + +# ---------------------------------------------------------- +# Haskell +# ---------------------------------------------------------- +hackage.haskell.org +haskell.org + +# ---------------------------------------------------------- +# Dart / Flutter +# ---------------------------------------------------------- +pub.dev +storage.flutter-io.cn +dart.dev + +# ---------------------------------------------------------- +# Swift / Cocoapods +# ---------------------------------------------------------- +swift.org +cocoapods.org +cdn.cocoapods.org + +# ---------------------------------------------------------- +# R / CRAN +# ---------------------------------------------------------- +cran.r-project.org +cran.rstudio.com + +# ---------------------------------------------------------- +# Perl +# ---------------------------------------------------------- +cpan.org +metacpan.org + +# ---------------------------------------------------------- +# Other languages / toolchains +# ---------------------------------------------------------- +ziglang.org +nim-lang.org +dlang.org +vlang.io +adoptium.net + +# ---------------------------------------------------------- +# CDNs hosting libraries & assets +# ---------------------------------------------------------- +cdnjs.cloudflare.com +cdn.jsdelivr.net +unpkg.com +esm.sh +esm.run +fonts.googleapis.com +fonts.gstatic.com +ajax.googleapis.com + +# ---------------------------------------------------------- +# IDE & editor extensions +# ---------------------------------------------------------- +marketplace.visualstudio.com +update.code.visualstudio.com +vscode.blob.core.windows.net +az764295.vo.msecnd.net +gallery.vsassets.io +plugins.jetbrains.com +download.jetbrains.com +open-vsx.org + +# ---------------------------------------------------------- +# Linux package repos & mirrors +# ---------------------------------------------------------- +archive.ubuntu.com +security.ubuntu.com +deb.debian.org +security.debian.org +download.opensuse.org +mirrors.fedoraproject.org +dl.fedoraproject.org +centos.org +mirror.centos.org +vault.centos.org +packages.microsoft.com +apt.llvm.org +ppa.launchpad.net + +# ---------------------------------------------------------- +# macOS / Homebrew (if applicable) +# ---------------------------------------------------------- +formulae.brew.sh +homebrew.bintray.com +ghcr.io + +# ---------------------------------------------------------- +# Infrastructure-as-Code & DevOps tooling +# ---------------------------------------------------------- +registry.terraform.io +releases.hashicorp.com +checkpoint-api.hashicorp.com +galaxy.ansible.com +charts.helm.sh +artifacthub.io +kubernetes.io +dl.k8s.io +storage.googleapis.com +get.helm.sh + +# ---------------------------------------------------------- +# CI/CD services +# ---------------------------------------------------------- +circleci.com +app.circleci.com +travis-ci.com +buildkite.com +dev.azure.com +vsrm.dev.azure.com + +# ---------------------------------------------------------- +# Cloud providers (broad — consider narrowing per use case) +# ---------------------------------------------------------- +# AWS +aws.amazon.com +amazonaws.com +elasticbeanstalk.com +cloudfront.net +# Azure +azure.com +azure.microsoft.com +windows.net +login.microsoftonline.com +management.azure.com +# GCP +cloud.google.com +googleapis.com +appspot.com +# Cloudflare +cloudflare.com +workers.dev +pages.dev +# Others +digitalocean.com +heroku.com +herokuapp.com +vercel.app +vercel.com +netlify.app +netlify.com +fly.dev +fly.io +railway.app +render.com +supabase.com +supabase.co + +# ---------------------------------------------------------- +# Database providers (docs, drivers, downloads) +# ---------------------------------------------------------- +postgresql.org +mysql.com +mongodb.com +downloads.mongodb.com +cloud.mongodb.com +redis.io +download.redis.io +elastic.co +artifacts.elastic.co +neo4j.com +mariadb.org +clickhouse.com +cockroachlabs.com +couchdb.apache.org +planetscale.com +neon.tech + +# ---------------------------------------------------------- +# Monitoring, logging & observability +# ---------------------------------------------------------- +sentry.io +datadoghq.com +newrelic.com +grafana.com +grafana.net +prometheus.io +pagerduty.com +honeycomb.io +splunk.com + +# ---------------------------------------------------------- +# Security scanning & vulnerability databases +# ---------------------------------------------------------- +snyk.io +api.snyk.io +sonarcloud.io +sonarqube.org +cve.mitre.org +nvd.nist.gov +osv.dev +security.snyk.io +github.com/advisories + +# ---------------------------------------------------------- +# Documentation & reference +# ---------------------------------------------------------- +stackoverflow.com +cdn.sstatic.net +developer.mozilla.org +devdocs.io +readthedocs.io +readthedocs.org + +# ---------------------------------------------------------- +# TLS / Certificates / OCSP +# ---------------------------------------------------------- +letsencrypt.org +acme-v02.api.letsencrypt.org +ocsp.digicert.com +ocsp.pki.goog +crl.microsoft.com +cacerts.digicert.com +ocsp.comodoca.com +ocsp.sectigo.com +ocsp.usertrust.com + +# ---------------------------------------------------------- +# Project management & communication +# ---------------------------------------------------------- +atlassian.com +atlassian.net +jira.com +trello.com +slack.com +wss-primary.slack.com +discord.com +gateway.discord.gg +notion.so +notion-static.com +linear.app +asana.com +teams.microsoft.com + +# ---------------------------------------------------------- +# Testing & browser automation services +# ---------------------------------------------------------- +browserstack.com +saucelabs.com +cypress.io +download.cypress.io +lambdatest.com +percy.io + +# ---------------------------------------------------------- +# AI coding assistants +# ---------------------------------------------------------- +api.anthropic.com +anthropic.com +claude.ai +api.openai.com +openai.com +copilot.github.com +copilot-proxy.githubusercontent.com +codeium.com +cursor.sh +platform.claude.com +tabnine.com + +# ---------------------------------------------------------- +# Internal registry proxies (uncomment if self-hosted) +# ---------------------------------------------------------- +# jfrog.io +# your-artifactory.example.com +# your-nexus.example.com +# your-verdaccio.example.com + +# ---------------------------------------------------------- +# Optional: Analytics & error tracking (uncomment if needed) +# ---------------------------------------------------------- +# google-analytics.com +# segment.io +# segment.com +# mixpanel.com +# amplitude.com +# bugsnag.com + +# ============================================================ +# END OF LIST +# Remember: This WILL be incomplete. Consider pairing with +# category-based filtering or egress proxy logging rather +# than relying on this as a sole enforcement mechanism. +# ============================================================ diff --git a/cli/src/pkg/docker/docker_build_test.go b/cli/src/pkg/docker/docker_build_test.go index d158d3fa..e91c4d39 100644 --- a/cli/src/pkg/docker/docker_build_test.go +++ b/cli/src/pkg/docker/docker_build_test.go @@ -15,6 +15,7 @@ import ( // TestDockerBuild_Silent tests DockerBuild with SilenceBuild enabled. // This test will actually build a Docker image but suppress build output unless it fails. func TestDockerBuild_Silent(t *testing.T) { + requireDocker(t) // Define options flags := docker.DockerFlags{ @@ -51,6 +52,7 @@ CMD ["echo", "Hello"] // TestDockerBuild_Normal tests DockerBuild with SilenceBuild disabled. func TestDockerBuild_Normal(t *testing.T) { + requireDocker(t) // Define options flags := docker.DockerFlags{ diff --git a/cli/src/pkg/docker/docker_test_helpers_test.go b/cli/src/pkg/docker/docker_test_helpers_test.go new file mode 100644 index 00000000..ebd3fd03 --- /dev/null +++ b/cli/src/pkg/docker/docker_test_helpers_test.go @@ -0,0 +1,23 @@ +// Copyright 2025-2026 : Nawa Manusitthipol +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package docker_test + +import ( + "io" + "os/exec" + "testing" +) + +func requireDocker(t *testing.T) { + t.Helper() + + cmd := exec.Command("docker", "version") + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + if err := cmd.Run(); err != nil { + t.Skipf("Skipping test - docker not available: %v", err) + } +} + diff --git a/docs/AGENT.md b/docs/AGENT.md index 70aa6816..13641a9f 100644 --- a/docs/AGENT.md +++ b/docs/AGENT.md @@ -42,11 +42,12 @@ cat /home/coder/code/.booth/Dockerfile # What's already configured? | Location | Persistence | What to do | |----------------------------------|------------------------------|---------------------------------| -| `/home/coder/code/` | Persists (mounted from host) | Project files, `.booth/` config | +| `/home/coder/code/` | Persists (mounted from host) | Project files | +| `/home/coder/code/.booth/` | Persists but **read-only** | Config — edit from host side | | `/home/coder/` (outside `code/`) | Ephemeral | Lost on restart | | `/opt/`, `/usr/`, `/etc/` | Ephemeral | Lost on restart | -**Key insight:** To make changes permanent, modify files in `/home/coder/code/.booth/` — these are the source of truth that rebuild the container. +**Key insight:** The `.booth/` directory is the source of truth that rebuilds the container, but it is **read-only inside the container** by default. To modify `.booth/` files, the user must edit them on the host (outside the container), or restart with `--writable-booth` to allow edits from inside. --- @@ -54,7 +55,7 @@ cat /home/coder/code/.booth/Dockerfile # What's already configured? ``` /home/coder/code/ # Project root (PERSISTENT - mounted from host) -├── .booth/ +├── .booth/ # READ-ONLY by default (use --writable-booth to allow edits) │ ├── config.toml # Runtime config (variant, ports, run-args, etc.) │ ├── Dockerfile # Custom image build │ ├── setups/ # Custom setup scripts (you create these) @@ -343,6 +344,7 @@ Setup scripts are the **source of truth** for: | Don't | Do Instead | |----------------------------------------------------------|-----------------------------------------------| +| Edit `.booth/` files from inside the container | Edit on the host, or use `--writable-booth` | | Install tools directly in running container (ephemeral) | Add to `.booth/Dockerfile` | | Edit files in `/etc/`, `/opt/`, `/usr/` directly | Create setup scripts in `.booth/setups/` | | Tell user to "just run `curl \| bash`" | Add proper setup to Dockerfile | @@ -358,6 +360,9 @@ Setup scripts are the **source of truth** for: **"Tool not found after restart"** - Was it added to `.booth/Dockerfile`? Ephemeral installs don't persist. +**"Read-only file system" when editing `.booth/` files** +- `.booth/` is mounted read-only by default. Edit files on the host side, or ask the user to restart with `--writable-booth`. + **"Permission denied"** - Setup scripts run as root during build. User-level changes go in startup scripts or `.booth/home/`. diff --git a/docs/AGENT_SETUP.md b/docs/AGENT_SETUP.md index a580ea90..83cd59fc 100644 --- a/docs/AGENT_SETUP.md +++ b/docs/AGENT_SETUP.md @@ -195,6 +195,16 @@ Access UI at shown URL. Stop with `docker stop `. ./booth -- python script.py ``` +### Allow Editing `.booth/` from Inside the Container + +By default, `.booth/` is **read-only** inside the container to prevent accidental or malicious config changes. If the user (or an AI agent inside) needs to edit `.booth/` files from within the container: + +```bash +./booth --writable-booth +``` + +> **Note:** This is mainly useful during initial setup or development of the booth configuration itself. For normal use, edit `.booth/` files on the host. + --- ## Available Setup Scripts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8b7bd762..ba985f0e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,9 @@ This file contains a list of changes for each released version. +## Unreleased +- Document that `--sandboxed` with `--dind` is **not supported** due to firewall bypass risk in the shared network namespace. + ## v0.16.0 - Rename binary from `coding-booth` to `codingbooth` - Booth example. diff --git a/docs/implementations/EGRESS_POLICY_PLAN.md b/docs/implementations/EGRESS_POLICY_PLAN.md new file mode 100644 index 00000000..fa33a8a7 --- /dev/null +++ b/docs/implementations/EGRESS_POLICY_PLAN.md @@ -0,0 +1,135 @@ +# Egress Policy - Decisions and Implementation Tasks + +This file captures decisions from the Envoy/firewall experiments and the task list to ship the feature. + +## Decisions + +1. **Architecture** + - Use a **sidecar-based egress model**. + - Run a dedicated **network namespace owner** container. + - Run both: + - the user workspace container, and + - the proxy container + in that shared namespace via `--network container:`. + +2. **Policy + Enforcement split** + - Keep these as separate components: + - **Policy engine** (Envoy/Squid/Tinyproxy/none) + - **Enforcement engine** (iptables/nftables/none) + - This enables pluggable backends. + +3. **Default security posture** + - Default to **deny outbound**. + - Explicit allow rules are required for access. + +4. **Configuration surface** + - Externally supported surface is **only** `--sandboxed`. + - Policy is provided by **one** of: + - `.booth/sandbox/allowlist.txt` (simple), or + - `.booth/sandbox/envoy.yaml` (advanced/custom). + - These are **mutually exclusive**. If both are set, fail fast with a clear error (no implicit precedence). + - Internally, `--sandboxed` uses Envoy + iptables with default deny (implementation detail, not user-tunable). + - If neither file exists, a default allowlist is materialized from the embedded template + (see `docs/implementations/example-allowlist.txt` or `codingbooth print-default-allowlist.txt`). + +6. **Immutability requirement for policy files** + - Policy files must be mounted into sidecar/proxy containers as **read-only bind mounts**. + - Containers must run **without `--privileged`** and without `CAP_SYS_ADMIN`. + - Do not mount Docker socket into workspace/proxy. + - This protects against in-container root modifying mounted policy files through normal overlay/write paths. + +7. **Operational note** + - If host-level trust is broken (host root, privileged container, or Docker socket control), policy can still be bypassed. + - This is expected and should be documented in threat model. +8. **GPU/USB compatibility note** + - GPU/USB access does **not** require `--privileged` or `CAP_NET_ADMIN`. + - As long as `--privileged` and `CAP_NET_ADMIN` are **not** granted, egress firewall rules remain enforced. +9. **DinD incompatibility (2026-02-06)** + - `--sandboxed` with `--dind` is **not supported**. + - The DinD sidecar shares the egress network namespace, and a user with Docker access can run + a privileged container to flush nftables/iptables, bypassing the firewall. + - Until further research, require `--sandboxed` to run **without** `--dind`. + +## Implementation Tasks + +## Phase 1 - Config + Parsing + +- [x] Add `[egress]` schema support to config parsing. +- [x] Add validation for: + - mode/enforcement enum values + - deny/allow default + - mutually exclusive/simple-vs-advanced policy inputs + - policy file existence checks. + +## Phase 2 - Runtime Orchestration + +- [x] Add netns-owner container lifecycle (create/start/reuse/cleanup). +- [x] Add proxy sidecar lifecycle for `egress.mode`. +- [x] Attach workspace container to netns-owner (`--network container:...`). +- [x] Add deterministic naming (`{container}-{port}-egress-netns`, etc). + +## Phase 3 - Policy Materialization + +- [x] Implement allowlist -> generated proxy config rendering. +- [x] Support direct custom config pass-through for `.booth/sandbox/envoy.yaml`. +- [x] Store generated artifacts under `.booth/tools/egress/` and mount read-only. + +## Phase 4 - Enforcement + +- [x] Implement `iptables` enforcement script for shared netns: + - allow loopback + - allow established/related + - allow DNS + - allow app -> local proxy port + - allow proxy process outbound 80/443 + - default OUTPUT DROP. +- [ ] Add optional `nftables` backend. +- [ ] Add teardown/restore logic for rules on stop. + +## Phase 5 - Hardening + +- [ ] Ensure sidecar/workspace are not launched privileged for egress mode. + - [ ] error out + - [ ] offer an explicit override +- [ ] Drop unnecessary capabilities. -- error out +- [ ] Set `no-new-privileges` where possible. +- [ ] Ensure policy mounts are read-only and verify write attempts fail. + +## Phase 6 - UX + CLI + +- [ ] Document `--verbose` as the debug surface for egress details (mode, policy file, proxy port, enforcement status). + +## Phase 7 - Docs + +- [ ] Add user doc page: quick start with `allowlist.txt`. +- [ ] Add advanced doc page: custom `envoy.yaml`. +- [ ] Add threat model/bypass boundaries section. +- [ ] Add troubleshooting (blocked domain, DNS issues, proxy startup failures). + +## Phase 8 - Tests + +- [ ] Integration tests: + - allowed domain succeeds via proxy + - blocked domain returns deny + - direct no-proxy outbound fails + - policy file write fails in container (root and non-root). +- [ ] Regression tests for cleanup (no leaked containers/rules). + +## Suggested MVP scope + +Start with `--sandboxed` only: +- Envoy + iptables with default deny (implementation detail). +- Policy provided by `.booth/sandbox/allowlist.txt` or `.booth/sandbox/envoy.yaml` (mutually exclusive). + +Defer alternate engines and user-tunable config until after MVP stabilizes. + +## Current MVP Progress + +- [x] `--sandboxed` flag + config parsing and validation +- [x] Envoy sidecar runtime wiring +- [x] iptables enforcement runtime wiring +- [x] Reuse DinD sidecar network namespace when `--dind` is enabled +- [x] Dedicated sandbox netns owner when `--dind` is not enabled +- [ ] **Re-evaluate DinD reuse** — shared netns allows firewall bypass via privileged DinD containers. +- [ ] CLI status/diagnostics command +- [ ] Full integration tests that run real Docker end-to-end in CI diff --git a/docs/implementations/EXAMPLES.md b/docs/implementations/EXAMPLES.md index 8dca4248..fd8916df 100644 --- a/docs/implementations/EXAMPLES.md +++ b/docs/implementations/EXAMPLES.md @@ -22,11 +22,12 @@ booth example list Output: ``` -Available examples (29): +Available examples (30): all-java aws bun conda demo deno dind elixir empty firebase gcloud go + firewall ... ... ``` @@ -147,3 +148,9 @@ Each example contains: - `.booth/config.toml` configuration - `.booth/Dockerfile` (optional) for custom images - Example code and documentation + +Security/network examples: +- `examples/workspaces/urlwhitelist-example/` - tinyproxy-based domain whitelist flow +- `examples/workspaces/firewall-example/` - `--sandboxed` egress enforcement (Envoy + iptables, with and without DinD reuse) +- `examples/workspaces/sandbox-allowlist-extra-example/` - `--sandboxed` with allowlist file + extra domains +- `examples/workspaces/sandbox-envoy-example/` - `--sandboxed` with custom `envoy.yaml` policy diff --git a/docs/implementations/URL_WHITELIST.md b/docs/implementations/URL_WHITELIST.md index bcce58e1..857d93db 100644 --- a/docs/implementations/URL_WHITELIST.md +++ b/docs/implementations/URL_WHITELIST.md @@ -147,6 +147,45 @@ For truly enforced network restrictions, future versions may explore: ## Usage +### Example allowlist template + +There is a ready-made allowlist template at `docs/implementations/example-allowlist.txt`. Use it as a starting point and customize it for your stack. + +**For sandbox allowlists:** + +```bash +mkdir -p .booth/sandbox +cp docs/implementations/example-allowlist.txt .booth/sandbox/allowlist.txt +``` + +You can also print the built-in default allowlist directly from the CLI: + +```bash +codingbooth print-default-allowlist.txt > .booth/sandbox/allowlist.txt +``` + +When `--sandboxed` is enabled and no policy files are present, CodingBooth will +materialize this embedded default into `.booth/sandbox/allowlist.txt`. + +**Optional extra allowlist** + +You can append extra domains via `.booth/config.toml`: +```toml +sandbox-allowlist = [ + "example.com", + "registry.npmjs.org" +] +``` +This list is merged into the active allowlist (default or file-based). It cannot be used with `sandbox-policy-file`. + +**For tinyproxy (in-container) allowlists:** + +```bash +cp docs/implementations/example-allowlist.txt .booth/home/.network-whitelist +``` + +Then add or remove domains as needed for your project. + ### Check Status ```bash @@ -385,6 +424,11 @@ unset HTTP_PROXY HTTPS_PROXY curl https://google.com # Still blocked by iptables ``` +For the newer sidecar-based egress sandbox flow (`--sandboxed`, Envoy + iptables), see `examples/workspaces/firewall-example/`. + +**Security note (2026-02-06):** `--sandboxed` with `--dind` is **not supported**. +Testing shows that a user with DinD access can start a privileged container in the shared network namespace and flush nftables, bypassing the egress firewall. Use `--sandboxed` **without** `--dind` until further research. + --- ## Related Files diff --git a/docs/implementations/example-allowlist.txt b/docs/implementations/example-allowlist.txt new file mode 100644 index 00000000..9d0182af --- /dev/null +++ b/docs/implementations/example-allowlist.txt @@ -0,0 +1,407 @@ +# ============================================================ +# Common software development egress allowlist +# ============================================================ +# NOTE: This list is inevitably incomplete. Any new dependency, +# tool, or framework may introduce domains not listed here. +# Review and maintain regularly. +# +# Put this file at: .booth/sandbox/allowlist.txt +# +# ============================================================ + +# ---------------------------------------------------------- +# Source control & collaboration +# ---------------------------------------------------------- +github.com +api.github.com +raw.githubusercontent.com +codeload.github.com +objects.githubusercontent.com +github-releases.githubusercontent.com +gist.github.com +gitlab.com +gitlab-static.net +registry.gitlab.com +bitbucket.org +codeberg.org +sr.ht + +# ---------------------------------------------------------- +# Container registries +# ---------------------------------------------------------- +docker.io +registry-1.docker.io +auth.docker.io +production.cloudflare.docker.com +hub.docker.com +ghcr.io +quay.io +mcr.microsoft.com +gallery.ecr.aws +gcr.io + +# ---------------------------------------------------------- +# JavaScript / Node.js +# ---------------------------------------------------------- +registry.npmjs.org +npmjs.org +yarnpkg.com +nodejs.org +deno.land + +# ---------------------------------------------------------- +# Python +# ---------------------------------------------------------- +pypi.org +files.pythonhosted.org +python.org +docs.python.org +conda.anaconda.org +repo.anaconda.com + +# ---------------------------------------------------------- +# Go +# ---------------------------------------------------------- +proxy.golang.org +sum.golang.org +golang.org +go.dev +pkg.go.dev + +# ---------------------------------------------------------- +# Rust +# ---------------------------------------------------------- +crates.io +index.crates.io +static.crates.io +static.rust-lang.org +rust-lang.org +docs.rs + +# ---------------------------------------------------------- +# Java / JVM / Kotlin / Scala +# ---------------------------------------------------------- +repo.maven.apache.org +repo1.maven.org +plugins.gradle.org +services.gradle.org +search.maven.org +dl.google.com +kotlinlang.org +scala-lang.org + +# ---------------------------------------------------------- +# .NET / NuGet +# ---------------------------------------------------------- +nuget.org +api.nuget.org +dotnet.microsoft.com + +# ---------------------------------------------------------- +# Ruby +# ---------------------------------------------------------- +rubygems.org +api.rubygems.org +ruby-lang.org + +# ---------------------------------------------------------- +# PHP +# ---------------------------------------------------------- +packagist.org +repo.packagist.org +getcomposer.org +php.net + +# ---------------------------------------------------------- +# Elixir / Erlang +# ---------------------------------------------------------- +hex.pm +repo.hex.pm +elixir-lang.org +erlang.org + +# ---------------------------------------------------------- +# Haskell +# ---------------------------------------------------------- +hackage.haskell.org +haskell.org + +# ---------------------------------------------------------- +# Dart / Flutter +# ---------------------------------------------------------- +pub.dev +storage.flutter-io.cn +dart.dev + +# ---------------------------------------------------------- +# Swift / Cocoapods +# ---------------------------------------------------------- +swift.org +cocoapods.org +cdn.cocoapods.org + +# ---------------------------------------------------------- +# R / CRAN +# ---------------------------------------------------------- +cran.r-project.org +cran.rstudio.com + +# ---------------------------------------------------------- +# Perl +# ---------------------------------------------------------- +cpan.org +metacpan.org + +# ---------------------------------------------------------- +# Other languages / toolchains +# ---------------------------------------------------------- +ziglang.org +nim-lang.org +dlang.org +vlang.io +adoptium.net + +# ---------------------------------------------------------- +# CDNs hosting libraries & assets +# ---------------------------------------------------------- +cdnjs.cloudflare.com +cdn.jsdelivr.net +unpkg.com +esm.sh +esm.run +fonts.googleapis.com +fonts.gstatic.com +ajax.googleapis.com + +# ---------------------------------------------------------- +# IDE & editor extensions +# ---------------------------------------------------------- +marketplace.visualstudio.com +update.code.visualstudio.com +vscode.blob.core.windows.net +az764295.vo.msecnd.net +gallery.vsassets.io +plugins.jetbrains.com +download.jetbrains.com +open-vsx.org + +# ---------------------------------------------------------- +# Linux package repos & mirrors +# ---------------------------------------------------------- +archive.ubuntu.com +security.ubuntu.com +deb.debian.org +security.debian.org +download.opensuse.org +mirrors.fedoraproject.org +dl.fedoraproject.org +centos.org +mirror.centos.org +vault.centos.org +packages.microsoft.com +apt.llvm.org +ppa.launchpad.net + +# ---------------------------------------------------------- +# macOS / Homebrew (if applicable) +# ---------------------------------------------------------- +formulae.brew.sh +homebrew.bintray.com +ghcr.io + +# ---------------------------------------------------------- +# Infrastructure-as-Code & DevOps tooling +# ---------------------------------------------------------- +registry.terraform.io +releases.hashicorp.com +checkpoint-api.hashicorp.com +galaxy.ansible.com +charts.helm.sh +artifacthub.io +kubernetes.io +dl.k8s.io +storage.googleapis.com +get.helm.sh + +# ---------------------------------------------------------- +# CI/CD services +# ---------------------------------------------------------- +circleci.com +app.circleci.com +travis-ci.com +buildkite.com +dev.azure.com +vsrm.dev.azure.com + +# ---------------------------------------------------------- +# Cloud providers (broad — consider narrowing per use case) +# ---------------------------------------------------------- +# AWS +aws.amazon.com +amazonaws.com +elasticbeanstalk.com +cloudfront.net +# Azure +azure.com +azure.microsoft.com +windows.net +login.microsoftonline.com +management.azure.com +# GCP +cloud.google.com +googleapis.com +appspot.com +# Cloudflare +cloudflare.com +workers.dev +pages.dev +# Others +digitalocean.com +heroku.com +herokuapp.com +vercel.app +vercel.com +netlify.app +netlify.com +fly.dev +fly.io +railway.app +render.com +supabase.com +supabase.co + +# ---------------------------------------------------------- +# Database providers (docs, drivers, downloads) +# ---------------------------------------------------------- +postgresql.org +mysql.com +mongodb.com +downloads.mongodb.com +cloud.mongodb.com +redis.io +download.redis.io +elastic.co +artifacts.elastic.co +neo4j.com +mariadb.org +clickhouse.com +cockroachlabs.com +couchdb.apache.org +planetscale.com +neon.tech + +# ---------------------------------------------------------- +# Monitoring, logging & observability +# ---------------------------------------------------------- +sentry.io +datadoghq.com +newrelic.com +grafana.com +grafana.net +prometheus.io +pagerduty.com +honeycomb.io +splunk.com + +# ---------------------------------------------------------- +# Security scanning & vulnerability databases +# ---------------------------------------------------------- +snyk.io +api.snyk.io +sonarcloud.io +sonarqube.org +cve.mitre.org +nvd.nist.gov +osv.dev +security.snyk.io +github.com/advisories + +# ---------------------------------------------------------- +# Documentation & reference +# ---------------------------------------------------------- +stackoverflow.com +cdn.sstatic.net +developer.mozilla.org +devdocs.io +readthedocs.io +readthedocs.org + +# ---------------------------------------------------------- +# TLS / Certificates / OCSP +# ---------------------------------------------------------- +letsencrypt.org +acme-v02.api.letsencrypt.org +ocsp.digicert.com +ocsp.pki.goog +crl.microsoft.com +cacerts.digicert.com +ocsp.comodoca.com +ocsp.sectigo.com +ocsp.usertrust.com + +# ---------------------------------------------------------- +# Project management & communication +# ---------------------------------------------------------- +atlassian.com +atlassian.net +jira.com +trello.com +slack.com +wss-primary.slack.com +discord.com +gateway.discord.gg +notion.so +notion-static.com +linear.app +asana.com +teams.microsoft.com + +# ---------------------------------------------------------- +# Testing & browser automation services +# ---------------------------------------------------------- +browserstack.com +saucelabs.com +cypress.io +download.cypress.io +lambdatest.com +percy.io + +# ---------------------------------------------------------- +# AI coding assistants +# ---------------------------------------------------------- +api.anthropic.com +anthropic.com +claude.ai +api.openai.com +openai.com +copilot.github.com +copilot-proxy.githubusercontent.com +codeium.com +cursor.sh +platform.claude.com +tabnine.com + +# ---------------------------------------------------------- +# Internal registry proxies (uncomment if self-hosted) +# ---------------------------------------------------------- +# jfrog.io +# your-artifactory.example.com +# your-nexus.example.com +# your-verdaccio.example.com + +# ---------------------------------------------------------- +# Optional: Analytics & error tracking (uncomment if needed) +# ---------------------------------------------------------- +# google-analytics.com +# segment.io +# segment.com +# mixpanel.com +# amplitude.com +# bugsnag.com + +# ============================================================ +# END OF LIST +# Remember: This WILL be incomplete. Consider pairing with +# category-based filtering or egress proxy logging rather +# than relying on this as a sole enforcement mechanism. +# ============================================================ diff --git a/docs/plans/EGRESS_POLICY_BREAKDOWN.md b/docs/plans/EGRESS_POLICY_BREAKDOWN.md new file mode 100644 index 00000000..fafe4542 --- /dev/null +++ b/docs/plans/EGRESS_POLICY_BREAKDOWN.md @@ -0,0 +1,75 @@ +# Egress Policy Breakdown + +This is the execution breakdown for shipping egress policy in Booth. + +## Current Status + +- [x] Decision doc created: `docs/implementations/EGRESS_POLICY_PLAN.md` +- [x] Experiments completed (sidecar netns + firewall + Envoy proxy) +- [x] Phase 1 (config parsing + validation) completed +- [x] Runtime orchestration (MVP: Envoy + iptables) +- [ ] Proxy policy generation +- [ ] Enforcement backends +- [ ] Tests + docs + UX polish + +## Milestones + +## Milestone 1 - Config Foundation (MVP start) + +Goal: parse and validate egress configuration without runtime behavior changes. + +Work items: +- Add `--sandboxed` shorthand flag to enable egress defaults. +- Add `[egress]` config model in AppConfig. +- Add AppContext accessors for egress values. +- Validate: + - mode enum + - enforcement enum + - default policy enum + - mutual exclusivity of allowlist vs policy file + - referenced file existence. + +Target files: +- `cli/src/pkg/appctx/app_config.go` +- `cli/src/pkg/appctx/app_context.go` +- `cli/src/pkg/booth/init/initialize_app_context.go` +- tests in `cli/src/pkg/appctx/` and `cli/src/pkg/booth/init/` + +## Milestone 2 - Runtime Wiring (Envoy + iptables only) + +Goal: first working backend pair using sidecar model. + +Work items: +- Reuse DinD sidecar netns if `--dind` is enabled. +- Otherwise create a dedicated egress netns-owner sidecar. +- Launch Envoy sidecar in shared netns. +- Attach workspace container to shared netns. +- Apply iptables enforcement in shared netns. + +## Milestone 3 - Policy Inputs + +Goal: support simple + advanced policy input modes. + +Work items: +- Simple mode: `.booth/sandbox/allowlist.txt` -> generated Envoy config. +- Advanced mode: `.booth/sandbox/envoy.yaml` passthrough. +- Mount policy artifacts read-only. + +## Milestone 4 - Hardening + UX + +Goal: make the feature safe and operable. + +Work items: +- Drop unnecessary capabilities on sidecars. +- Ensure non-privileged runtime for workspace/proxy containers. +- Add status/diagnostic command output. +- Add troubleshooting docs and threat model notes. + +## Milestone 5 - Expand Backends + +Goal: optional alternative engines. + +Work items: +- Add `squid`/`tinyproxy` modes. +- Add optional nftables backend. +- Keep policy/enforcement contracts stable. diff --git a/examples/demo/.booth/Boothfile b/examples/demo/.booth/Boothfile index 6d7f7a79..818559e3 100644 --- a/examples/demo/.booth/Boothfile +++ b/examples/demo/.booth/Boothfile @@ -3,35 +3,35 @@ # Demo booth - showcases multiple languages and tools # Build arguments for version pinning -arg JDK_VERSION=25 -arg PY_VERSION=3.12 -arg GO_VERSION=1.25.4 -arg NODE_MAJOR=20 +# arg JDK_VERSION=25 +# arg PY_VERSION=3.12 +# arg GO_VERSION=1.25.4 +# arg NODE_MAJOR=20 # Languages -setup python ${PY_VERSION} -setup jdk ${JDK_VERSION} -setup go ${GO_VERSION} -setup nodejs ${NODE_MAJOR} +# setup python ${PY_VERSION} +# setup jdk ${JDK_VERSION} +# setup go ${GO_VERSION} +# setup nodejs ${NODE_MAJOR} # Force jbang to cache dependencies (avoid download on first run) -run echo 'class Hi { public static void main(String[] args) { System.out.println("Hi"); }}' | jbang - +# run echo 'class Hi { public static void main(String[] args) { System.out.println("Hi"); }}' | jbang - # Build tools -setup mvn -setup gradle -setup jenv +# setup mvn +# setup gradle +# setup jenv # AI tools -setup antigravity +# setup antigravity setup claude-code -# VS Code extensions -setup java-code-extension -setup go-code-extension +# # VS Code extensions +# setup java-code-extension +# setup go-code-extension -# Jupyter notebooks with language kernels -setup notebook -setup python-nb-kernel -setup bash-nb-kernel -setup java-jjava-nb-kernel +# # Jupyter notebooks with language kernels +# setup notebook +# setup python-nb-kernel +# setup bash-nb-kernel +# setup java-jjava-nb-kernel diff --git a/examples/demo/.booth/Dockerfile b/examples/demo/.booth/Dockerfile deleted file mode 100644 index a013431b..00000000 --- a/examples/demo/.booth/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# syntax=docker/dockerfile:1.7 -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -# Customizations -ARG JDK_VERSION=25 -ARG JDK_VENDOR=temurin -ARG MVN_VERSION=3.9.11 -ARG PY_VERSION=3.12 -ARG GO_VERSION=1.25.4 -ARG NODE_MAJOR=20 - -# Given user is root and /opt/codingbooth/setups is in PATH (from base image) - -RUN python--setup.sh "${PY_VERSION}" -RUN jdk--setup.sh "${JDK_VERSION}" -RUN go--setup.sh "${GO_VERSION}" -RUN nodejs--setup.sh "${NODE_MAJOR}" - -# Force jbang run to not show download afterward. -RUN echo 'class Hi { public static void main(String[] args) { System.out.println("Hi again"); }}' | jbang - - -COPY ./.booth/setups /tmp/setups - -RUN mvn--setup.sh -RUN gradle--setup.sh -RUN jenv--setup.sh - -RUN antigravity--setup.sh -RUN claude-code--setup.sh -RUN /tmp/setups/codex--setup.sh - -RUN java-code-extension--setup.sh -RUN go-code-extension--setup.sh -RUN /tmp/setups/codex-code-extension--setup.sh - -RUN notebook--setup.sh -RUN python-nb-kernel--setup.sh -RUN bash-nb-kernel--setup.sh -RUN java-jjava-nb-kernel--setup.sh diff --git a/examples/demo/.booth/tools/codingbooth.lock b/examples/demo/.booth/tools/codingbooth.lock deleted file mode 100644 index c06b8d7f..00000000 --- a/examples/demo/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:33Z -cache=shared diff --git a/examples/demo/booth b/examples/demo/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/demo/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/demo/claude-code--setup.sh b/examples/demo/claude-code--setup.sh new file mode 100644 index 00000000..2d32627d --- /dev/null +++ b/examples/demo/claude-code--setup.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +set -Eeuo pipefail +trap 'echo "Error on line $LINENO"; exit 1' ERR + +# -------------------------- +# Root setup - installs Claude Code at BUILD time +# Based on the official install script but for system-wide installation +# -------------------------- +[ "$EUID" -eq 0 ] || { echo "Run as root (use sudo)"; exit 1; } + +# This script will always be installed by root. +HOME=/root + +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(dirname "$0")" +source "$SCRIPT_DIR/libs/skip-setup.sh" +if ! "$SCRIPT_DIR/cb-has-desktop.sh"; then + skip_setup "$SCRIPT_NAME" "desktop environment not available" +fi + +# --- Defaults --- +CLAUDE_CODE_VERSION="${1:-latest}" + +STARTUP_FILE="/usr/share/startup.d/70-cb-claude-code--startup.sh" +PROFILE_FILE="/etc/profile.d/70-cb-claude-code--profile.sh" + +# ==== Install Claude Code ==== + +GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" + +# Detect platform (same logic as official install script) +ARCH=$(uname -m) +case "$ARCH" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; +esac + +# Check for musl libc +if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then + PLATFORM="linux-${ARCH}-musl" +else + PLATFORM="linux-${ARCH}" +fi + +echo "Installing Claude Code for ${PLATFORM}..." + +cd /tmp + +# Resolve version (same as official script - always use latest for most up-to-date installer) +echo "Fetching latest version..." +VERSION=$(curl -fsSL "${GCS_BUCKET}/latest") +echo "Version: ${VERSION}" + +# Download manifest and extract checksum +echo "Fetching manifest..." +MANIFEST=$(curl -fsSL "${GCS_BUCKET}/${VERSION}/manifest.json") + +CHECKSUM="" +if command -v jq &>/dev/null; then + CHECKSUM=$(echo "$MANIFEST" | jq -r ".\"${PLATFORM}\".checksum // empty") +else + # Fallback: extract checksum using bash regex (from official script) + MANIFEST_NORMALIZED=$(echo "$MANIFEST" | tr -d '\n\r\t' | sed 's/ \+/ /g') + if [[ $MANIFEST_NORMALIZED =~ \"$PLATFORM\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then + CHECKSUM="${BASH_REMATCH[1]}" + fi +fi + +# Download binary +BINARY_URL="${GCS_BUCKET}/${VERSION}/${PLATFORM}/claude" +BINARY_FILE="claude-${VERSION}-${PLATFORM}" +echo "Downloading from ${BINARY_URL}..." +curl -fsSL -o "$BINARY_FILE" "$BINARY_URL" + +# Verify checksum +if [[ -n "$CHECKSUM" ]]; then + echo "Verifying checksum..." + echo "$CHECKSUM $BINARY_FILE" | sha256sum -c - || { + echo "Checksum verification failed!" + rm -f "$BINARY_FILE" + exit 1 + } +fi + +chmod +x "$BINARY_FILE" + +# Install system-wide (instead of user's ~/.local/bin) +echo "Installing to /usr/local/bin/claude..." +mv "$BINARY_FILE" /usr/local/bin/claude + +# Verify +echo "Verifying installation..." +/usr/local/bin/claude --version || echo "(Version check may require user context)" + +# ---- Create startup file: runs once per container start as normal user ---- +export CLAUDE_CODE_VERSION="$VERSION" +envsubst '$CLAUDE_CODE_VERSION' > "${STARTUP_FILE}" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +# Claude Code startup script +# Ensures config and credentials from cb-home-seed are properly copied + +CB_SEED_DIR="/etc/cb-home-seed/.claude" +CB_SEED_JSON="/etc/cb-home-seed/.claude.json" +CLAUDE_DIR="$HOME/.claude" +CLAUDE_JSON="$HOME/.claude.json" + +mkdir -p "$CLAUDE_DIR" + +# Create symlink at ~/.local/bin/claude -> /usr/local/bin/claude +# Claude Code's native install expects the binary at ~/.local/bin/claude. +# Without this, it errors: "installMethod is native, but claude command not found" +CLAUDE_LOCAL_BIN="$HOME/.local/bin" +mkdir -p "$CLAUDE_LOCAL_BIN" +if [[ ! -e "$CLAUDE_LOCAL_BIN/claude" ]]; then + ln -s /usr/local/bin/claude "$CLAUDE_LOCAL_BIN/claude" +fi + +# Copy .claude.json config file (contains hasCompletedOnboarding, theme, etc.) +# This must happen BEFORE claude runs to skip onboarding wizard +if [[ -f "$CB_SEED_JSON" && ! -f "$CLAUDE_JSON" ]]; then + cp "$CB_SEED_JSON" "$CLAUDE_JSON" +fi + +# Copy .claude/ directory contents (credentials, plugins, etc.) +if [[ -d "$CB_SEED_DIR" ]]; then + # Use rsync if available (better merge), otherwise cp + if command -v rsync &>/dev/null; then + rsync -a --ignore-existing "$CB_SEED_DIR/" "$CLAUDE_DIR/" + else + # Copy files that don't exist in destination + find "$CB_SEED_DIR" -type f | while read -r src; do + rel="${src#$CB_SEED_DIR/}" + dst="$CLAUDE_DIR/$rel" + if [[ ! -f "$dst" ]]; then + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + fi + done + fi +fi +EOF +chmod 755 "${STARTUP_FILE}" + +# ---- Create profile file: sourced at beginning of user shell session ---- +envsubst '$CLAUDE_CODE_VERSION' > "${PROFILE_FILE}" <<'EOF' +# Profile: Claude Code: $CLAUDE_CODE_VERSION +# Installed system-wide in /usr/local/bin - no PATH modification needed +EOF +chmod 644 "${PROFILE_FILE}" + +echo "" +echo "Claude Code installed successfully!" +echo " Version: ${VERSION}" +echo " Binary: /usr/local/bin/claude" +echo " Startup: ${STARTUP_FILE}" +echo " Profile: ${PROFILE_FILE}" +echo "" +echo "Users can run 'claude' directly. Config will be set up on first run." +echo "" +echo "=== Credential Seeding ===" +echo "To reuse credentials from host, add to .booth/config.toml:" +echo "" +echo ' run-args = [' +echo ' # Claude Code config and credentials (home-seeding: may update tokens/session)' +echo ' "-v", "~/.claude.json:/etc/cb-home-seed/.claude.json:ro",' +echo ' "-v", "~/.claude:/etc/cb-home-seed/.claude:ro"' +echo ' ]' +echo "" diff --git a/examples/update-booth.sh b/examples/update-booth.sh index 47c67a1d..e5da4b79 100755 --- a/examples/update-booth.sh +++ b/examples/update-booth.sh @@ -22,42 +22,19 @@ updated=0 # Update booth in immediate subfolders of examples/ for dir in "$SCRIPT_DIR"/*/; do [[ ! -d "$dir" ]] && continue - target="$dir/booth" - if [[ -f "$target" ]]; then - cp "$SOURCE_BOOTH" "$target" - chmod +x "$target" - # Remove lock file to force fresh install - rm -rf "$dir/.booth/tools" - echo " Updated: ${dir#$PROJECT_ROOT/}booth" - - # Run booth install to update tools - cd "$dir" - ./booth install - cd - - echo " Installed tools" - : $((updated++)) - fi + rm -rf "$dir/.booth/tools" + cp ../codingbooth $dir/booth + echo "Installed tools: $dir" + : $((updated++)) done # Update booth in immediate subfolders of examples/workspaces/ if [[ -d "$SCRIPT_DIR/workspaces" ]]; then for dir in "$SCRIPT_DIR/workspaces"/*/; do - [[ ! -d "$dir" ]] && continue - target="$dir/booth" - if [[ -f "$target" ]]; then - cp "$SOURCE_BOOTH" "$target" - chmod +x "$target" - # Remove lock file to force fresh install - rm -rf "$dir/.booth/tools" - echo " Updated: ${dir#$PROJECT_ROOT/}booth" - - # Run booth install to update tools - cd "$dir" - ./booth install - cd - - echo " Installed tools" - : $((updated++)) - fi + rm -rf "$dir/.booth/tools" + cp ../codingbooth $dir/booth + echo "Installed tools: $dir" + : $((updated++)) done fi diff --git a/examples/workspaces/all-java-example/.booth/tools/codingbooth.lock b/examples/workspaces/all-java-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 0bf62d94..00000000 --- a/examples/workspaces/all-java-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:36Z -cache=shared diff --git a/examples/workspaces/all-java-example/.cb-tests/test002-jenv-on-host.sh b/examples/workspaces/all-java-example/.cb-tests/test002-jenv-on-host.sh deleted file mode 100755 index 3b818116..00000000 --- a/examples/workspaces/all-java-example/.cb-tests/test002-jenv-on-host.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - -# -# Test that jenv is properly installed and configured -# - -set -euo pipefail - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' # No Color - -echo "=== Testing jenv Installation ===" -echo "" - -failed=0 - -# Test 1: jenv versions runs successfully -echo "Testing 'jenv versions'..." -if output=$("../../../codingbooth" --variant base -- 'jenv versions' 2>&1); then - echo "$output" - echo "" - echo -e "${GREEN}✓${NC} 'jenv versions' completed successfully" -else - echo -e "${RED}✗${NC} 'jenv versions' failed" - failed=1 -fi -echo "" - -# Test 2: jenv version returns current version -echo "Testing 'jenv version'..." -if version_output=$("../../../codingbooth" --variant base --port 10100 -- 'jenv version' 2>&1); then - echo "$version_output" - # Check that output contains a version number pattern (e.g., "25" or "25.0.1") - if echo "$version_output" | grep -qE '[0-9]+(\.[0-9]+)?'; then - echo -e "${GREEN}✓${NC} 'jenv version' returned a valid version" - else - echo -e "${RED}✗${NC} 'jenv version' output doesn't contain a version number" - failed=1 - fi -else - echo -e "${RED}✗${NC} 'jenv version' failed" - failed=1 -fi - -echo "" -if [ $failed -eq 0 ]; then - echo -e "${GREEN}All jenv checks passed!${NC}" -else - echo -e "${RED}jenv checks FAILED!${NC}" - exit 1 -fi diff --git a/examples/workspaces/all-java-example/booth b/examples/workspaces/all-java-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/all-java-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/aws-example/.booth/tools/codingbooth.lock b/examples/workspaces/aws-example/.booth/tools/codingbooth.lock deleted file mode 100644 index a38437f5..00000000 --- a/examples/workspaces/aws-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:37Z -cache=shared diff --git a/examples/workspaces/aws-example/booth b/examples/workspaces/aws-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/aws-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/boothfile-example/.booth/tools/codingbooth.lock b/examples/workspaces/boothfile-example/.booth/tools/codingbooth.lock deleted file mode 100644 index cf3ebe9f..00000000 --- a/examples/workspaces/boothfile-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:39Z -cache=shared diff --git a/examples/workspaces/boothfile-example/booth b/examples/workspaces/boothfile-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/boothfile-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/bun-example/.booth/tools/codingbooth.lock b/examples/workspaces/bun-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 683eb036..00000000 --- a/examples/workspaces/bun-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:41Z -cache=shared diff --git a/examples/workspaces/bun-example/.cb-tests/test001-use-bun-installed-app--on-host.sh b/examples/workspaces/bun-example/.cb-tests/test001-use-bun-installed-app--on-host.sh index 85265d44..4bea82a5 100755 --- a/examples/workspaces/bun-example/.cb-tests/test001-use-bun-installed-app--on-host.sh +++ b/examples/workspaces/bun-example/.cb-tests/test001-use-bun-installed-app--on-host.sh @@ -10,8 +10,12 @@ GREEN='\033[0;32m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$SCRIPT_DIR/.." -BOOTH="$REPO_ROOT/booth" +REPO_ROOT="$SCRIPT_DIR/../../../.." +if [ -x "$REPO_ROOT/codingbooth" ]; then + BOOTH="$REPO_ROOT/codingbooth" +else + BOOTH="$REPO_ROOT/booth" +fi "$BOOTH" --variant base --port 12000 -- "./.cb-tests/inBooth--run-all-tests.sh" 2>&1 | tee "$0.out" diff --git a/examples/workspaces/bun-example/booth b/examples/workspaces/bun-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/bun-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/conda-example/.booth/tools/codingbooth.lock b/examples/workspaces/conda-example/.booth/tools/codingbooth.lock deleted file mode 100644 index f39b78f1..00000000 --- a/examples/workspaces/conda-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:42Z -cache=shared diff --git a/examples/workspaces/conda-example/booth b/examples/workspaces/conda-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/conda-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/deno-example/.booth/tools/codingbooth.lock b/examples/workspaces/deno-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 6fe43195..00000000 --- a/examples/workspaces/deno-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:44Z -cache=shared diff --git a/examples/workspaces/deno-example/booth b/examples/workspaces/deno-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/deno-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/dind-example/.booth/tools/codingbooth.lock b/examples/workspaces/dind-example/.booth/tools/codingbooth.lock deleted file mode 100644 index df10645b..00000000 --- a/examples/workspaces/dind-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:45Z -cache=shared diff --git a/examples/workspaces/dind-example/booth b/examples/workspaces/dind-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/dind-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/elixir-example/.booth/tools/codingbooth.lock b/examples/workspaces/elixir-example/.booth/tools/codingbooth.lock deleted file mode 100644 index d4947e93..00000000 --- a/examples/workspaces/elixir-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:48Z -cache=shared diff --git a/examples/workspaces/elixir-example/booth b/examples/workspaces/elixir-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/elixir-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/empty-example/.booth/tools/codingbooth.lock b/examples/workspaces/empty-example/.booth/tools/codingbooth.lock deleted file mode 100644 index c9576441..00000000 --- a/examples/workspaces/empty-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:49Z -cache=shared diff --git a/examples/workspaces/empty-example/booth b/examples/workspaces/empty-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/empty-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/firebase-example/.booth/tools/codingbooth.lock b/examples/workspaces/firebase-example/.booth/tools/codingbooth.lock deleted file mode 100644 index c8844978..00000000 --- a/examples/workspaces/firebase-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:51Z -cache=shared diff --git a/examples/workspaces/firebase-example/booth b/examples/workspaces/firebase-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/firebase-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/gcloud-example/.booth/tools/codingbooth.lock b/examples/workspaces/gcloud-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 1d87ad8b..00000000 --- a/examples/workspaces/gcloud-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:52Z -cache=shared diff --git a/examples/workspaces/gcloud-example/booth b/examples/workspaces/gcloud-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/gcloud-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/go-example/.booth/tools/codingbooth.lock b/examples/workspaces/go-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 1f23a035..00000000 --- a/examples/workspaces/go-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:54Z -cache=shared diff --git a/examples/workspaces/go-example/booth b/examples/workspaces/go-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/go-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/haskell-example/.booth/tools/codingbooth.lock b/examples/workspaces/haskell-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 58addbc3..00000000 --- a/examples/workspaces/haskell-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:56Z -cache=shared diff --git a/examples/workspaces/haskell-example/booth b/examples/workspaces/haskell-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/haskell-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/homebrew-example/.booth/tools/codingbooth.lock b/examples/workspaces/homebrew-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 6346df5d..00000000 --- a/examples/workspaces/homebrew-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:57Z -cache=shared diff --git a/examples/workspaces/homebrew-example/booth b/examples/workspaces/homebrew-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/homebrew-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/java-example/.booth/tools/codingbooth.lock b/examples/workspaces/java-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 86081643..00000000 --- a/examples/workspaces/java-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:52:59Z -cache=shared diff --git a/examples/workspaces/java-example/booth b/examples/workspaces/java-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/java-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/jetbrain-exmple/.booth/tools/codingbooth.lock b/examples/workspaces/jetbrain-exmple/.booth/tools/codingbooth.lock deleted file mode 100644 index 1d98c0e9..00000000 --- a/examples/workspaces/jetbrain-exmple/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:01Z -cache=shared diff --git a/examples/workspaces/jetbrain-exmple/booth b/examples/workspaces/jetbrain-exmple/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/jetbrain-exmple/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/js-example/.booth/tools/codingbooth.lock b/examples/workspaces/js-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 21940cb0..00000000 --- a/examples/workspaces/js-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:02Z -cache=shared diff --git a/examples/workspaces/js-example/.cb-tests/test-on-container.sh b/examples/workspaces/js-example/.cb-tests/test-on-container.sh index 4dc23995..daced24e 100755 --- a/examples/workspaces/js-example/.cb-tests/test-on-container.sh +++ b/examples/workspaces/js-example/.cb-tests/test-on-container.sh @@ -56,11 +56,14 @@ else fi # Test Deno +# NOTE: Deno detection is kept but server test is disabled (HAS_DENO stays false). +# Deno's HTTP server startup is intermittent in CI — it sometimes fails to bind +# the port in time, causing flaky test failures unrelated to the workspace setup. echo "Checking Deno installation..." if deno --version > /dev/null 2>&1; then DENO_VERSION=$(deno --version | head -1) pass "Deno installed: $DENO_VERSION" - HAS_DENO=true + # HAS_DENO=true else skip "Deno" fi diff --git a/examples/workspaces/js-example/booth b/examples/workspaces/js-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/js-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/kind-app-example/.booth/tools/codingbooth.lock b/examples/workspaces/kind-app-example/.booth/tools/codingbooth.lock deleted file mode 100644 index db3a9a3c..00000000 --- a/examples/workspaces/kind-app-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:04Z -cache=shared diff --git a/examples/workspaces/kind-app-example/booth b/examples/workspaces/kind-app-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/kind-app-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/kind-example/.booth/tools/codingbooth.lock b/examples/workspaces/kind-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 347c1daa..00000000 --- a/examples/workspaces/kind-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:05Z -cache=shared diff --git a/examples/workspaces/kind-example/booth b/examples/workspaces/kind-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/kind-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/kotlin-example/.booth/tools/codingbooth.lock b/examples/workspaces/kotlin-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 578758e8..00000000 --- a/examples/workspaces/kotlin-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:07Z -cache=shared diff --git a/examples/workspaces/kotlin-example/booth b/examples/workspaces/kotlin-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/kotlin-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/neovim-example/.booth/tools/codingbooth.lock b/examples/workspaces/neovim-example/.booth/tools/codingbooth.lock deleted file mode 100644 index c5994f5f..00000000 --- a/examples/workspaces/neovim-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:09Z -cache=shared diff --git a/examples/workspaces/neovim-example/.cb-tests/test001-neovim-exists-on-host.sh b/examples/workspaces/neovim-example/.cb-tests/test001-neovim-exists-on-host.sh index a28eb54b..203d7553 100755 --- a/examples/workspaces/neovim-example/.cb-tests/test001-neovim-exists-on-host.sh +++ b/examples/workspaces/neovim-example/.cb-tests/test001-neovim-exists-on-host.sh @@ -15,12 +15,18 @@ GREEN='\033[0;32m' NC='\033[0m' # No Color SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR/../../../.." +if [ -x "$REPO_ROOT/codingbooth" ]; then + BOOTH="$REPO_ROOT/codingbooth" +else + BOOTH="$REPO_ROOT/booth" +fi echo "=== Testing Neovim Exists ===" echo "" # Capture nvim --version output -output=$("$SCRIPT_DIR/../../../../booth" --variant base --port 27000 -- 'nvim --version' 2>&1) +output=$("$BOOTH" --variant base --port 27000 -- 'nvim --version' 2>&1) echo "$output" echo "" diff --git a/examples/workspaces/neovim-example/booth b/examples/workspaces/neovim-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/neovim-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/npm-example/.booth/tools/codingbooth.lock b/examples/workspaces/npm-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 9e8c62d9..00000000 --- a/examples/workspaces/npm-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:10Z -cache=shared diff --git a/examples/workspaces/npm-example/booth b/examples/workspaces/npm-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/npm-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/php-example/.booth/tools/codingbooth.lock b/examples/workspaces/php-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 86a1c268..00000000 --- a/examples/workspaces/php-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:11Z -cache=shared diff --git a/examples/workspaces/php-example/booth b/examples/workspaces/php-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/php-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/pip-example/.booth/tools/codingbooth.lock b/examples/workspaces/pip-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 224729fd..00000000 --- a/examples/workspaces/pip-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:13Z -cache=shared diff --git a/examples/workspaces/pip-example/booth b/examples/workspaces/pip-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/pip-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/python-example/.booth/tools/codingbooth.lock b/examples/workspaces/python-example/.booth/tools/codingbooth.lock deleted file mode 100644 index ded69729..00000000 --- a/examples/workspaces/python-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:14Z -cache=shared diff --git a/examples/workspaces/python-example/.cb-tests/test001-python-version-on-host.sh b/examples/workspaces/python-example/.cb-tests/test001-python-version-on-host.sh index 3100236e..2cbd0e0c 100755 --- a/examples/workspaces/python-example/.cb-tests/test001-python-version-on-host.sh +++ b/examples/workspaces/python-example/.cb-tests/test001-python-version-on-host.sh @@ -15,12 +15,18 @@ GREEN='\033[0;32m' NC='\033[0m' # No Color SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR/../../../.." +if [ -x "$REPO_ROOT/codingbooth" ]; then + BOOTH="$REPO_ROOT/codingbooth" +else + BOOTH="$REPO_ROOT/booth" +fi echo "=== Testing Python Version (default 3.12) ===" echo "" # Capture python --version output -output=$("$SCRIPT_DIR/../../../../booth" --variant base --port 31000 -- 'python --version' 2>&1) +output=$("$BOOTH" --variant base --port 31000 -- 'python --version' 2>&1) echo "$output" echo "" diff --git a/examples/workspaces/python-example/.cb-tests/test002-python-version-override-on-host.sh b/examples/workspaces/python-example/.cb-tests/test002-python-version-override-on-host.sh index 0ead467d..3d84661e 100755 --- a/examples/workspaces/python-example/.cb-tests/test002-python-version-override-on-host.sh +++ b/examples/workspaces/python-example/.cb-tests/test002-python-version-override-on-host.sh @@ -15,12 +15,18 @@ GREEN='\033[0;32m' NC='\033[0m' # No Color SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR/../../../.." +if [ -x "$REPO_ROOT/codingbooth" ]; then + BOOTH="$REPO_ROOT/codingbooth" +else + BOOTH="$REPO_ROOT/booth" +fi echo "=== Testing Python Version Override (3.13 via --build-arg) ===" echo "" # Capture python --version output with build-arg override -output=$("$SCRIPT_DIR/../../../../booth" --variant base --port 31100 --build-arg PY_VERSION=3.13 -- 'python --version' 2>&1) +output=$("$BOOTH" --variant base --port 31100 --build-arg PY_VERSION=3.13 -- 'python --version' 2>&1) echo "$output" echo "" diff --git a/examples/workspaces/python-example/booth b/examples/workspaces/python-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/python-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/ruby-example/.booth/tools/codingbooth.lock b/examples/workspaces/ruby-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 4d17bfb1..00000000 --- a/examples/workspaces/ruby-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:16Z -cache=shared diff --git a/examples/workspaces/ruby-example/booth b/examples/workspaces/ruby-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/ruby-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/rust-example/.booth/Dockerfile b/examples/workspaces/rust-example/.booth/Dockerfile deleted file mode 100644 index f88ee44a..00000000 --- a/examples/workspaces/rust-example/.booth/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -# syntax=docker/dockerfile:1.7 -# Rust Example Dockerfile -# Installs Rust and popular crates on top of the base workspace image - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -RUN rust--setup.sh -RUN cargo--install.sh ripgrep fd-find bat diff --git a/examples/workspaces/rust-example/.booth/tools/codingbooth.lock b/examples/workspaces/rust-example/.booth/tools/codingbooth.lock deleted file mode 100644 index e771be32..00000000 --- a/examples/workspaces/rust-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:18Z -cache=shared diff --git a/examples/workspaces/rust-example/booth b/examples/workspaces/rust-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/rust-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/sandbox-allowlist-extra-example/.booth/Boothfile b/examples/workspaces/sandbox-allowlist-extra-example/.booth/Boothfile new file mode 100644 index 00000000..ba97ce48 --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/.booth/Boothfile @@ -0,0 +1,3 @@ +# syntax=codingbooth/boothfile:1 + +setup python 3.13 diff --git a/examples/workspaces/sandbox-allowlist-extra-example/.booth/config.toml b/examples/workspaces/sandbox-allowlist-extra-example/.booth/config.toml new file mode 100644 index 00000000..90d4c449 --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/.booth/config.toml @@ -0,0 +1,7 @@ +variant = "base" +sandboxed = true + +sandbox-allowlist-file = ".booth/sandbox/allowlist.txt" +sandbox-allowlist = [ + "example.com" +] diff --git a/examples/workspaces/sandbox-allowlist-extra-example/.booth/sandbox/allowlist.txt b/examples/workspaces/sandbox-allowlist-extra-example/.booth/sandbox/allowlist.txt new file mode 100644 index 00000000..71ffd043 --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/.booth/sandbox/allowlist.txt @@ -0,0 +1,2 @@ +# Domains allowed by sandbox policy (base list) +pypi.org diff --git a/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/tags.txt b/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/tags.txt new file mode 100644 index 00000000..ad336995 --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/tags.txt @@ -0,0 +1,2 @@ +sandbox +security diff --git a/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/test-on-container.sh b/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/test-on-container.sh new file mode 100755 index 00000000..27a9a11a --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/test-on-container.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +set -euo pipefail + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' + +pass() { echo -e "${GREEN}✓${NC} $1"; } +fail() { echo -e "${RED}✗${NC} $1"; exit 1; } +info() { echo -e "${YELLOW}ℹ${NC} $1"; } + +echo "=== Sandbox Allowlist + Extra Container Tests ===" + +echo +info "Check proxy env vars are present" +[[ -n "${HTTP_PROXY:-}" ]] || fail "HTTP_PROXY is not set" +[[ -n "${HTTPS_PROXY:-}" ]] || fail "HTTPS_PROXY is not set" +pass "Proxy environment variables are set" + +echo +info "Allowed domain via explicit proxy: pypi.org (from allowlist file)" +if curl -4 -I -sS --max-time 12 -x http://127.0.0.1:15001 https://pypi.org >/tmp/firewall-allow.out; then + pass "pypi.org is reachable via sandbox proxy" +else + fail "pypi.org should be reachable via sandbox proxy" +fi + +echo +info "Allowed domain via explicit proxy: example.com (from sandbox-allowlist)" +connect_code=$(curl -4 -sS --max-time 12 -o /dev/null -w '%{http_connect}' -x http://127.0.0.1:15001 https://example.com 2>/dev/null || true) +if [[ "$connect_code" == "403" ]]; then + fail "example.com blocked by proxy (CONNECT $connect_code) — should be allowed via sandbox-allowlist" +else + pass "example.com allowed by sandbox proxy (CONNECT $connect_code)" +fi + +echo +info "Blocked domain via explicit proxy: reddit.com" +if curl -4 -I -sS --max-time 12 -x http://127.0.0.1:15001 https://reddit.com >/tmp/firewall-blocked.out; then + fail "reddit.com should be blocked by proxy policy" +else + pass "reddit.com blocked by proxy policy (expected)" +fi + +echo +info "Direct no-proxy bypass attempt should fail" +if HTTPS_PROXY= HTTP_PROXY= curl -4 -I -sS --max-time 8 https://google.com >/tmp/firewall-bypass.out; then + fail "direct bypass should be blocked by firewall" +else + pass "direct bypass blocked by firewall (expected)" +fi + +echo +if [[ -n "${DOCKER_HOST:-}" ]]; then + info "DinD mode checks" + [[ "${DOCKER_HOST}" == "tcp://localhost:2375" ]] || fail "DOCKER_HOST should be tcp://localhost:2375 in DinD mode" + pass "DOCKER_HOST is correctly configured for DinD reuse" + + if docker version >/tmp/firewall-dind-version.out 2>&1; then + pass "docker command works against DinD daemon" + else + info "docker command did not complete in DinD mode (acceptable for this egress test)" + fi +else + info "Non-DinD mode checks" + pass "DOCKER_HOST not set in non-DinD mode" +fi + +echo +echo -e "${GREEN}=== All container tests passed! ===${NC}" diff --git a/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/test001-sandbox-egress-on-host.sh b/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/test001-sandbox-egress-on-host.sh new file mode 100755 index 00000000..3cd7358a --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/.cb-tests/test001-sandbox-egress-on-host.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +set -euo pipefail + +cd "$(dirname "$0")/.." + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +pass() { echo -e "${GREEN}✓${NC} $1"; } +fail() { echo -e "${RED}✗${NC} $1"; exit 1; } + +echo "=== test001: sandbox allowlist + extra (non-DinD) ===" + +if ../../../codingbooth --variant base --version latest --port 35210 --name sandbox-allowlist-extra-example --sandboxed -- ./.cb-tests/test-on-container.sh; then + pass "Sandbox non-DinD test passed" +else + fail "Sandbox non-DinD test failed" +fi diff --git a/examples/workspaces/sandbox-allowlist-extra-example/README.md b/examples/workspaces/sandbox-allowlist-extra-example/README.md new file mode 100644 index 00000000..0ec73929 --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/README.md @@ -0,0 +1,48 @@ +# Sandbox Allowlist + Extra Example + +This example demonstrates `--sandboxed` with: +- a file-based allowlist, and +- extra domains appended via `sandbox-allowlist`. + +**Security note (2026-02-06):** `--sandboxed` with `--dind` is **not supported**. +DinD can bypass the egress firewall by running a privileged container in the shared network namespace. Use `--sandboxed` **without** `--dind` until further research. + +The policy is read from `.booth/sandbox/allowlist.txt` and merged with `sandbox-allowlist` in `.booth/config.toml`. +Enforcement uses: + +- Envoy forward proxy policy (domain allowlist) +- iptables egress rules (force traffic through proxy) + +## Quick run + +```bash +cd examples/workspaces/sandbox-allowlist-extra-example +./booth +``` + +### Files to look at + +- `.booth/config.toml` — shows `sandbox-allowlist` extra entries +- `.booth/sandbox/allowlist.txt` — base allowlist file + +### Example behavior (inside container) + +```bash +# allowed (from file) +curl -I -x http://127.0.0.1:15001 https://pypi.org + +# allowed (from sandbox-allowlist extra) +curl -I -x http://127.0.0.1:15001 https://example.com + +# blocked by policy +curl -I -x http://127.0.0.1:15001 https://reddit.com + +# direct bypass blocked by firewall +HTTPS_PROXY= HTTP_PROXY= curl -I --max-time 8 https://google.com +``` + +## Run tests + +```bash +./run-automatic-on-host-test.sh +``` diff --git a/examples/workspaces/sandbox-allowlist-extra-example/run-automatic-on-host-test.sh b/examples/workspaces/sandbox-allowlist-extra-example/run-automatic-on-host-test.sh new file mode 100755 index 00000000..ee84d2cc --- /dev/null +++ b/examples/workspaces/sandbox-allowlist-extra-example/run-automatic-on-host-test.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +cd "$(dirname "$0")" + +failed=0 +failed_tests=() +total_tests=0 + +for f in .cb-tests/test0*.sh ; do + [ -f "$f" ] || continue + test_name="$(basename "$f")" + echo "$test_name" + total_tests=$((total_tests + 1)) + + if ! ./$f; then + failed=1 + failed_tests+=("$test_name") + fi + echo "" +done + +num_failed=${#failed_tests[@]} + +if [ $failed -eq 0 ]; then + echo "All $total_tests tests passed." +else + echo "$num_failed out of $total_tests tests FAILED." + echo "Failed tests:" + for t in "${failed_tests[@]}"; do + echo " - $t" + done +fi + +exit $failed diff --git a/examples/workspaces/sandbox-envoy-example/.booth/Boothfile b/examples/workspaces/sandbox-envoy-example/.booth/Boothfile new file mode 100644 index 00000000..ce51ee6b --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.booth/Boothfile @@ -0,0 +1,3 @@ +# syntax=codingbooth/boothfile:1 + +setup claude-code diff --git a/examples/workspaces/sandbox-envoy-example/.booth/config.toml b/examples/workspaces/sandbox-envoy-example/.booth/config.toml new file mode 100644 index 00000000..28283a36 --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.booth/config.toml @@ -0,0 +1,4 @@ +sandboxed = true +variant = "base" + +sandbox-policy-file = ".booth/sandbox/envoy.yaml" diff --git a/examples/workspaces/sandbox-envoy-example/.booth/sandbox/allowlist.txt b/examples/workspaces/sandbox-envoy-example/.booth/sandbox/allowlist.txt new file mode 100644 index 00000000..6a1d21d9 --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.booth/sandbox/allowlist.txt @@ -0,0 +1,3 @@ +# This example uses sandbox-policy-file with envoy.yaml. +# The allowlist file is not used here. + diff --git a/examples/workspaces/sandbox-envoy-example/.booth/sandbox/envoy.yaml b/examples/workspaces/sandbox-envoy-example/.booth/sandbox/envoy.yaml new file mode 100644 index 00000000..14d2649a --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.booth/sandbox/envoy.yaml @@ -0,0 +1,77 @@ +static_resources: + listeners: + - name: egress_proxy + address: + socket_address: + address: 0.0.0.0 + port_value: 15001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + codec_type: AUTO + route_config: + name: proxy_routes + virtual_hosts: + - name: forward_proxy + domains: ["*"] + routes: + - match: + connect_matcher: {} + route: + cluster: dynamic_forward_proxy_cluster + upgrade_configs: + - upgrade_type: CONNECT + connect_config: {} + - match: + prefix: "/" + redirect: + https_redirect: true + http_filters: + - name: envoy.filters.http.rbac + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: ALLOW + policies: + allow_pypi: + permissions: + - header: + name: ":authority" + string_match: + safe_regex: + google_re2: {} + regex: "(^|.*\\.)pypi\\.org(:[0-9]+)?$" + principals: + - any: true + - name: envoy.filters.http.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + upgrade_configs: + - upgrade_type: CONNECT + + clusters: + - name: dynamic_forward_proxy_cluster + connect_timeout: 5s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + +admin: + address: + socket_address: + address: 127.0.0.1 + port_value: 9901 diff --git a/examples/workspaces/sandbox-envoy-example/.cb-tests/tags.txt b/examples/workspaces/sandbox-envoy-example/.cb-tests/tags.txt new file mode 100644 index 00000000..ad336995 --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.cb-tests/tags.txt @@ -0,0 +1,2 @@ +sandbox +security diff --git a/examples/workspaces/sandbox-envoy-example/.cb-tests/test-on-container.sh b/examples/workspaces/sandbox-envoy-example/.cb-tests/test-on-container.sh new file mode 100755 index 00000000..29bde344 --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.cb-tests/test-on-container.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +set -euo pipefail + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' + +pass() { echo -e "${GREEN}✓${NC} $1"; } +fail() { echo -e "${RED}✗${NC} $1"; exit 1; } +info() { echo -e "${YELLOW}ℹ${NC} $1"; } + +echo "=== Sandbox Envoy Policy Container Tests ===" + +echo +info "Check proxy env vars are present" +[[ -n "${HTTP_PROXY:-}" ]] || fail "HTTP_PROXY is not set" +[[ -n "${HTTPS_PROXY:-}" ]] || fail "HTTPS_PROXY is not set" +pass "Proxy environment variables are set" + +echo +info "Allowed domain via explicit proxy: pypi.org" +if curl -4 -I -sS --max-time 12 -x http://127.0.0.1:15001 https://pypi.org >/tmp/firewall-allow.out; then + pass "pypi.org is reachable via sandbox proxy" +else + fail "pypi.org should be reachable via sandbox proxy" +fi + +echo +info "Blocked domain via explicit proxy: example.com" +if curl -4 -I -sS --max-time 12 -x http://127.0.0.1:15001 https://example.com >/tmp/firewall-blocked.out; then + fail "example.com should be blocked by proxy policy" +else + pass "example.com blocked by proxy policy (expected)" +fi + +echo +info "Direct no-proxy bypass attempt should fail" +if HTTPS_PROXY= HTTP_PROXY= curl -4 -I -sS --max-time 8 https://google.com >/tmp/firewall-bypass.out; then + fail "direct bypass should be blocked by firewall" +else + pass "direct bypass blocked by firewall (expected)" +fi + +echo +if [[ -n "${DOCKER_HOST:-}" ]]; then + info "DinD mode checks" + [[ "${DOCKER_HOST}" == "tcp://localhost:2375" ]] || fail "DOCKER_HOST should be tcp://localhost:2375 in DinD mode" + pass "DOCKER_HOST is correctly configured for DinD reuse" + + if docker version >/tmp/firewall-dind-version.out 2>&1; then + pass "docker command works against DinD daemon" + else + info "docker command did not complete in DinD mode (acceptable for this egress test)" + fi +else + info "Non-DinD mode checks" + pass "DOCKER_HOST not set in non-DinD mode" +fi + +echo +echo -e "${GREEN}=== All container tests passed! ===${NC}" diff --git a/examples/workspaces/sandbox-envoy-example/.cb-tests/test001-sandbox-egress-on-host.sh b/examples/workspaces/sandbox-envoy-example/.cb-tests/test001-sandbox-egress-on-host.sh new file mode 100755 index 00000000..c4300305 --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/.cb-tests/test001-sandbox-egress-on-host.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +set -euo pipefail + +cd "$(dirname "$0")/.." + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +pass() { echo -e "${GREEN}✓${NC} $1"; } +fail() { echo -e "${RED}✗${NC} $1"; exit 1; } + +echo "=== test001: sandbox envoy policy (non-DinD) ===" + +if ../../../codingbooth --variant base --version latest --port 35220 --name sandbox-envoy-example --sandboxed -- ./.cb-tests/test-on-container.sh; then + pass "Sandbox non-DinD test passed" +else + fail "Sandbox non-DinD test failed" +fi diff --git a/examples/workspaces/sandbox-envoy-example/README.md b/examples/workspaces/sandbox-envoy-example/README.md new file mode 100644 index 00000000..948111b4 --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/README.md @@ -0,0 +1,42 @@ +# Sandbox Envoy Policy Example + +This example demonstrates `--sandboxed` with a custom Envoy policy file. + +**Security note (2026-02-06):** `--sandboxed` with `--dind` is **not supported**. +DinD can bypass the egress firewall by running a privileged container in the shared network namespace. Use `--sandboxed` **without** `--dind` until further research. + +The policy is read from `.booth/sandbox/envoy.yaml` and enforced by: + +- Envoy forward proxy policy (domain allowlist) +- iptables egress rules (force traffic through proxy) + +## Quick run + +```bash +cd examples/workspaces/sandbox-envoy-example +./booth +``` + +### Files to look at + +- `.booth/config.toml` — uses `sandbox-policy-file` +- `.booth/sandbox/envoy.yaml` — custom Envoy RBAC policy + +### Example behavior (inside container) + +```bash +# allowed (by custom envoy.yaml) +curl -I -x http://127.0.0.1:15001 https://pypi.org + +# blocked by policy +curl -I -x http://127.0.0.1:15001 https://example.com + +# direct bypass blocked by firewall +HTTPS_PROXY= HTTP_PROXY= curl -I --max-time 8 https://google.com +``` + +## Run tests + +```bash +./run-automatic-on-host-test.sh +``` diff --git a/examples/workspaces/sandbox-envoy-example/run-automatic-on-host-test.sh b/examples/workspaces/sandbox-envoy-example/run-automatic-on-host-test.sh new file mode 100755 index 00000000..ee84d2cc --- /dev/null +++ b/examples/workspaces/sandbox-envoy-example/run-automatic-on-host-test.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +cd "$(dirname "$0")" + +failed=0 +failed_tests=() +total_tests=0 + +for f in .cb-tests/test0*.sh ; do + [ -f "$f" ] || continue + test_name="$(basename "$f")" + echo "$test_name" + total_tests=$((total_tests + 1)) + + if ! ./$f; then + failed=1 + failed_tests+=("$test_name") + fi + echo "" +done + +num_failed=${#failed_tests[@]} + +if [ $failed -eq 0 ]; then + echo "All $total_tests tests passed." +else + echo "$num_failed out of $total_tests tests FAILED." + echo "Failed tests:" + for t in "${failed_tests[@]}"; do + echo " - $t" + done +fi + +exit $failed diff --git a/examples/workspaces/server-example/.booth/tools/codingbooth.lock b/examples/workspaces/server-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 40ad541d..00000000 --- a/examples/workspaces/server-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:19Z -cache=shared diff --git a/examples/workspaces/server-example/booth b/examples/workspaces/server-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/server-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/urlwhitelist-example/.booth/tools/codingbooth.lock b/examples/workspaces/urlwhitelist-example/.booth/tools/codingbooth.lock deleted file mode 100644 index 2b04ce1e..00000000 --- a/examples/workspaces/urlwhitelist-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:21Z -cache=shared diff --git a/examples/workspaces/urlwhitelist-example/booth b/examples/workspaces/urlwhitelist-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/urlwhitelist-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/examples/workspaces/zig-example/.booth/tools/codingbooth.lock b/examples/workspaces/zig-example/.booth/tools/codingbooth.lock deleted file mode 100644 index c7ab5c3b..00000000 --- a/examples/workspaces/zig-example/.booth/tools/codingbooth.lock +++ /dev/null @@ -1,3 +0,0 @@ -version=0.16.0 -downloaded_at=2026-02-04T21:53:22Z -cache=shared diff --git a/examples/workspaces/zig-example/booth b/examples/workspaces/zig-example/booth deleted file mode 100755 index 84e09673..00000000 --- a/examples/workspaces/zig-example/booth +++ /dev/null @@ -1,960 +0,0 @@ -#!/bin/bash -# Copyright 2025-2026 : Nawa Manusitthipol -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. - - -# CodingBooth Wrapper (booth) -# Downloads, verifies, and runs the platform-specific CodingBooth binary. -# Install: curl -fsSL https://github.com/NawaMan/WorkSpace/releases/download/latest/booth | bash - -set -euo pipefail -trap 'status=$?; echo "❌ Error on line $LINENO (exit $status)" >&2; exit "$status"' ERR - -# --- PIPE INSTALL DETECTION --- -# Detect if running via pipe (curl ... | bash) -# When piped, $0 is the shell name, not a script path -if [[ "$0" == "bash" || "$0" == "-bash" || "$0" == "/bin/bash" || \ - "$0" == "sh" || "$0" == "-sh" || "$0" == "/bin/sh" || \ - "$0" == "zsh" || "$0" == "-zsh" || "$0" == "/bin/zsh" ]]; then - echo "Installing CodingBooth wrapper..." - curl -fsSL -o booth https://github.com/NawaMan/WorkSpace/releases/download/latest/booth - chmod +x booth - echo "" - echo "" - echo "" - ./booth install - echo "" - echo "✅ CodingBooth has been installed." - echo "" - echo "" - echo "" - echo "Run './booth help' to see available commands." - echo "" - ./booth help - exit 0 -fi - -VERSION=0.8.0 -VERBOSE="${VERBOSE:-true}" - -# --- NESTED BOOTH DETECTION --- -# Detect if running inside a CodingBooth container and prevent accidental nested execution. -# Users can explicitly allow nested execution by setting BOOTH_IN_BOOTH=true and using a different port. -detect_nested_booth() { - # Check if we're inside a CodingBooth container - # Markers: /opt/codingbooth/ directory exists OR BOOTH_CONTAINER_NAME is set - if [[ ! -d "/opt/codingbooth" && -z "${BOOTH_CONTAINER_NAME:-}" ]]; then - return 0 # Not inside a container, continue normally - fi - - # We're inside a CodingBooth container - if [[ "${BOOTH_IN_BOOTH:-}" != "true" ]]; then - cat >&2 <<'EOF' -╔═══════════════════════════════════════════════════════════════════════════╗ -║ Running booth inside a booth container ║ -╠═══════════════════════════════════════════════════════════════════════════╣ -║ You appear to be running 'booth' from inside a CodingBooth container. ║ -║ This is usually accidental - the booth script is visible here because ║ -║ your project folder is mounted at /home/coder/code. ║ -║ ║ -║ If you intentionally want to run a nested booth (booth-in-booth), set: ║ -║ ║ -║ export BOOTH_IN_BOOTH=true ║ -║ ║ -║ AND specify a different port (not the current container's port): ║ -║ ║ -║ ./booth --port NEXT ... ║ -║ ./booth --port 11000 ... ║ -║ ║ -╚═══════════════════════════════════════════════════════════════════════════╝ -EOF - exit 1 - fi - - # BOOTH_IN_BOOTH=true is set, now check port - local requested_port="" - local args=("$@") - local i=0 - while [[ $i -lt ${#args[@]} ]]; do - case "${args[$i]}" in - --port) - if [[ $((i+1)) -lt ${#args[@]} ]]; then - requested_port="${args[$((i+1))]}" - fi - break - ;; - --port=*) - requested_port="${args[$i]#--port=}" - break - ;; - esac - ((i++)) || true - done - - # Get current container's ports - local host_port="${BOOTH_HOST_PORT:-}" - local code_port="${BOOTH_CODE_PORT:-10000}" - - # If no port specified, error out - we need an explicit different port - if [[ -z "$requested_port" ]]; then - cat >&2 <&2 <&2 <&2; exit 1 ;; - *) version_arg="$1"; shift ;; - esac - done - # Validate cache mode - if [[ "$cache_mode" != "local" && "$cache_mode" != "shared" ]]; then - echo "Error: Invalid cache mode '$cache_mode'. Use 'local' or 'shared'." >&2 - exit 1 - fi - DownloadBooth "$version_arg" "$cache_mode" - exit 0 - ;; - tools-cache) - shift # Remove 'tools-cache' - case "${1:-list}" in - list) ToolsCacheList ;; - clean) shift; ToolsCacheClean "$@" ;; - *) echo "Unknown tools-cache command: $1" >&2; exit 1 ;; - esac - exit 0 - ;; - run) [[ "${1-}" == "run" ]] && shift ; ;; - *) ;; - esac - - ### --- RUN MODE --- ### - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Read version and cache mode from lock file - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth is not installed." - echo "Please run: $0 install" - exit 1 - fi - - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2-) - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Handle legacy lock files without cache= line - if [[ -z "$lock_cache" ]]; then - lock_cache="shared" - fi - - if [[ -z "$lock_version" ]]; then - echo "Invalid lock file: missing version" - echo "Please run: $0 install" - exit 1 - fi - - # Detect platform - local platform binary_name - if ! platform=$(detect_platform); then - echo "Error: Failed to detect platform" >&2 - exit 1 - fi - binary_name=$(get_binary_name "$platform") - - # Find binary directory (local first, then shared cache) - local binary_dir sha_file dest - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - # Binary not found, auto-download - echo "Binary missing, downloading version $lock_version..." - DownloadBooth "$lock_version" "$lock_cache" - if ! binary_dir=$(find_binary_dir "$lock_version" "$platform"); then - echo "Failed to download binary" - exit 1 - fi - fi - - sha_file="$binary_dir/codingbooth.sha256" - dest="$binary_dir/$binary_name" - - # Verify binary exists - if [[ ! -f "$dest" || ! -f "$sha_file" ]]; then - echo "CodingBooth binary or checksum missing for platform: $platform" - echo "Please run: $0 install" - exit 1 - fi - - # Ensure binary is newer than checksum - if [[ "$dest" -ot "$sha_file" ]]; then - echo "Binary appears older than its checksum file." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Verify SHA256 for this platform's binary - local expected_sha256 actual_sha256 - expected_sha256=$(grep " $binary_name\$" "$sha_file" 2>/dev/null | awk '{print $1}') - if [[ -z "$expected_sha256" ]]; then - echo "No SHA256 entry found for $binary_name" - echo "Run: $0 update to restore the official release." - exit 1 - fi - - actual_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "Binary ($binary_name) failed SHA256 verification." - echo "Run: $0 update to restore the official release." - exit 1 - fi - - # Create/update symlink named 'booth' so the binary displays correct name - local booth_link="$binary_dir/booth" - if [[ ! -L "$booth_link" ]] || [[ "$(readlink "$booth_link")" != "$binary_name" ]]; then - ln -sf "$binary_name" "$booth_link" 2>/dev/null || true - fi - - # Execute via symlink if available, otherwise direct - if [[ -L "$booth_link" ]]; then - exec "$booth_link" "$@" - else - exec "$dest" "$@" - fi -} - -function PrintHelp() { - cat < [args...] - -Purpose: - This script is the *CodingBooth Wrapper*. - - It downloads, verifies, and runs the CodingBooth binary. - - Binaries are cached in a shared location (default) or per-project. - -Wrapper commands: - install [VERSION] Download binaries to shared cache (default) - install --cache=shared [VER] Download binaries to shared cache (explicit) - install --cache=local [VER] Download binaries to .booth/tools/ (project-local) - update [VERSION] Re-download binaries (force refresh) - uninstall Remove project lock file and local binaries - - tools-cache list Show cached binary versions and sizes - tools-cache clean Interactively remove cached versions - tools-cache clean --all Remove all cached versions - tools-cache clean VER Remove specific version - - run [ARGS...] Run booth with ARGS (after integrity checks) - shell-config Add 'booth' command to your shell (bash/zsh) - version Show version information - help Show this help message - -Cache modes: - --cache=shared Store in user cache, shared across projects (default) - --cache=local Store in .booth/tools/, project-specific - -Cache locations (for --cache=shared): - Linux: ~/.cache/codingbooth/ - macOS: ~/Library/Caches/codingbooth/ - Windows: %LOCALAPPDATA%\\codingbooth\\ - -Notes: - - Lock file (.booth/tools/codingbooth.lock) is version-controlled - - Binaries are auto-downloaded when lock file exists but binary missing - - Use --cache=local for CI/CD or portable/air-gapped environments - - Set VERBOSE=true for extra logs during install - -Help/Version disambiguation: - ./booth help Show this wrapper help (install, update, cache commands) - ./booth --help Show codingbooth binary help (run flags, variants, etc.) - ./booth version Show wrapper version + binary version info - ./booth --version Show codingbooth binary version only -EOF -} - -function ShellConfig() { - local booth_func='unalias booth 2>/dev/null; booth() { d="$PWD"; while [[ "$d" != "/" && ! -x "$d/booth" ]]; do d="${d%/*}"; done; if [[ -x "$d/booth" ]]; then "$d/booth" "$@"; else echo "booth not found" >&2; fi; }' - - # All supported shell rc files - local rc_files=( - "$HOME/.bashrc" - "$HOME/.zshrc" - "$HOME/.bash_profile" - "$HOME/.profile" - ) - - local added=() - local skipped=() - local not_found=() - - for rc_file in "${rc_files[@]}"; do - if [[ ! -f "$rc_file" ]]; then - not_found+=("$rc_file") - continue - fi - - # Check if already configured (idempotent) - if grep -q 'booth()' "$rc_file" 2>/dev/null; then - skipped+=("$rc_file") - continue - fi - - # Add the function - { - echo "" - echo "# CodingBooth - run 'booth' from any subdirectory" - echo "$booth_func" - } >> "$rc_file" - - added+=("$rc_file") - done - - # Report results - if [[ ${#added[@]} -gt 0 ]]; then - echo "✓ Added booth function to:" - for f in "${added[@]}"; do - echo " $f" - done - fi - - if [[ ${#skipped[@]} -gt 0 ]]; then - echo "✓ Already configured (skipped):" - for f in "${skipped[@]}"; do - echo " $f" - done - fi - - if [[ ${#added[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then - echo "No shell config files found." - echo "" - echo "You can manually add this to your shell config:" - echo " $booth_func" - exit 1 - fi - - echo "" - if [[ ${#added[@]} -gt 0 ]]; then - echo "To activate, restart your terminal or run:" - echo " source ~/.bashrc # for bash" - echo " source ~/.zshrc # for zsh" - echo "" - fi - echo "Then you can run 'booth' from any subdirectory of a CodingBooth project." -} - -function PrintVersion() { - cat <<'EOF' - ____ _ _ ____ _ _ - / ___|___ __| (_)_ __ __ _| __ ) ___ ___ | |_| |__ -| | / _ \ / _` | | '_ \ / _` | _ \ / _ \ / _ \| __| '_ \ -| |__| (_) | (_| | | | | | (_| | |_) | (_) | (_) | |_| | | | - \____\___/ \__,_|_|_| |_|\__, |____/ \___/ \___/ \__|_| |_| - |___/ -EOF - echo "CodingBooth Wrapper: $VERSION" - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - # Detect current platform - local platform binary_name - platform=$(detect_platform 2>/dev/null || echo "unknown") - binary_name=$(get_binary_name "$platform") - - # Check if lock file exists - if [[ ! -f "$lock_file" ]]; then - echo "CodingBooth: not installed" - echo "Platform: $platform" - echo "Shared cache: $BOOTH_CACHE_DIR" - exit 0 - fi - - # Read lock file - local lock_version lock_cache - lock_version=$(grep '^version=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "unknown") - lock_cache=$(grep '^cache=' "$lock_file" 2>/dev/null | cut -d= -f2- || echo "shared") - - # Find binary - local binary_dir TOOL - if binary_dir=$(find_binary_dir "$lock_version" "$platform" 2>/dev/null); then - TOOL="$binary_dir/$binary_name" - else - echo "CodingBooth: $lock_version (binary missing)" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - exit 0 - fi - - [[ ! -x "$TOOL" ]] && chmod +x "$TOOL" 2>/dev/null || true - - local TOOL_VERSION - TOOL_VERSION=$("$TOOL" version 2>/dev/null || echo "unknown") - - echo "" - echo "$TOOL_VERSION" - echo "Platform: $platform" - echo "Cache mode: $lock_cache" - if [[ "$lock_cache" == "shared" ]]; then - echo "Binary location: $binary_dir" - fi -} - -# Portable SHA256 helper -function hash_sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@" - elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@" - else echo "Error: No SHA256 tool found (sha256sum or shasum)." >&2 ; return 1 - fi -} - -# Detect platform (OS-ARCH format) -function detect_platform() { - local os arch - - # Detect OS - case "$(uname -s)" in - Linux*) os="linux" ;; - Darwin*) os="darwin" ;; - MINGW*|MSYS*|CYGWIN*) os="windows" ;; - *) echo "Error: Unsupported OS: $(uname -s)" >&2; return 1 ;; - esac - - # Detect architecture - case "$(uname -m)" in - x86_64|amd64) arch="amd64" ;; - aarch64|arm64) arch="arm64" ;; - *) echo "Error: Unsupported architecture: $(uname -m)" >&2; return 1 ;; - esac - - echo "${os}-${arch}" -} - -# Get binary name for platform (adds .exe for Windows) -function get_binary_name() { - local platform="$1" - if [[ "$platform" == windows-* ]]; then - echo "codingbooth-${platform}.exe" - else - echo "codingbooth-${platform}" - fi -} - -# All supported platforms (5 total) -ALL_PLATFORMS=( - "linux-amd64" - "linux-arm64" - "darwin-amd64" - "darwin-arm64" - "windows-amd64" -) - -function UninstallBooth() { - local tools_dir=".booth/tools" - local sha_file="$tools_dir/codingbooth.sha256" - local lock_file="$tools_dir/codingbooth.lock" - - # Remove local binaries if they exist - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - rm -f "$tools_dir/$binary_name" - done - - rm -f "$sha_file" "$lock_file" - - rmdir "$tools_dir" 2>/dev/null || true - rmdir ".booth" 2>/dev/null || true - - echo "CodingBooth has been uninstalled from this project." - echo "To clean shared cache, run: $0 tools-cache clean" -} - -# Format bytes to human-readable size -format_size() { - local bytes=$1 - if command -v numfmt >/dev/null 2>&1; then - numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" - else - # Fallback for systems without numfmt (e.g., macOS) - if [[ $bytes -ge 1073741824 ]]; then - echo "$(( bytes / 1073741824 ))GiB" - elif [[ $bytes -ge 1048576 ]]; then - echo "$(( bytes / 1048576 ))MiB" - elif [[ $bytes -ge 1024 ]]; then - echo "$(( bytes / 1024 ))KiB" - else - echo "${bytes}B" - fi - fi -} - -# Get directory size in bytes (portable) -get_dir_size() { - local dir="$1" - if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS: du -sk gives size in KB - local kb - kb=$(du -sk "$dir" 2>/dev/null | cut -f1 || echo 0) - echo $((kb * 1024)) - else - # Linux: du -sb gives size in bytes - du -sb "$dir" 2>/dev/null | cut -f1 || echo 0 - fi -} - -function ToolsCacheList() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - echo "" - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions found." - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" - return 0 - fi - - echo "Cached binary versions:" - echo "" - - local total_size=0 - local version_count=0 - - for version_dir in "$versions_dir"/*/; do - [[ ! -d "$version_dir" ]] && continue - - local version - version=$(basename "$version_dir") - - # Calculate size - local size_bytes size_human - size_bytes=$(get_dir_size "$version_dir") - size_human=$(format_size "$size_bytes") - - # List platforms - local platforms=() - for bin in "$version_dir"/codingbooth-*; do - [[ -f "$bin" ]] || continue - local name - name=$(basename "$bin") - name=${name#codingbooth-} - name=${name%.exe} - platforms+=("$name") - done - - printf " %-12s %10s [%s]\n" "$version" "$size_human" "${platforms[*]}" - - total_size=$((total_size + size_bytes)) - : $((version_count++)) - done - - if [[ $version_count -eq 0 ]]; then - echo " (no versions cached)" - fi - - echo "" - local total_human - total_human=$(format_size "$total_size") - echo "Total: $total_human in $version_count version(s)" - echo "" - echo "Cache location: $BOOTH_CACHE_DIR" -} - -function ToolsCacheClean() { - local versions_dir="${BOOTH_CACHE_DIR}/versions" - - if [[ ! -d "$versions_dir" ]]; then - echo "No cached versions to clean." - return 0 - fi - - # Parse arguments - local clean_all=false - local target_version="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --all) clean_all=true; shift ;; - --unused) echo "Warning: --unused not yet implemented"; shift ;; - -*) echo "Unknown option: $1" >&2; exit 1 ;; - *) target_version="$1"; shift ;; - esac - done - - if [[ "$clean_all" == "true" ]]; then - local size_before - size_before=$(get_dir_size "$versions_dir") - - rm -rf "$versions_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed all cached versions. Freed $size_human" - return 0 - fi - - if [[ -n "$target_version" ]]; then - local target_dir="$versions_dir/$target_version" - if [[ ! -d "$target_dir" ]]; then - echo "Version $target_version not found in cache." - return 1 - fi - - local size_before - size_before=$(get_dir_size "$target_dir") - - rm -rf "$target_dir" - - local size_human - size_human=$(format_size "$size_before") - echo "Removed version $target_version. Freed $size_human" - return 0 - fi - - # Interactive mode: list and prompt - ToolsCacheList - echo "" - read -rp "Enter version to remove (or 'all' or press Enter to cancel): " choice - - if [[ "$choice" == "all" ]]; then - ToolsCacheClean --all - elif [[ -n "$choice" ]]; then - ToolsCacheClean "$choice" - else - echo "No version selected." - fi -} - -function DownloadBooth() { - local CB_VERSION=${1:-latest} - local CACHE_MODE=${2:-shared} - - # Detect common mistake: using --version flag instead of positional argument - if [[ "$CB_VERSION" == --* ]]; then - echo "Error: Invalid version '$CB_VERSION'" >&2 - echo "Usage: $0 install [VERSION]" >&2 - echo "Example: $0 install 0.13.0" >&2 - exit 1 - fi - - local tools_dir=".booth/tools" - local lock_file="$tools_dir/codingbooth.lock" - - REPO_URL="https://github.com/NawaMan/WorkSpace" - DWLD_URL="${REPO_URL}/releases/download" - - # Download version.txt to get the actual version first - local actual_version="" - local VERSION_URL="${DWLD_URL}/${CB_VERSION}/version.txt" - if actual_version=$(curl -fsSL "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n'); then - [[ "$VERBOSE" == "true" ]] && echo " Version: $actual_version" - else - actual_version="$CB_VERSION" - fi - - # Determine target directory based on cache mode - local target_dir sha_file - if [[ "$CACHE_MODE" == "local" ]]; then - target_dir="$tools_dir" - sha_file="$tools_dir/codingbooth.sha256" - else - target_dir="$(get_cache_version_dir "$actual_version")" - sha_file="$target_dir/codingbooth.sha256" - fi - - mkdir -p "$target_dir" - mkdir -p "$tools_dir" # Always need tools dir for lock file - - # Create .gitignore based on cache mode - if [[ "$CACHE_MODE" == "local" ]]; then - cat > ".booth/.gitignore" <<'GITIGNORE' -# Binaries excluded - re-download from lock version -tools/codingbooth-* -tools/*.sha256 -GITIGNORE - else - cat > ".booth/.gitignore" <<'GITIGNORE' -# Lock file is version-controlled -# Binaries are in ~/.cache/codingbooth/ (not here) -GITIGNORE - fi - - # Clear previous SHA256 file (will rebuild with all binaries) - > "$sha_file" - - local current_platform - current_platform=$(detect_platform 2>/dev/null || echo "unknown") - local download_count=0 - local fail_count=0 - - if [[ "$CACHE_MODE" == "local" ]]; then - echo "Downloading CodingBooth binaries to project (--cache=local)..." - else - echo "Downloading CodingBooth binaries to shared cache..." - echo " Cache: $target_dir" - fi - - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - local TOOL_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}" - local SHA256_URL="${DWLD_URL}/${CB_VERSION}/${binary_name}.sha256" - - if [[ "$VERBOSE" == "true" ]]; then - echo " Checking: $binary_name" - else - echo -n " $platform ... " - fi - - local tmpsha256 - tmpsha256=$(mktemp "/tmp/booth.sha256.XXXXXX") - - # Download SHA256 first to check if we need to download the binary - if ! curl -fsSLo "$tmpsha256" "$SHA256_URL"; then - echo "FAILED (sha256 fetch)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - local expected_sha256 - expected_sha256=$(awk '{print $1}' "$tmpsha256") - if ! [[ "$expected_sha256" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "FAILED (malformed sha256)" - rm -f "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Check if binary already exists with correct checksum - if [[ -f "$dest" ]]; then - local existing_sha256 - existing_sha256=$(hash_sha256 "$dest" | awk '{print $1}') - if [[ "$expected_sha256" == "$existing_sha256" ]]; then - # Binary already exists and is valid, skip download - printf '%s %s\n' "$expected_sha256" "$binary_name" >> "$sha_file" - rm -f "$tmpsha256" - : $((download_count++)) - if [[ "$VERBOSE" != "true" ]]; then - echo "✓ up-to-date" - else - echo " Already up-to-date: $binary_name" - fi - continue - fi - fi - - # Need to download - binary missing or checksum mismatch - if [[ "$VERBOSE" == "true" ]]; then - echo " Downloading: $binary_name" - fi - - local tmpfile - tmpfile=$(mktemp "/tmp/booth.XXXXXX") - - # Download binary - if ! curl -fsSLo "$tmpfile" "$TOOL_URL"; then - echo "FAILED (download)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Verify SHA256 - local actual_sha256 - actual_sha256=$(hash_sha256 "$tmpfile" | awk '{print $1}') - if [[ "$expected_sha256" != "$actual_sha256" ]]; then - echo "FAILED (sha256 mismatch)" - rm -f "$tmpfile" "$tmpsha256" - : $((fail_count++)) - continue - fi - - # Install verified binary with 744 permissions - mv -f "$tmpfile" "$dest" - chmod 744 "$dest" - - # Append to combined SHA256 file - printf '%s %s\n' "$actual_sha256" "$binary_name" >> "$sha_file" - - rm -f "$tmpsha256" - : $((download_count++)) - - if [[ "$VERBOSE" != "true" ]]; then - echo "OK" - fi - done - - if [[ $fail_count -gt 0 ]]; then - echo "Warning: $fail_count platform(s) failed to download" >&2 - fi - - if [[ $download_count -eq 0 ]]; then - echo "Error: No binaries were downloaded successfully" >&2 - return 1 - fi - - # Write lock file (always in .booth/tools/) - { - echo "version=${actual_version}" - echo "downloaded_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "cache=${CACHE_MODE}" - } > "$lock_file" - - # Touch all binaries to be newer than checksum - for platform in "${ALL_PLATFORMS[@]}"; do - local binary_name - binary_name=$(get_binary_name "$platform") - local dest="$target_dir/$binary_name" - [[ -f "$dest" ]] && touch "$dest" - done - - echo "CodingBooth installed: downloaded, verified, and installed." - if [[ "$VERBOSE" == "true" ]]; then - echo "Lock file: $lock_file" - fi -} - -# Early handling of version/help/shell-config so they don't require curl -case "${COMMAND}" in - version) PrintVersion ; exit 0 ; ;; - help) PrintHelp ; exit 0 ; ;; - shell-config) ShellConfig ; exit 0 ; ;; -esac - -# Check for nested booth execution (running booth inside a booth container) -# This check runs for all commands except help/version/shell-config -detect_nested_booth "$@" - -# Need curl for install/run/update/uninstall -if ! command -v curl >/dev/null 2>&1; then - echo "Error: curl is required but was not found." >&2 - exit 1 -fi - -Main "$@" diff --git a/tests/basic/run-basic-tests.sh b/tests/basic/run-basic-tests.sh index 266b9500..1df444f7 100755 --- a/tests/basic/run-basic-tests.sh +++ b/tests/basic/run-basic-tests.sh @@ -8,6 +8,15 @@ failed=0 failed_tests=() total_tests=0 +if ! command -v docker >/dev/null 2>&1; then + echo "SKIP: Docker is not installed or not in PATH" + exit 0 +fi +if ! docker info >/dev/null 2>&1; then + echo "SKIP: Docker daemon is not accessible (permission or not running)" + exit 0 +fi + for f in test0*.sh ; do echo "$f" total_tests=$((total_tests + 1)) diff --git a/tests/complex/run-complex-tests.sh b/tests/complex/run-complex-tests.sh index 86db3b3b..2845d835 100755 --- a/tests/complex/run-complex-tests.sh +++ b/tests/complex/run-complex-tests.sh @@ -24,6 +24,15 @@ echo "============================================================" echo "Running Complex Tests" echo "============================================================" +if ! command -v docker >/dev/null 2>&1; then + echo "SKIP: Docker is not installed or not in PATH" + exit 0 +fi +if ! docker info >/dev/null 2>&1; then + echo "SKIP: Docker daemon is not accessible (permission or not running)" + exit 0 +fi + FAILED=0 PASSED=0 FAILED_TESTS=() diff --git a/tests/complex/test-boothfile-custom-setup/.booth/.Dockerfile.generated b/tests/complex/test-boothfile-custom-setup/.booth/.Dockerfile.generated deleted file mode 100644 index 3eab11b0..00000000 --- a/tests/complex/test-boothfile-custom-setup/.booth/.Dockerfile.generated +++ /dev/null @@ -1,12 +0,0 @@ -# syntax=docker/dockerfile:1.7 -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -COPY .booth/setups/ /home/coder/.booth/setups/ -ENV PATH=/home/coder/.booth/setups:$PATH - -RUN mytool--setup.sh diff --git a/tests/complex/test-boothfile-env-workdir/.booth/.Dockerfile.generated b/tests/complex/test-boothfile-env-workdir/.booth/.Dockerfile.generated deleted file mode 100644 index 696c1041..00000000 --- a/tests/complex/test-boothfile-env-workdir/.booth/.Dockerfile.generated +++ /dev/null @@ -1,12 +0,0 @@ -# syntax=docker/dockerfile:1.7 -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -ENV MY_TEST_VAR=hello_from_boothfile -ENV ANOTHER_VAR=42 -WORKDIR /tmp/testdir -RUN mkdir -p /tmp/testdir diff --git a/tests/complex/test-boothfile-multi-setup/.booth/.Dockerfile.generated b/tests/complex/test-boothfile-multi-setup/.booth/.Dockerfile.generated deleted file mode 100644 index 818d39bb..00000000 --- a/tests/complex/test-boothfile-multi-setup/.booth/.Dockerfile.generated +++ /dev/null @@ -1,11 +0,0 @@ -# syntax=docker/dockerfile:1.7 -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -RUN python--setup.sh 3.12 -RUN nodejs--setup.sh 20 -RUN pip--install.sh requests diff --git a/tests/complex/test-boothfile-nodejs/.booth/.Dockerfile.generated b/tests/complex/test-boothfile-nodejs/.booth/.Dockerfile.generated deleted file mode 100644 index 37baf1fc..00000000 --- a/tests/complex/test-boothfile-nodejs/.booth/.Dockerfile.generated +++ /dev/null @@ -1,9 +0,0 @@ -# syntax=docker/dockerfile:1.7 -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -RUN nodejs--setup.sh 20 diff --git a/tests/complex/test-boothfile-python/.booth/.Dockerfile.generated b/tests/complex/test-boothfile-python/.booth/.Dockerfile.generated deleted file mode 100644 index 6dc6549f..00000000 --- a/tests/complex/test-boothfile-python/.booth/.Dockerfile.generated +++ /dev/null @@ -1,9 +0,0 @@ -# syntax=docker/dockerfile:1.7 -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest -FROM nawaman/codingbooth:${BOOTH_VARIANT_TAG}-${BOOTH_VERSION_TAG} - -ARG BOOTH_VARIANT_TAG=base -ARG BOOTH_VERSION_TAG=latest - -RUN python--setup.sh 3.12 diff --git a/tests/complex/test-env/test--workspace-env.sh b/tests/complex/test-env/test--workspace-env.sh index 6ddc0f58..196988a7 100755 --- a/tests/complex/test-env/test--workspace-env.sh +++ b/tests/complex/test-env/test--workspace-env.sh @@ -39,7 +39,7 @@ if [[ ! -x "$CB_SCRIPT" ]]; then fi # ---- Test booth ----------------------------------------------------------- -TMPDIR="$(mktemp -d "$HOME/cb-test.XXXXXX")" +TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/cb-test.XXXXXX")" cleanup() { # Booth container is started with --rm, so nothing to stop. rm -rf "$TMPDIR" || true diff --git a/tests/complex/test-lifecycle-bind-port/test--lifecycle-bind-port.sh b/tests/complex/test-lifecycle-bind-port/test--lifecycle-bind-port.sh index fe6b05b6..6da4c907 100755 --- a/tests/complex/test-lifecycle-bind-port/test--lifecycle-bind-port.sh +++ b/tests/complex/test-lifecycle-bind-port/test--lifecycle-bind-port.sh @@ -71,7 +71,7 @@ BIND_ARG="${HOST_DIR}:/tmp/lifecycle-extra" # 1) Create keep-alive booth with extra bind and extra port mapping. if run_coding_booth --variant base --name "$NAME" --port "$UI_PORT" --daemon --keep-alive \ - -v "$BIND_ARG" -p "${EXTRA_PORT}:12345" -- 'sleep 60' >/dev/null 2>&1; then + -v "$BIND_ARG" -p "${EXTRA_PORT}:12345" -- 'sleep 120' >/dev/null 2>&1; then if [[ "$(host_port "$NAME" 10000)" == "$UI_PORT" ]] \ && [[ "$(host_port "$NAME" 12345)" == "$EXTRA_PORT" ]] \ diff --git a/tests/complex/test-lifecycle-name-port/test--lifecycle-name-port.sh b/tests/complex/test-lifecycle-name-port/test--lifecycle-name-port.sh index fb9327e9..31b63b11 100755 --- a/tests/complex/test-lifecycle-name-port/test--lifecycle-name-port.sh +++ b/tests/complex/test-lifecycle-name-port/test--lifecycle-name-port.sh @@ -105,14 +105,18 @@ else fi # 4) To change port, container must be recreated. -if run_coding_booth stop --name "$NAME" >/dev/null 2>&1 \ - && run_coding_booth remove --name "$NAME" >/dev/null 2>&1 \ - && run_coding_booth --variant base --name "$NAME" --port "$PORT_B" --keep-alive -- 'sleep 1' >/dev/null 2>&1; then +run_coding_booth stop --name "$NAME" >/dev/null 2>&1 || true +if ! run_coding_booth remove --name "$NAME" >/dev/null 2>&1; then + run_coding_booth remove --force --name "$NAME" >/dev/null 2>&1 || true +fi +if run_coding_booth --variant base --name "$NAME" --port "$PORT_B" --keep-alive -- 'sleep 1' >/dev/null 2>&1; then ACTUAL_PORT="$(host_port_10000 "$NAME")" if [[ "$ACTUAL_PORT" == "$PORT_B" ]] && [[ "$(state_of "$NAME")" == "exited" ]]; then print_test_result "true" "$0" "4" "recreate allows UI port override" else print_test_result "false" "$0" "4" "expected recreated container on port $PORT_B" + echo " Actual port: ${ACTUAL_PORT:-}" + echo " State: $(state_of "$NAME")" FAILED=$((FAILED + 1)) fi else diff --git a/tests/complex/test-sandbox-allowlist-extra/test--sandbox-allowlist-extra.sh b/tests/complex/test-sandbox-allowlist-extra/test--sandbox-allowlist-extra.sh new file mode 100755 index 00000000..de009378 --- /dev/null +++ b/tests/complex/test-sandbox-allowlist-extra/test--sandbox-allowlist-extra.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +# Complex test: sandbox-allowlist adds extra domains to allowlist file + +set -euo pipefail + +source ../../common--source.sh + +# ---- Config ------------------------------------------------------------------- +CB_SCRIPT="${CB_SCRIPT:-../../../codingbooth}" +if command -v readlink >/dev/null 2>&1; then + CB_SCRIPT="$(readlink -f "$CB_SCRIPT")" +else + CB_SCRIPT="$(cd "$(dirname "$CB_SCRIPT")" && pwd -P)/$(basename "$CB_SCRIPT")" +fi + +RUN_ID="$(date +%s)-$$" +CONTAINER_NAME="cb-test-sandbox-allowlist-extra-${RUN_ID}" +IMAGE_NAME="${CB_IMAGE_NAME:-nawaman/codingbooth:base-latest}" + +# ---- Preconditions ------------------------------------------------------------ +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker not found in PATH" + exit 1 +fi + +if [[ ! -x "$CB_SCRIPT" ]]; then + echo "ERROR: CB_SCRIPT not executable or not found: $CB_SCRIPT" >&2 + exit 1 +fi + +# ---- Test booth ----------------------------------------------------------- +TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/cb-test-sandbox-allowlist-extra.XXXXXX")" +cleanup() { + rm -rf "$TMPDIR" || true +} +trap cleanup EXIT + +pushd "$TMPDIR" >/dev/null + +mkdir -p .booth/sandbox +cat > .booth/sandbox/allowlist.txt <<'EOF' +pypi.org +EOF + +cat > .booth/config.toml <<'EOF' +sandboxed = true +sandbox-allowlist-file = ".booth/sandbox/allowlist.txt" +sandbox-allowlist = [ + "example.com" +] +EOF + +rm -f cb-log.log +touch cb-log.log + +run_cb() { + echo -e "${COLOR_BOOTH:-}> codingbooth --sandboxed --verbose --config .booth/config.toml --image $IMAGE_NAME --name $CONTAINER_NAME -- $*${COLOR_RESET:-}" >&2 + "$CB_SCRIPT" --sandboxed --verbose --config .booth/config.toml --image "$IMAGE_NAME" --name "$CONTAINER_NAME" -- "$@" | tee cb-log.log +} + +# ---- Assertions --------------------------------------------------------------- +total_checks=0 +failed_checks=0 +failed_msgs=() + +pass() { + total_checks=$((total_checks + 1)) + print_test_result "true" "$0" "$total_checks" "$*" +} + +fail() { + total_checks=$((total_checks + 1)) + failed_checks=$((failed_checks + 1)) + failed_msgs+=("$*") + print_test_result "false" "$0" "$total_checks" "$*" +} + +proxy_connect_code() { + local output code + set +e + output="$(run_cb "curl -s -o /dev/null -w \"\\n%{http_connect}\\n\" --max-time 8 $1 || true" | tr -d '\r')" + set -e + code="$(printf "%s\n" "$output" | awk 'NF{line=$0} END{print line}' | grep -Eo '[0-9]{3}' | tail -n 1 || true)" + if [[ -z "$code" ]]; then + code="000" + fi + echo "$code" +} + +code="$(proxy_connect_code "https://pypi.org")" +if [[ "$code" == "403" ]]; then + fail "allowlist file domain blocked by proxy (pypi.org) -> CONNECT $code" +else + pass "allowlist file domain allowed by proxy (pypi.org) -> CONNECT $code" +fi + +code="$(proxy_connect_code "https://example.com")" +if [[ "$code" == "403" ]]; then + fail "sandbox-allowlist domain blocked by proxy (example.com) -> CONNECT $code" +else + pass "sandbox-allowlist domain allowed by proxy (example.com) -> CONNECT $code" +fi + +code="$(proxy_connect_code "https://reddit.com")" +if [[ "$code" == "403" ]]; then + pass "non-allowlisted domain blocked by proxy (reddit.com) -> CONNECT $code" +else + fail "non-allowlisted domain not blocked by proxy (reddit.com) -> CONNECT $code" +fi + +popd >/dev/null + +# ---- Summary ------------------------------------------------------------------ +if (( failed_checks == 0 )); then + echo "✅ All $total_checks checks passed." + exit 0 +else + echo "❌ $failed_checks out of $total_checks checks failed." + echo "Failed checks:" + for msg in "${failed_msgs[@]}"; do + echo " - $msg" + done + exit 1 +fi diff --git a/tests/complex/test-sandbox-allowlist/test--sandbox-allowlist.sh b/tests/complex/test-sandbox-allowlist/test--sandbox-allowlist.sh new file mode 100755 index 00000000..d21a14b4 --- /dev/null +++ b/tests/complex/test-sandbox-allowlist/test--sandbox-allowlist.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +# Complex test: --sandboxed with allowlist.txt +# Verifies allowlisted domain is reachable and non-allowlisted is blocked. + +set -euo pipefail + +source ../../common--source.sh + +# ---- Config ------------------------------------------------------------------- +CB_SCRIPT="${CB_SCRIPT:-../../../codingbooth}" +if command -v readlink >/dev/null 2>&1; then + CB_SCRIPT="$(readlink -f "$CB_SCRIPT")" +else + CB_SCRIPT="$(cd "$(dirname "$CB_SCRIPT")" && pwd -P)/$(basename "$CB_SCRIPT")" +fi + +RUN_ID="$(date +%s)-$$" +CONTAINER_NAME="cb-test-sandbox-allowlist-${RUN_ID}" +IMAGE_NAME="${CB_IMAGE_NAME:-nawaman/codingbooth:base-latest}" + +# ---- Preconditions ------------------------------------------------------------ +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker not found in PATH" + exit 1 +fi + +if [[ ! -x "$CB_SCRIPT" ]]; then + echo "ERROR: CB_SCRIPT not executable or not found: $CB_SCRIPT" >&2 + exit 1 +fi + +# ---- Test booth ----------------------------------------------------------- +TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/cb-test-sandbox-allowlist.XXXXXX")" +cleanup() { + rm -rf "$TMPDIR" || true +} +trap cleanup EXIT + +pushd "$TMPDIR" >/dev/null + +mkdir -p .booth/sandbox +cat > .booth/sandbox/allowlist.txt <<'EOF' +pypi.org +EOF + +run_cb() { + echo -e "${COLOR_BOOTH:-}> codingbooth --sandboxed --image $IMAGE_NAME --name $CONTAINER_NAME -- $*${COLOR_RESET:-}" >&2 + "$CB_SCRIPT" --sandboxed --image "$IMAGE_NAME" --name "$CONTAINER_NAME" -- "$@" +} + +# ---- Assertions --------------------------------------------------------------- +total_checks=0 +failed_checks=0 +failed_msgs=() + +pass() { + total_checks=$((total_checks + 1)) + print_test_result "true" "$0" "$total_checks" "$*" +} + +fail() { + total_checks=$((total_checks + 1)) + failed_checks=$((failed_checks + 1)) + failed_msgs+=("$*") + print_test_result "false" "$0" "$total_checks" "$*" +} + +http_code() { + local output code + set +e + output="$(run_cb "curl -s -o /dev/null -w \"\\n%{http_code}\\n\" --max-time 8 $1 || true" | tr -d '\r')" + set -e + code="$(printf "%s\n" "$output" | awk 'NF{line=$0} END{print line}' | grep -Eo '[0-9]{3}' | tail -n 1 || true)" + if [[ -z "$code" ]]; then + code="000" + fi + echo "$code" +} + +code="$(http_code "https://pypi.org")" +if [[ "$code" == "000" || "$code" == "403" ]]; then + fail "allowlisted domain blocked (pypi.org) -> HTTP $code" +else + pass "allowlisted domain reachable (pypi.org) -> HTTP $code" +fi + +code="$(http_code "https://example.com")" +if [[ "$code" == "000" || "$code" == "403" ]]; then + pass "non-allowlisted domain blocked (example.com) -> HTTP $code" +else + fail "non-allowlisted domain reachable (example.com) -> HTTP $code" +fi + +popd >/dev/null + +# ---- Summary ------------------------------------------------------------------ +if (( failed_checks == 0 )); then + echo "✅ All $total_checks checks passed." + exit 0 +else + echo "❌ $failed_checks out of $total_checks checks failed." + echo "Failed checks:" + for msg in "${failed_msgs[@]}"; do + echo " - $msg" + done + exit 1 +fi diff --git a/tests/complex/test-sandbox-envoy/test--sandbox-envoy.sh b/tests/complex/test-sandbox-envoy/test--sandbox-envoy.sh new file mode 100755 index 00000000..8067d235 --- /dev/null +++ b/tests/complex/test-sandbox-envoy/test--sandbox-envoy.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +# Complex test: --sandboxed with custom envoy.yaml +# Verifies allowlisted domain is reachable and non-allowlisted is blocked. + +set -euo pipefail + +source ../../common--source.sh + +# ---- Config ------------------------------------------------------------------- +CB_SCRIPT="${CB_SCRIPT:-../../../codingbooth}" +if command -v readlink >/dev/null 2>&1; then + CB_SCRIPT="$(readlink -f "$CB_SCRIPT")" +else + CB_SCRIPT="$(cd "$(dirname "$CB_SCRIPT")" && pwd -P)/$(basename "$CB_SCRIPT")" +fi + +RUN_ID="$(date +%s)-$$" +CONTAINER_NAME="cb-test-sandbox-envoy-${RUN_ID}" +IMAGE_NAME="${CB_IMAGE_NAME:-nawaman/codingbooth:base-latest}" + +# ---- Preconditions ------------------------------------------------------------ +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker not found in PATH" + exit 1 +fi + +if [[ ! -x "$CB_SCRIPT" ]]; then + echo "ERROR: CB_SCRIPT not executable or not found: $CB_SCRIPT" >&2 + exit 1 +fi + +# ---- Test booth ----------------------------------------------------------- +TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/cb-test-sandbox-envoy.XXXXXX")" +cleanup() { + rm -rf "$TMPDIR" || true +} +trap cleanup EXIT + +pushd "$TMPDIR" >/dev/null + +mkdir -p .booth/sandbox +cat > .booth/sandbox/envoy.yaml <<'EOF' +static_resources: + listeners: + - name: egress_proxy + address: + socket_address: + address: 0.0.0.0 + port_value: 15001 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: egress_http + codec_type: AUTO + route_config: + name: proxy_routes + virtual_hosts: + - name: forward_proxy + domains: ["*"] + routes: + - match: + connect_matcher: {} + route: + cluster: dynamic_forward_proxy_cluster + upgrade_configs: + - upgrade_type: CONNECT + connect_config: {} + - match: + prefix: "/" + route: + cluster: dynamic_forward_proxy_cluster + http_filters: + - name: envoy.filters.http.rbac + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: ALLOW + policies: + allow_pypi: + permissions: + - header: + name: ":authority" + string_match: + safe_regex: + google_re2: {} + regex: "(^|.*\\.)pypi\\.org(:[0-9]+)?$" + principals: + - any: true + - name: envoy.filters.http.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + upgrade_configs: + - upgrade_type: CONNECT + + clusters: + - name: dynamic_forward_proxy_cluster + connect_timeout: 5s + lb_policy: CLUSTER_PROVIDED + cluster_type: + name: envoy.clusters.dynamic_forward_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + +admin: + address: + socket_address: + address: 127.0.0.1 + port_value: 9901 +EOF + +run_cb() { + echo -e "${COLOR_BOOTH:-}> codingbooth --sandboxed --image $IMAGE_NAME --name $CONTAINER_NAME -- $*${COLOR_RESET:-}" >&2 + "$CB_SCRIPT" --sandboxed --image "$IMAGE_NAME" --name "$CONTAINER_NAME" -- "$@" +} + +# ---- Assertions --------------------------------------------------------------- +total_checks=0 +failed_checks=0 +failed_msgs=() + +pass() { + total_checks=$((total_checks + 1)) + print_test_result "true" "$0" "$total_checks" "$*" +} + +fail() { + total_checks=$((total_checks + 1)) + failed_checks=$((failed_checks + 1)) + failed_msgs+=("$*") + print_test_result "false" "$0" "$total_checks" "$*" +} + +http_code() { + local output code + set +e + output="$(run_cb "curl -s -o /dev/null -w \"\\n%{http_code}\\n\" --max-time 8 $1 || true" | tr -d '\r')" + set -e + code="$(printf "%s\n" "$output" | awk 'NF{line=$0} END{print line}' | grep -Eo '[0-9]{3}' | tail -n 1 || true)" + if [[ -z "$code" ]]; then + code="000" + fi + echo "$code" +} + +code="$(http_code "https://pypi.org")" +if [[ "$code" == "000" || "$code" == "403" ]]; then + fail "allowlisted domain blocked (pypi.org) -> HTTP $code" +else + pass "allowlisted domain reachable (pypi.org) -> HTTP $code" +fi + +code="$(http_code "https://example.com")" +if [[ "$code" == "000" || "$code" == "403" ]]; then + pass "non-allowlisted domain blocked (example.com) -> HTTP $code" +else + fail "non-allowlisted domain reachable (example.com) -> HTTP $code" +fi + +popd >/dev/null + +# ---- Summary ------------------------------------------------------------------ +if (( failed_checks == 0 )); then + echo "✅ All $total_checks checks passed." + exit 0 +else + echo "❌ $failed_checks out of $total_checks checks failed." + echo "Failed checks:" + for msg in "${failed_msgs[@]}"; do + echo " - $msg" + done + exit 1 +fi diff --git a/tests/complex/test-sandbox-ro/test--sandbox-ro.sh b/tests/complex/test-sandbox-ro/test--sandbox-ro.sh new file mode 100755 index 00000000..fa349685 --- /dev/null +++ b/tests/complex/test-sandbox-ro/test--sandbox-ro.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# Copyright 2025-2026 : Nawa Manusitthipol +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +# Complex test: .booth/ is always read-only inside the container by default. +# Use --writable-booth to opt out of this protection. + +set -euo pipefail + +source ../../common--source.sh + +# ---- Config ------------------------------------------------------------------- +CB_SCRIPT="${CB_SCRIPT:-../../../codingbooth}" +if command -v readlink >/dev/null 2>&1; then + CB_SCRIPT="$(readlink -f "$CB_SCRIPT")" +else + CB_SCRIPT="$(cd "$(dirname "$CB_SCRIPT")" && pwd -P)/$(basename "$CB_SCRIPT")" +fi + +RUN_ID="$(date +%s)-$$" +CONTAINER_NAME="cb-test-sandbox-ro-${RUN_ID}" +IMAGE_NAME="${CB_IMAGE_NAME:-nawaman/codingbooth:base-latest}" + +# ---- Preconditions ------------------------------------------------------------ +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker not found in PATH" + exit 1 +fi + +if [[ ! -x "$CB_SCRIPT" ]]; then + echo "ERROR: CB_SCRIPT not executable or not found: $CB_SCRIPT" >&2 + exit 1 +fi + +# ---- Test booth ----------------------------------------------------------- +TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/cb-test-sandbox-ro.XXXXXX")" +cleanup() { + rm -rf "$TMPDIR" || true +} +trap cleanup EXIT + +pushd "$TMPDIR" >/dev/null + +# Prepare .booth files +mkdir -p .booth/sandbox + +cat > .booth/config.toml <<'EOF' +# test config +variant = "base" +EOF + +cat > .booth/Boothfile <<'EOF' +# test Boothfile (not used because we pass --image) +EOF + +cat > .booth/sandbox/allowlist.txt <<'EOF' +github.com +EOF + +touch normal.txt + +# Helper to run the booth with sandbox enabled +run_cb() { + echo -e "${COLOR_BOOTH:-}> codingbooth --sandboxed --image $IMAGE_NAME --name $CONTAINER_NAME -- $*${COLOR_RESET:-}" >&2 + "$CB_SCRIPT" --sandboxed --image "$IMAGE_NAME" --name "$CONTAINER_NAME" -- "$@" +} + +# Helper to run without sandbox (default) +run_cb_plain() { + echo -e "${COLOR_BOOTH:-}> codingbooth --image $IMAGE_NAME --name $CONTAINER_NAME -- $*${COLOR_RESET:-}" >&2 + "$CB_SCRIPT" --image "$IMAGE_NAME" --name "$CONTAINER_NAME" -- "$@" +} + +# Helper to run with --writable-booth +run_cb_writable() { + echo -e "${COLOR_BOOTH:-}> codingbooth --writable-booth --image $IMAGE_NAME --name $CONTAINER_NAME -- $*${COLOR_RESET:-}" >&2 + "$CB_SCRIPT" --writable-booth --image "$IMAGE_NAME" --name "$CONTAINER_NAME" -- "$@" +} + +# ---- Assertions --------------------------------------------------------------- +total_checks=0 +failed_checks=0 +failed_msgs=() + +pass() { + total_checks=$((total_checks + 1)) + print_test_result "true" "$0" "$total_checks" "$*" +} + +fail() { + total_checks=$((total_checks + 1)) + failed_checks=$((failed_checks + 1)) + failed_msgs+=("$*") + print_test_result "false" "$0" "$total_checks" "$*" +} + +expect_fail() { + local label="$1" + shift + set +e + run_cb "$@" >/dev/null 2>&1 + local status=$? + set -e + if [[ $status -eq 0 ]]; then + fail "$label (expected failure, but exit=0)" + else + pass "$label (write blocked as expected)" + fi +} + +expect_success() { + local label="$1" + shift + set +e + run_cb "$@" >/dev/null 2>&1 + local status=$? + set -e + if [[ $status -ne 0 ]]; then + fail "$label (expected success, exit=$status)" + else + pass "$label" + fi +} + +# Same helpers, but against the non-sandboxed run +expect_fail_plain() { + local label="$1" + shift + set +e + run_cb_plain "$@" >/dev/null 2>&1 + local status=$? + set -e + if [[ $status -eq 0 ]]; then + fail "$label (expected failure, but exit=0)" + else + pass "$label (write blocked as expected)" + fi +} + +expect_success_plain() { + local label="$1" + shift + set +e + run_cb_plain "$@" >/dev/null 2>&1 + local status=$? + set -e + if [[ $status -ne 0 ]]; then + fail "$label (expected success, exit=$status)" + else + pass "$label" + fi +} + +# Same helpers, but with --writable-booth +expect_fail_writable() { + local label="$1" + shift + set +e + run_cb_writable "$@" >/dev/null 2>&1 + local status=$? + set -e + if [[ $status -eq 0 ]]; then + fail "$label (expected failure, but exit=0)" + else + pass "$label (write blocked as expected)" + fi +} + +expect_success_writable() { + local label="$1" + shift + set +e + run_cb_writable "$@" >/dev/null 2>&1 + local status=$? + set -e + if [[ $status -ne 0 ]]; then + fail "$label (expected success, exit=$status)" + else + pass "$label" + fi +} + +# .booth should be read-only with --sandboxed +expect_fail "config.toml is read-only (sandboxed)" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/config.toml" +expect_fail "Boothfile is read-only (sandboxed)" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/Boothfile" +expect_fail "allowlist.txt is read-only (sandboxed)" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/sandbox/allowlist.txt" + +# Control: normal file should still be writable +expect_success "normal.txt is writable" "bash" "-lc" "echo 'ok' > /home/coder/code/normal.txt" + +# .booth should ALSO be read-only without --sandboxed (always-on protection) +expect_fail_plain "config.toml is read-only (no sandbox)" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/config.toml" +expect_fail_plain "Boothfile is read-only (no sandbox)" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/Boothfile" +expect_fail_plain "allowlist.txt is read-only (no sandbox)" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/sandbox/allowlist.txt" + +# Control: normal file should still be writable without sandbox +expect_success_plain "normal.txt is writable (no sandbox)" "bash" "-lc" "echo 'ok' > /home/coder/code/normal.txt" + +# --writable-booth should allow writing to .booth files +expect_success_writable "config.toml writable with --writable-booth" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/config.toml" +expect_success_writable "Boothfile writable with --writable-booth" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/Boothfile" +expect_success_writable "allowlist.txt writable with --writable-booth" "bash" "-lc" "echo 'thing' > /home/coder/code/.booth/sandbox/allowlist.txt" + +popd >/dev/null + +# ---- Summary ------------------------------------------------------------------ +if (( failed_checks == 0 )); then + echo "✅ All $total_checks checks passed." + exit 0 +else + echo "❌ $failed_checks out of $total_checks checks failed." + echo "Failed checks:" + for msg in "${failed_msgs[@]}"; do + echo " - $msg" + done + exit 1 +fi diff --git a/tests/dryrun/test--config.toml b/tests/dryrun/test--config.toml deleted file mode 100644 index 424d16ad..00000000 --- a/tests/dryrun/test--config.toml +++ /dev/null @@ -1,12 +0,0 @@ -daemon = true -dind = true -dockerfile = "test--config.toml" -dryrun = true -env-file = "test--.env" -keep-alive = true -name = "test-container" -pull = true -variant = "base" -verbose = true -version = "0.17.0--rc2" -run-args = "-p;10005" diff --git a/tests/dryrun/test001--dryrun.sh b/tests/dryrun/test001--dryrun.sh index 64ca19b4..c053ebaf 100755 --- a/tests/dryrun/test001--dryrun.sh +++ b/tests/dryrun/test001--dryrun.sh @@ -84,6 +84,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test002--help.sh b/tests/dryrun/test002--help.sh index ffe3db1d..17c71772 100755 --- a/tests/dryrun/test002--help.sh +++ b/tests/dryrun/test002--help.sh @@ -17,8 +17,8 @@ if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then CURRENT_PATH="$(pwd -W)" fi -# Just check the USAGE section (first 16 lines) - the full help is ~98 lines -ACTUAL=$(run_coding_booth --help | head -16) +# Just check the USAGE section (first 17 lines) - the full help is ~98 lines +ACTUAL=$(run_coding_booth --help | head -17) HERE="$PWD" VERSION="$(cat ../../version.txt)" @@ -38,7 +38,8 @@ USAGE: codingbooth remove [--name ] [--force] (remove booth container(s)) codingbooth prune [--yes] (remove stopped booth containers) codingbooth example (manage examples) - codingbooth emit-dockerfile [options] (compile Boothfile to Dockerfile)" + codingbooth emit-dockerfile [options] (compile Boothfile to Dockerfile) + codingbooth print-default-allowlist.txt (print built-in egress allowlist)" if diff -u <(echo "$EXPECT" | normalize_output) <(echo "$ACTUAL" | normalize_output); then print_test_result "true" "$0" "1" "Help output matches expected" diff --git a/tests/dryrun/test003--command.sh b/tests/dryrun/test003--command.sh index 0c3bf8b2..8acb1808 100755 --- a/tests/dryrun/test003--command.sh +++ b/tests/dryrun/test003--command.sh @@ -70,6 +70,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test004--name.sh b/tests/dryrun/test004--name.sh index 0639ffb0..0307e5ff 100755 --- a/tests/dryrun/test004--name.sh +++ b/tests/dryrun/test004--name.sh @@ -70,6 +70,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test005-version.sh b/tests/dryrun/test005-version.sh index 7406f70b..e9faa68f 100755 --- a/tests/dryrun/test005-version.sh +++ b/tests/dryrun/test005-version.sh @@ -73,6 +73,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test006--port.sh b/tests/dryrun/test006--port.sh index 23c9c47d..cd2234db 100755 --- a/tests/dryrun/test006--port.sh +++ b/tests/dryrun/test006--port.sh @@ -74,6 +74,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test007--daemon.sh b/tests/dryrun/test007--daemon.sh index ed19cda3..4dbb63e6 100755 --- a/tests/dryrun/test007--daemon.sh +++ b/tests/dryrun/test007--daemon.sh @@ -82,6 +82,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test008--keep-alive.sh b/tests/dryrun/test008--keep-alive.sh index 49fa0b05..9c1591d7 100755 --- a/tests/dryrun/test008--keep-alive.sh +++ b/tests/dryrun/test008--keep-alive.sh @@ -71,6 +71,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test009--workspace.sh b/tests/dryrun/test009--workspace.sh index 37a2a413..1bbf173f 100755 --- a/tests/dryrun/test009--workspace.sh +++ b/tests/dryrun/test009--workspace.sh @@ -75,6 +75,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=tests' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test011-variant.sh b/tests/dryrun/test011-variant.sh index 67e7ad8b..f187fb8b 100755 --- a/tests/dryrun/test011-variant.sh +++ b/tests/dryrun/test011-variant.sh @@ -94,6 +94,7 @@ docker \\ -e 'BOOTH_SILENCE_BUILD=false' \\ -e 'BOOTH_PULL=false' \\ -e 'BOOTH_DIND=false' \\ + -e 'BOOTH_SANDBOX=false' \\ -e 'BOOTH_DOCKERFILE=' \\ -e 'BOOTH_PROJECT_NAME=dryrun' \\ -e 'BOOTH_TIMEZONE=America/Toronto' \\ diff --git a/tests/dryrun/test012--config-file--envvars.sh b/tests/dryrun/test012--config-file--envvars.sh index 1f7f2c47..4eae8f64 100755 --- a/tests/dryrun/test012--config-file--envvars.sh +++ b/tests/dryrun/test012--config-file--envvars.sh @@ -63,6 +63,7 @@ LOCAL_BUILD: false PORT_GENERATED: true PREBUILD_REPO: nawaman/codingbooth RUN_ARGS: +SANDBOX: false SCRIPT_DIR: $(realpath "$HERE/../..") SCRIPT_NAME: codingbooth VARIANT: base @@ -125,6 +126,7 @@ LOCAL_BUILD: false PORT_GENERATED: false PREBUILD_REPO: nawaman/codingbooth RUN_ARGS: +SANDBOX: false SCRIPT_DIR: $(realpath "$HERE/../..") SCRIPT_NAME: codingbooth VARIANT: codeserver diff --git a/tests/dryrun/test013--config-file--args.sh b/tests/dryrun/test013--config-file--args.sh index 3aeb19eb..12bcd487 100755 --- a/tests/dryrun/test013--config-file--args.sh +++ b/tests/dryrun/test013--config-file--args.sh @@ -63,6 +63,7 @@ LOCAL_BUILD: false PORT_GENERATED: true PREBUILD_REPO: nawaman/codingbooth RUN_ARGS: +SANDBOX: false SCRIPT_DIR: $(realpath "$HERE/../..") SCRIPT_NAME: codingbooth VARIANT: base @@ -128,6 +129,7 @@ LOCAL_BUILD: true PORT_GENERATED: true PREBUILD_REPO: nawaman/codingbooth RUN_ARGS: \"-p\" \"10005\" +SANDBOX: false SCRIPT_DIR: $(realpath "$HERE/../..") SCRIPT_NAME: codingbooth VARIANT: base diff --git a/tests/manual/run-lifecycle-cross-user-manual-test.sh b/tests/manual/run-lifecycle-cross-user-manual-test.sh index 26a48161..aa368967 100755 --- a/tests/manual/run-lifecycle-cross-user-manual-test.sh +++ b/tests/manual/run-lifecycle-cross-user-manual-test.sh @@ -87,6 +87,17 @@ GOCACHE=/tmp/go-cache ./build/cli-build.sh >/dev/null cp -f ./codingbooth ./examples/playground/codingbooth chmod +x ./examples/playground/codingbooth +# Ensure sandbox allowlist exists for playground config +SANDBOX_ALLOWLIST="$PLAYGROUND_DIR/.booth/sandbox/allowlist.txt" +if [[ ! -f "$SANDBOX_ALLOWLIST" ]]; then + mkdir -p "$(dirname "$SANDBOX_ALLOWLIST")" + cat > "$SANDBOX_ALLOWLIST" <<'EOF' +pypi.org +files.pythonhosted.org +EOF + echo "Note: created missing sandbox allowlist at $SANDBOX_ALLOWLIST" +fi + echo "Step B: create keep-alive booth and commit image as current user" ( cd "$PLAYGROUND_DIR" diff --git a/tests/unit/run-docker-tests.sh b/tests/unit/run-docker-tests.sh index b7352ec8..5a661817 100755 --- a/tests/unit/run-docker-tests.sh +++ b/tests/unit/run-docker-tests.sh @@ -20,10 +20,18 @@ LOG_FILE="$(dirname "$0")/run-docker-tests.log" # Redirect output to log file and stdout exec > >(tee -i "$LOG_FILE") 2>&1 -# Check if docker is available +GOCACHE="${GOCACHE:-/tmp/codingbooth-go-build}" +mkdir -p "$GOCACHE" +export GOCACHE + +# Check if docker is available and accessible if ! command -v docker &> /dev/null; then - echo -e "${RED}Error: Docker is not installed or not in PATH${NC}" - exit 1 + echo -e "${YELLOW}SKIP: Docker is not installed or not in PATH${NC}" + exit 0 +fi +if ! docker info >/dev/null 2>&1; then + echo -e "${YELLOW}SKIP: Docker daemon is not accessible (permission or not running)${NC}" + exit 0 fi # Check if Go is available diff --git a/tests/unit/run-go-integration-tests.sh b/tests/unit/run-go-integration-tests.sh index 2360d49e..23e98adb 100755 --- a/tests/unit/run-go-integration-tests.sh +++ b/tests/unit/run-go-integration-tests.sh @@ -16,6 +16,19 @@ LOG_FILE="$SCRIPT_DIR/run-go-integration-tests.log" # Redirect output to log file and stdout exec > >(tee -i "$LOG_FILE") 2>&1 +GOCACHE="${GOCACHE:-/tmp/codingbooth-go-build}" +mkdir -p "$GOCACHE" +export GOCACHE + +if ! command -v docker >/dev/null 2>&1; then + echo "SKIP: Docker is not installed or not in PATH" + exit 0 +fi +if ! docker info >/dev/null 2>&1; then + echo "SKIP: Docker daemon is not accessible (permission or not running)" + exit 0 +fi + echo "========================================" echo "Running Go Integration Tests Only" echo "========================================" diff --git a/tests/unit/run-go-unit-tests.sh b/tests/unit/run-go-unit-tests.sh index 49db750b..b3e62ab1 100755 --- a/tests/unit/run-go-unit-tests.sh +++ b/tests/unit/run-go-unit-tests.sh @@ -16,6 +16,10 @@ LOG_FILE="$SCRIPT_DIR/run-go-unit-tests.log" # Redirect output to log file and stdout exec > >(tee -i "$LOG_FILE") 2>&1 +GOCACHE="${GOCACHE:-/tmp/codingbooth-go-build}" +mkdir -p "$GOCACHE" +export GOCACHE + echo "========================================" echo "Running Go Unit Tests Only" echo "========================================" diff --git a/variants/base/setups/claude-code--setup.sh b/variants/base/setups/claude-code--setup.sh index cd313d72..e8a24fc0 100755 --- a/variants/base/setups/claude-code--setup.sh +++ b/variants/base/setups/claude-code--setup.sh @@ -113,6 +113,11 @@ CLAUDE_JSON="$HOME/.claude.json" mkdir -p "$CLAUDE_DIR" +mkdir -p "/home/coder/.local/bin" +if [[ ! -e "/home/coder/.local/bin/claude" ]]; then + ln -s /usr/local/bin/claude "/home/coder/.local/bin/claude" +fi + # Copy .claude.json config file (contains hasCompletedOnboarding, theme, etc.) # This must happen BEFORE claude runs to skip onboarding wizard if [[ -f "$CB_SEED_JSON" && ! -f "$CLAUDE_JSON" ]]; then diff --git a/version.txt b/version.txt index d40e9e27..5b0cdcc6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.17.0--rc2 \ No newline at end of file +0.19.0--rc \ No newline at end of file