diff --git a/site/scripts/check-navigation.mjs b/site/scripts/check-navigation.mjs
index 653f797c..59520bef 100644
--- a/site/scripts/check-navigation.mjs
+++ b/site/scripts/check-navigation.mjs
@@ -102,4 +102,20 @@ export async function checkNavigation(page, base) {
assert.deepEqual(await page.evaluate(() => window.__navigationProbe), { loads: 3, blank: false, wrongTheme: false })
assert.equal(await page.locator('html').getAttribute('data-mcp-install-cooldown-enabled'), '1')
console.log('Navigation: document retained, Back, search, code tabs, menu and saved theme/cooldown pass')
+
+ const legacy = `${base}/go/latest/examples/workspace-from-file/`
+ const current = `${base}/go/latest/workspace/internals/examples/`
+ await page.goto(`${legacy}?from=legacy#where-this-comes-from`, { waitUntil: 'load' })
+ await page.waitForURL(`${current}?from=legacy#where-this-comes-from`)
+ assert.equal(await page.locator('#where-this-comes-from').count(), 1, 'Legacy section still exists')
+ const fallback = await page.context().browser().newContext({ javaScriptEnabled: false })
+ try {
+ const reader = await fallback.newPage()
+ await reader.goto(legacy, { waitUntil: 'load' })
+ await reader.waitForURL(current)
+ assert.match(await reader.locator('h1').textContent(), /Go workspace/)
+ } finally {
+ await fallback.close()
+ }
+ console.log('Redirects: legacy query/section links and the JavaScript-disabled fallback pass')
}
diff --git a/site/src/components/docs/RedirectPage.astro b/site/src/components/docs/RedirectPage.astro
new file mode 100644
index 00000000..cd7a8dd2
--- /dev/null
+++ b/site/src/components/docs/RedirectPage.astro
@@ -0,0 +1,31 @@
+---
+import { DEFAULT_LOCALE } from '../../i18n/locales'
+
+interface Props {
+ destination: string
+ title: string
+}
+
+const { destination, title } = Astro.props
+const canonical = new URL(destination, Astro.site ?? 'https://libtmux.org').href
+---
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+ Continue to {title}
+
+
diff --git a/site/src/content/docs/examples/workspace-from-file.md b/site/src/content/docs/examples/workspace-from-file.md
index 5d00c163..a2908803 100644
--- a/site/src/content/docs/examples/workspace-from-file.md
+++ b/site/src/content/docs/examples/workspace-from-file.md
@@ -1,7 +1,7 @@
---
-supportedPorts: [py, ts, rs, go, java, dotnet, cxx, swift]
+supportedPorts: []
title: Build a workspace from a file
-description: Describe a session in configuration, validate it, and build its windows and panes.
+description: Create tmux windows and panes from a command file on a private server.
sidebar:
label: Build a workspace from a file
group: Examples
@@ -9,242 +9,95 @@ sidebar:
tableOfContents: true
---
-[tmuxp](https://tmuxp.git-pull.com/) describes sessions, windows, panes, and
-shell commands in configuration files. A workspace builder turns that
-configuration into tmux objects. [Source details](#where-this-comes-from) identify the example
-files and their checks.
+A tmux command file can create a session, arrange its windows, and split its
+panes. This example builds two windows with three panes, prints their names
+and pane counts, then removes its private server. It requires tmux 3.2 or
+newer and a POSIX shell.
-
-For Python, use [tmuxp](https://tmuxp.git-pull.com/), a separate application
-built on libtmux's `Server`, `Session`, `Window`, and `Pane` APIs.
-
+## Define the layout
-```typescript file="examples/workspace/workspace.ts"
-```
-
-
-TypeScript's `applyWorkspace` reuses existing objects when the same
-configuration is applied again.
-
+Save this command file:
-```go file="workspace/example_test.go"
+```text title="workspace.conf"
+new-session -d -s dev -n editor -x 100 -y 30 'sh'
+split-window -h -t '=dev:editor' 'sh'
+new-window -t '=dev' -n logs 'sh'
+select-window -t '=dev:editor'
+select-pane -t '=dev:editor.0'
```
-
-Go's `Example()`, in `workspace/example_test.go`, is a Go `Example`
-function: `go test` runs it and checks its output against the
-`// Output:` comment at the end, so this is executed on every test run
-rather than merely present in a README. `Parse` rejects a field it doesn't
-recognize rather than dropping it silently, and reports every problem it
-finds at once with the line it's on. `Build` is not atomic: tmux has no
-transaction, so a failure partway through leaves whatever was already
-created in place, identified by the session `Build` still returns.
-
-
-```rust
-use libtmux::test::TestServer;
-use tmux_workspace::{Workspace, WorkspaceBuilder};
-
-let source = "
-session_name: dev
-windows:
- - window_name: editor
- panes: [/bin/sh, /bin/sh]
-";
-let workspace = Workspace::from_yaml(source)?;
-
-let guard = TestServer::new().await?;
-let session = WorkspaceBuilder::new(guard.server()).build(&workspace).await?;
-
-assert_eq!(session.name().to_string_lossy(), "dev");
-assert_eq!(session.windows().await?.len(), 1);
+The `editor` window has two panes; `logs` has one. Each pane starts `sh`.
+Explicit targets keep each command tied to the intended session and window.
+
+## Build and inspect it
+
+Save this complete program as `workspace.sh` beside the configuration file:
+
+```sh title="workspace.sh"
+(
+ set -eu
+ directory=$(mktemp -d "${TMPDIR:-/tmp}/libtmux-workspace.XXXXXX")
+ socket="$directory/tmux.sock"
+
+ cleanup() {
+ status=$?
+ trap - EXIT
+ if [ -S "$socket" ] && ! tmux -S "$socket" kill-server; then
+ printf '%s\n' "Cannot stop tmux; kept $directory." >&2
+ exit 1
+ fi
+ rm -rf "$directory" || status=$?
+ exit "$status"
+ }
+ trap cleanup EXIT
+ trap 'exit 1' HUP INT TERM
+
+ tmux -S "$socket" -f /dev/null start-server \; \
+ set-option -s exit-empty off
+ tmux -S "$socket" source-file ./workspace.conf
+ tmux -S "$socket" list-windows -t '=dev' \
+ -F '#{window_name}: #{window_panes} panes'
+)
```
-
-Rust's `freeze(&session).await?` exports an existing session to the workspace
-format. It recovers windows, panes, and working directories, but cannot recover
-the shell command originally typed to start a process.
-
-
-```java
-Workspace workspace = WorkspaceBuilder.parse("""
- session_name: built
- windows:
- - window_name: editor
- layout: even-horizontal
- panes:
- - shell_command: echo one
- - shell_command: echo two
- - window_name: server
- panes:
- - echo three
- """);
-
-Session session = WorkspaceBuilder.build(server, workspace);
-
-session.name(); // → built
-session.windows().size(); // → 2
-session.windows().get(0).panes().size(); // → 2
-```
-
-
-Java's `read` and `parse` validate the configuration and return a `Workspace`
-value. Only `build` changes tmux state.
-
-
-```csharp
-WorkspaceFile workspace = WorkspaceFile.Parse("""
- session_name: api
- start_directory: /tmp
- windows:
- - window_name: editor
- panes:
- - shell_command: echo editing
- - window_name: server
- panes:
- - shell_command: echo serving
- """);
-
-WorkspaceResult result = await new WorkspaceBuilder(server).BuildAsync(workspace, ct);
-Console.WriteLine($"{result.Session.Name}: {result.Windows.Count} windows");
-```
+Run it from that directory:
-
-The .NET builder can wait for shell readiness before sending commands; [Sending
-keys](/guides/sending-keys/#the-race-you-cant-see-from-the-call-site) explains
-the startup race. It polls `pane_current_command`, `cursor_x`, and `cursor_y`
-for up to ten seconds by default. `PaneReadiness.Auto` waits for zsh, `Always`
-waits for every pane running the session's default shell, and `Never` sends
-immediately. If `BuildAsync` fails partway through,
-`WorkspaceBuildException.PartialResult` identifies what was created.
-
-
-
-C++'s `examples/workspace/` implements a consumer of the core API with its own
-`workspace.hpp` and `tmuxp.hpp` types. Those types are part of the example, not
-the library package. See [the example's
-README](https://github.com/libtmux/libtmux-cxx/tree/main/examples/workspace) to
-adapt it.
-
-
-```swift file="Examples/Sources/ExampleCode/Workspaces.swift"
+```console
+$ sh workspace.sh
```
-
-Swift's `WorkspaceBuilder.build` rejects an existing session with the requested
-name. `Workspace.decode(yaml:)` reads tmuxp YAML when the `YAMLWorkspaces` trait
-is enabled. `Workspace.decode(json:)` needs no additional trait.
-
-
-## Where this comes from
-
-
-
-### Python
-
-
-**Source:** Not listed.
-
-**In this page:** no fence; the README says tmuxp is a separate project by
-design
-
-**Checked by:** n/a
-
-
-
-
-### TypeScript
-
-
-**Source:** `examples/workspace/workspace.ts` (`@libtmux/workspace`)
-
-**In this page:** read whole from the file
-
-**Checked by:** run against real tmux by `bun test examples/workspace`
-
-
-
-
-### Go
-
-
-**Source:** `workspace/example_test.go` (`workspace.Parse` / `workspace.Build`)
-
-**In this page:** read whole from the file
+The result is:
-**Checked by:** `Example()` and its siblings run under `go test` and are checked
-against their own `// Output:` comments
-
-
-
-
-### Rust
-
-
-**Source:** `crates/tmux-workspace/README.md`, "Build it"
-
-**In this page:** hand-quoted
-
-**Checked by:** the crate's own `crates/tmux-workspace/src/lib.rs` includes the
-README as a doc comment (`#![doc = include_str!("../README.md")]`), so `cargo
-test --doc` runs this exact block
-
-
-
-
-### Java
-
-
-**Source:** `libtmux-workspace/README.md`, "What you get back"
-
-**In this page:** hand-quoted
-
-**Checked by:** every Java fence in the module's README is compiled and run
-against real tmux by `docs-tests`
-
-
-
-
-### .NET
-
-
-**Source:** `src/LibTmux.Workspace/README.md`
-
-**In this page:** hand-quoted
-
-**Checked by:** one of the READMEs and docs `ReadmeExampleTests` compiles and
-runs against real tmux
-
-
-
-
-### C++
-
-
-**Source:** `examples/workspace/` (a consumer, not a library API)
-
-**In this page:** prose only
+```text
+editor: 2 panes
+logs: 1 panes
+```
-**Checked by:** `examples/workspace/tests/` runs the consumer suite against
-real tmux; `ctest -R consumer.workspace` selects it. It exercises the
-example's own types, not a published `libtmux` API
-
+`source-file` executes the configuration on the private server. Setting
+`exit-empty` to `off` keeps that server available for cleanup even when a
+configuration error prevents session creation. Errors remain visible and
+make the program fail. Cleanup runs after partial creation too; if stopping
+tmux fails, the script keeps the socket directory and reports its location.
-
-
-### Swift
-
+## Use a workspace manager
-**Source:** `Examples/Sources/ExampleCode/Workspaces.swift`
+For YAML or JSON configuration, validation, and language APIs, choose a port:
-**In this page:** read whole from the file
+[Python CLI](/py/latest/workspace/examples/) ·
+[TypeScript](/ts/latest/workspace/internals/examples/) ·
+[Go](/go/latest/workspace/internals/examples/) ·
+[Rust](/rs/latest/workspace/internals/examples/) ·
+[Java](/java/latest/workspace/internals/examples/) ·
+[.NET](/dotnet/latest/workspace/internals/examples/) ·
+[C++](/cxx/latest/workspace/internals/examples/) ·
+[Swift](/swift/latest/workspace/internals/examples/)
-**Checked by:** matched against the README's "Workspaces, from a file or from
-Swift" section by `Scripts/check_examples.py`; compiled and run by `swift test
---package-path Examples`
-
+The [workspace concept guide](/concepts/workspaces/) explains configuration
+and ownership. The [capture example](/examples/capture-pane-output/) shows
+how to wait for pane output after sending a command.
-### Source inclusion
+
+
-Some blocks are excerpts from tested README examples. The source
-details above identify those files and their checks.
+The [tmux manual source](https://github.com/tmux/tmux/blob/94796f6b1182507efac8a272fc309a79e22e58a5/tmux.1)
+describes `source-file`, window targets, and `exit-empty`.
diff --git a/site/src/content/docs/ports/cxx/workspace/internals/examples.md b/site/src/content/docs/ports/cxx/workspace/internals/examples.md
index 1045ce21..8ee1cc5f 100644
--- a/site/src/content/docs/ports/cxx/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/cxx/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: "C++ workspace builder examples"
description: "Internal examples for building and inspecting workspaces through the C++ API."
port: cxx
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -115,4 +116,7 @@ This header belongs to the source consumer. Installing the core package alone
does not supply the workspace include directory. For YAML input, the separate
consumer build also needs its parser and yaml-cpp dependency.
+
+
+
[Workspace builder source](https://github.com/libtmux/libtmux-cxx/blob/393d4b0ad666f18a6581f1eb281741a75a7503f0/examples/workspace/include/libtmux_consumers/workspace.hpp)
diff --git a/site/src/content/docs/ports/dotnet/workspace/internals/examples.md b/site/src/content/docs/ports/dotnet/workspace/internals/examples.md
index 09be21b8..bcfac208 100644
--- a/site/src/content/docs/ports/dotnet/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/dotnet/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: ".NET workspace builder examples"
description: "Internal examples for building and inspecting workspaces through the .NET API."
port: dotnet
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -110,4 +111,7 @@ owned-scope cleanup uses its own lifetime.
That does not establish completion of programs running in the panes. A build
failure can leave partial results; the owned server scope removes them here.
+
+
+
[Workspace API source](https://github.com/libtmux/libtmux-dotnet/tree/320dc64f4b8b7815842471327a5e6b84a1499bf8/src/LibTmux.Workspace)
diff --git a/site/src/content/docs/ports/go/workspace/internals/examples.md b/site/src/content/docs/ports/go/workspace/internals/examples.md
index 8026f5cd..df5219ba 100644
--- a/site/src/content/docs/ports/go/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/go/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: Go workspace builder examples
description: Build and inspect a workspace from YAML on a private tmux server with Go.
port: go
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -185,5 +186,8 @@ error.
For a workspace that stays open, let your application retain its explicitly
selected server and choose when to stop it.
+
+
+
[Builder source](https://github.com/libtmux/libtmux-go/blob/bb06e26e116e941813ca40bf45e7e3a47d38f52a/workspace/builder.go);
[configuration source](https://github.com/libtmux/libtmux-go/blob/bb06e26e116e941813ca40bf45e7e3a47d38f52a/workspace/workspace.go).
diff --git a/site/src/content/docs/ports/java/workspace/internals/examples.md b/site/src/content/docs/ports/java/workspace/internals/examples.md
index 012962c6..abb62ab1 100644
--- a/site/src/content/docs/ports/java/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/java/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: "Java workspace builder examples"
description: "Internal examples for building and inspecting workspaces through the Java API."
port: java
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -124,4 +125,7 @@ Building creates the layout and sends the pane commands. It does not wait for
those programs to finish. Keep the session running instead of calling
`killServer` when adapting this example into an application launcher.
+
+
+
[Workspace API source](https://github.com/libtmux/libtmux-java/tree/842228310449e879ebcaa3f910597757c9dbffd6/libtmux-workspace/src/main/java/io/github/libtmux/workspace)
diff --git a/site/src/content/docs/ports/py/workspace/examples.md b/site/src/content/docs/ports/py/workspace/examples.md
index e0afad60..5094cf91 100644
--- a/site/src/content/docs/ports/py/workspace/examples.md
+++ b/site/src/content/docs/ports/py/workspace/examples.md
@@ -3,6 +3,7 @@ title: "Python workspace examples"
description: "Load tmux workspaces from YAML or JSON with the tmuxp CLI."
port: py
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
label: Examples
order: 3
@@ -83,4 +84,7 @@ selection, attachment, and existing sessions.
For contributors studying how a file becomes a session, see the
[internal builder example](../internals/examples/).
+
+
+
[Two-pane YAML source](https://github.com/tmux-python/tmuxp/blob/618b398acc05506d3c682906c36cdeb29dcfa1ff/examples/2-pane-vertical.yaml)
diff --git a/site/src/content/docs/ports/rs/workspace/internals/examples.md b/site/src/content/docs/ports/rs/workspace/internals/examples.md
index b592cbda..0ca1a15b 100644
--- a/site/src/content/docs/ports/rs/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/rs/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: Rust workspace builder examples
description: Build, inspect, and capture a workspace on a private tmux server with Rust.
port: rs
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -142,5 +143,8 @@ shutdown both fail, the error includes both failures. For a persistent
workspace, let your application retain an explicitly selected server and
choose its own shutdown point.
+
+
+
[Builder source](https://github.com/libtmux/libtmux-rs/blob/d4e08b4eaab62ef4eeedab79b47973ae9a1de310/crates/tmux-workspace/src/lib.rs);
[capture source](https://github.com/libtmux/libtmux-rs/blob/d4e08b4eaab62ef4eeedab79b47973ae9a1de310/crates/tmux-workspace/src/freeze.rs).
diff --git a/site/src/content/docs/ports/swift/workspace/internals/examples.md b/site/src/content/docs/ports/swift/workspace/internals/examples.md
index e7f7a7bc..f52be278 100644
--- a/site/src/content/docs/ports/swift/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/swift/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: Swift workspace builder examples
description: Build and inspect a private workspace with a complete Swift program.
port: swift
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -166,4 +167,7 @@ Describe panes with Swift values or decode a configuration with
`Workspace.decode(json:)`. The [guide](../guides/) explains ownership,
configuration input and failed-build cleanup.
+
+
+
[Library source](https://github.com/libtmux/libtmux-swift/tree/254f8b2be7eb60cacc3ffcb3ea8e456784f582df/Sources/TmuxWorkspace).
diff --git a/site/src/content/docs/ports/ts/workspace/internals/examples.md b/site/src/content/docs/ports/ts/workspace/internals/examples.md
index 0537fe50..f31e698d 100644
--- a/site/src/content/docs/ports/ts/workspace/internals/examples.md
+++ b/site/src/content/docs/ports/ts/workspace/internals/examples.md
@@ -3,6 +3,7 @@ title: TypeScript workspace builder examples
description: Build and inspect a workspace on a private tmux server with TypeScript.
port: ts
product: workspace
+aliases: [examples/workspace-from-file]
sidebar:
group: Internals
label: Examples
@@ -139,5 +140,8 @@ For a workspace that stays open, let the application retain its explicitly
selected server and choose when to stop it. A failed build can leave partial
work; the example's server cleanup removes it.
+
+
+
[Example source](https://github.com/libtmux/libtmux-ts/blob/3fe1ca654b81b8cbf4a13b777a001a3298c87a6f/examples/workspace/workspace.ts);
[Workspace builder source](https://github.com/libtmux/libtmux-ts/blob/3fe1ca654b81b8cbf4a13b777a001a3298c87a6f/packages/workspace/src/builder.ts).
diff --git a/site/src/data/example-sources.json b/site/src/data/example-sources.json
index ef4582d9..1509e414 100644
--- a/site/src/data/example-sources.json
+++ b/site/src/data/example-sources.json
@@ -17,12 +17,6 @@
"sha256": "28725dec488dc4b80ef9812d328f1df2fce947d975ac6fc406e8bea85670545a",
"content": "// Command quickstart demonstrates a complete session, window, and pane lifecycle.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/libtmux/libtmux-go/tmux\"\n)\n\nfunc main() {\n\tif err := start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\n// start owns cleanup because log.Fatal skips deferred calls in main.\nfunc start() error {\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tserver, err := tmux.NewServer(tmux.ServerOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"configure tmux server: %w\", err)\n\t}\n\treturn run(ctx, server)\n}\n\n// run accepts injected server state so tests can isolate the example.\nfunc run(ctx context.Context, server tmux.Server) (err error) {\n\t// docs:quickstart given:ctx context.Context; server tmux.Server\n\tsession, err := server.NewSession(ctx, tmux.NewSessionRequest{\n\t\tName: \"libtmux-go-quickstart\", WindowName: \"start\",\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create session: %w\", err)\n\t}\n\tdefer func() {\n\t\tcleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)\n\t\tdefer cleanupCancel()\n\t\terr = errors.Join(err, session.Kill(cleanupCtx))\n\t}()\n\n\twindow, err := session.NewWindow(ctx, tmux.NewWindowRequest{Name: new(\"work\")})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create window: %w\", err)\n\t}\n\tpane, err := window.SplitPane(ctx, tmux.SplitPaneRequest{\n\t\tDirection: tmux.PaneDirectionRight, Command: \"sh\",\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"split window: %w\", err)\n\t}\n\toutput, err := pane.OpenObservation(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"watch pane: %w\", err)\n\t}\n\tdefer func() { err = errors.Join(err, output.Close()) }()\n\tif _, err := fmt.Fprintln(pane.Writer(ctx), \"printf 'libtmux ready\\\\n'\"); err != nil {\n\t\treturn fmt.Errorf(\"send command: %w\", err)\n\t}\n\t// docs:end\n\n\tscanner := bufio.NewScanner(output.Reader(ctx))\n\tfor scanner.Scan() {\n\t\tif scanner.Text() == \"libtmux ready\" {\n\t\t\tfmt.Println(\"libtmux ready\")\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"read pane: %w\", scanner.Err())\n}\n"
},
- "go:workspace/example_test.go": {
- "repository": "libtmux/libtmux-go",
- "revision": "bb06e26e116e941813ca40bf45e7e3a47d38f52a",
- "sha256": "2eaaf9c21bb471139af18b70585554b61ca0463b16486984e5aa4ecbc8368e8b",
- "content": "package workspace_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/libtmux/libtmux-go/tmux\"\n\t\"github.com/libtmux/libtmux-go/workspace\"\n)\n\n// Load a tmuxp-style document and build the session it describes.\nfunc Example() {\n\tctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n\tdefer cancel()\n\tserver, err := tmux.NewServer(tmux.ServerOptions{\n\t\tSocketName: \"libtmux-go-example-workspace\",\n\t})\n\tif err != nil {\n\t\tfmt.Println(\"server:\", err)\n\t\treturn\n\t}\n\tdefer killExampleServer(server)\n\n\tdocument := []byte(`\nsession_name: review\nwindows:\n - window_name: editor\n panes:\n - shell_command: printf 'ready\\n'\n - window_name: tests\n panes:\n - shell_command: printf 'ready\\n'\n - shell_command: printf 'ready\\n'\n`)\n\tparsed, err := workspace.Parse(document)\n\tif err != nil {\n\t\tfmt.Println(\"parse:\", err)\n\t\treturn\n\t}\n\n\tsession, err := workspace.Build(ctx, server, parsed)\n\tif err != nil {\n\t\tfmt.Println(\"build:\", err)\n\t\treturn\n\t}\n\n\tname, _ := session.Name()\n\twindows, err := session.SearchWindows(ctx, nil)\n\tif err != nil {\n\t\tfmt.Println(\"search windows:\", err)\n\t\treturn\n\t}\n\tfmt.Println(name, len(windows))\n\t// Output: review 2\n}\n\n// Create the initial session, prefer a retained connection where tmux supports\n// one, then populate the rest of the workspace through that session.\nfunc ExampleBuildInto() {\n\tctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n\tdefer cancel()\n\tserver, err := tmux.NewServer(tmux.ServerOptions{\n\t\tSocketName: \"libtmux-go-example-workspace-continue\",\n\t})\n\tif err != nil {\n\t\tfmt.Println(\"server:\", err)\n\t\treturn\n\t}\n\tdefer killExampleServer(server)\n\n\tdescribed := workspace.Workspace{\n\t\tSessionName: \"review\",\n\t\tWindows: []workspace.Window{\n\t\t\t{Name: \"editor\", Panes: []workspace.Pane{{Shell: \"sleep 60\"}}},\n\t\t\t{Name: \"tests\", Panes: []workspace.Pane{{Shell: \"sleep 60\"}}},\n\t\t},\n\t}\n\trequest, err := described.InitialSessionRequest()\n\tif err != nil {\n\t\tfmt.Println(\"request:\", err)\n\t\treturn\n\t}\n\t_, connection, err := server.NewSessionConnection(\n\t\tctx,\n\t\trequest,\n\t\ttmux.ConnectionOptions{},\n\t)\n\tif err != nil {\n\t\tfmt.Println(\"create:\", err)\n\t\treturn\n\t}\n\tdefer func() { _ = connection.Close() }()\n\tsession := connection.Session()\n\tif err := workspace.BuildInto(ctx, session, described); err != nil {\n\t\tfmt.Println(\"build:\", err)\n\t\treturn\n\t}\n\twindows, err := session.SearchWindows(ctx, nil)\n\tif err != nil {\n\t\tfmt.Println(\"search windows:\", err)\n\t\treturn\n\t}\n\tfmt.Println(len(windows))\n\t// Output: 2\n}\n\n// A misspelled key fails the parse rather than being dropped, so a workspace\n// that does not do what its author meant says so before anything is built.\nfunc ExampleParse_unknownField() {\n\t_, err := workspace.Parse([]byte(\"session_name: review\\nwindow:\\n - {}\\n\"))\n\tfmt.Println(err != nil)\n\t// Output: true\n}\n\n// killExampleServer stops an example's server on a context of its own, since an\n// example's own context may already be spent by the time it returns.\nfunc killExampleServer(server tmux.Server) {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\t_ = server.Kill(ctx)\n}\n"
- },
"java:examples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.java": {
"repository": "libtmux/libtmux-java",
"revision": "842228310449e879ebcaa3f910597757c9dbffd6",
@@ -89,22 +83,10 @@
"sha256": "b51fb2fce7c3df5571e8d596c3ca2d636cb38b2e13a8f9d6d278d1d64010f4a7",
"content": "// The examples in the README's \"Change what is there\" section.\n\nimport LibTmux\n\npublic func buildASessionByHand(_ server: Server) async throws -> Pane {\n let session = try await server.newSession(named: \"work\", windowName: \"editor\")\n _ = try await server.setOption(\"@purpose\", to: \"development\", scope: .session(session))\n let logs = try await server.newWindow(in: session, named: \"logs\").window\n let pane = try await server.splitWindow(logs, direction: .right)\n try await server.run(\"tail -f /tmp/build.log\", in: pane)\n return pane\n}\n\npublic func readBackWhatAPanePrinted(_ server: Server, _ pane: Pane) async throws -> [String] {\n let lines = try await server.capture(pane)\n print(lines.suffix(5).joined(separator: \"\\n\"))\n return lines\n}\n\npublic func spendOneProcessOnAllOfIt(_ server: Server) async throws {\n var plan = TmuxCommandList()\n for name in [\"edit\", \"test\", \"logs\"] {\n plan = plan.then(\"new-window\", [\"-d\", \"-n\", name])\n }\n _ = try await server.run(plan)\n}\n"
},
- "swift:Examples/Sources/ExampleCode/Workspaces.swift": {
- "repository": "libtmux/libtmux-swift",
- "revision": "254f8b2be7eb60cacc3ffcb3ea8e456784f582df",
- "sha256": "a721e1f287c7b492a598edee125a8905caf8b10a886e53d53291ffd5b99432ac",
- "content": "// The examples in the README's `WorkspaceBuilder` section.\n\nimport Foundation\nimport LibTmux\nimport TmuxWorkspace\n\npublic func describeAWorkspaceInSwift() -> Workspace {\n let workspace = Workspace(\n sessionName: \"work\",\n windows: [\n WindowPlan(\n windowName: \"editor\",\n layout: \"even-horizontal\",\n panes: [PanePlan(), PanePlan()]\n ),\n WindowPlan(\n windowName: \"logs\",\n panes: [PanePlan(shellCommands: [\"tail -f /tmp/build.log\"])]\n ),\n ]\n )\n return workspace\n}\n\npublic func buildItOnAServer(_ server: Server, _ workspace: Workspace) async throws -> Session {\n let session = try await WorkspaceBuilder.build(workspace, on: server)\n print(session.name, session.windowCount)\n return session\n}\n\npublic func readAWorkspaceWrittenAsJSON(_ json: Data) throws -> Workspace {\n try Workspace.decode(json: json)\n}\n\n// Deliberately NOT wrapped in `#if YAMLWorkspaces`. A trait defines its\n// compilation condition only inside the package that declares it, so that guard\n// in a consumer package is always false and deletes the example — loudly if a\n// test calls it, silently if nothing does. The symbol is nonetheless here,\n// because the dependency was resolved with the trait on.\npublic func readATmuxpFile(_ text: String) throws -> Workspace {\n try Workspace.decode(yaml: text)\n}\n"
- },
"ts:examples/quickstart/quickstart.ts": {
"repository": "libtmux/libtmux-ts",
"revision": "3fe1ca654b81b8cbf4a13b777a001a3298c87a6f",
"sha256": "4058cfefdbd2a54c69f9da93a3d57dfe2f49abd20af8256a42456fd7947c186b",
"content": "import { Server, TmuxCommandError, type ServerSnapshot } from \"libtmux\";\n\n/**\n * A runnable tour of the API, driven by the tests so it cannot rot.\n *\n * Every step here appears in README.md.\n */\nexport async function quickstart(server: Server): Promise {\n // Nothing is read until you ask. `snapshot()` is the only step that talks to\n // tmux; everything reachable from it resolves locally.\n const session = await server.newSession({ name: \"quickstart\" });\n const editor = await session.newWindow({ name: \"editor\" });\n await editor.split();\n\n const snapshot = await server.snapshot();\n\n // Declarative filtering, serializable and stable on the wire.\n const found = snapshot.windows.where({ name: \"editor\" }).one();\n\n // Relations are plain properties: no await, no tmux command.\n const paneCount = found.panes.length;\n if (paneCount !== 2) throw new Error(`expected two panes, saw ${String(paneCount)}`);\n\n // A criterion is spelled like the handle accessor it filters.\n if (snapshot.panes.count({ currentCommand: { contains: \"\" } }) === 0) {\n throw new Error(\"expected panes to report a current command\");\n }\n const first = found.panes.at(0);\n if (first === undefined) throw new Error(\"expected a pane\");\n await first.sendKeys(\"echo hello-from-libtmux\", { literal: true });\n\n // Failures carry their parts rather than a formatted sentence.\n try {\n await server.setOption(\"not-a-real-option\", \"1\");\n } catch (error) {\n if (!(error instanceof TmuxCommandError)) throw error;\n if (error.args[0] !== \"set-option\") throw error;\n }\n\n return snapshot;\n}\n"
- },
- "ts:examples/workspace/workspace.ts": {
- "repository": "libtmux/libtmux-ts",
- "revision": "3fe1ca654b81b8cbf4a13b777a001a3298c87a6f",
- "sha256": "35ffadc734aad94422f0e7c939bd448db97e13130d1c79047ba9c343a0672cf2",
- "content": "import type { Server } from \"libtmux/server\";\nimport type { Session } from \"libtmux/session\";\nimport { applyWorkspace } from \"@libtmux/workspace\";\nimport type { WorkspaceInput } from \"@libtmux/workspace/config\";\n\n/**\n * Build the shape most people reach for tmux to get: one session, a window\n * per concern, each pane already running the thing it is there for.\n *\n * `server.batch` plans several windows and resolves them from one final\n * snapshot. `buildWorkspace`, below, delegates declared topology to the\n * workspace package instead of maintaining another reconciler here.\n */\nexport async function buildSimpleWorkspace(server: Server): Promise {\n const built = await server.newSession({\n name: \"work\",\n shellCommand: \"sleep 30\",\n windowName: \"editor\",\n });\n\n const [logs, shell] = await server.batch([\n built.plan.newWindow({ name: \"logs\", shellCommand: \"tail -f /dev/null\" }),\n built.plan.newWindow({ name: \"shell\" }),\n ]);\n\n await logs.selectLayout(\"even-horizontal\");\n void shell.name; // \"shell\"\n\n return built;\n}\n\nexport const DEVELOPMENT_WORKSPACE = {\n session_name: \"workspace-example\",\n windows: [\n { panes: [\"sleep 30\", \"sleep 30\"], window_name: \"editor\" },\n { panes: [\"sleep 30\"], window_name: \"server\" },\n { window_name: \"logs\" },\n ],\n} satisfies WorkspaceInput;\n\n/** Apply the package's declarative workspace to a server. */\nexport function buildWorkspace(server: Server): Promise {\n return applyWorkspace(server, DEVELOPMENT_WORKSPACE);\n}\n\n/**\n * Tear a workspace down without caring whether it is there.\n *\n * Killing a session that has already gone is not a failure worth propagating,\n * which is the one case worth handling separately from every other tmux error.\n */\nexport async function removeWorkspace(server: Server, name: string): Promise {\n const found = (await server.snapshot()).sessions.first({ name });\n if (found === undefined) return false;\n await found.kill();\n return true;\n}\n"
}
}
diff --git a/site/src/data/mentions.json b/site/src/data/mentions.json
index a291d5b2..68c1b55a 100644
--- a/site/src/data/mentions.json
+++ b/site/src/data/mentions.json
@@ -1267,13 +1267,6 @@
"title": ".NET workspace builder API",
"section": "ports"
},
- {
- "port": "dotnet",
- "symbol": "LibTmux.Workspace.PaneReadiness.Always",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "dotnet",
"symbol": "LibTmux.Workspace.PaneReadiness.Auto",
@@ -1288,13 +1281,6 @@
"title": ".NET workspace builder API",
"section": "ports"
},
- {
- "port": "dotnet",
- "symbol": "LibTmux.Workspace.PaneReadiness.Auto",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "dotnet",
"symbol": "LibTmux.Workspace.PaneReadiness.Never",
@@ -1309,13 +1295,6 @@
"title": ".NET workspace builder API",
"section": "ports"
},
- {
- "port": "dotnet",
- "symbol": "LibTmux.Workspace.PaneReadiness.Never",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "dotnet",
"symbol": "LibTmux.Workspace.WorkspaceBuilder",
@@ -1358,13 +1337,6 @@
"title": ".NET workspace builder API",
"section": "ports"
},
- {
- "port": "dotnet",
- "symbol": "LibTmux.Workspace.WorkspaceBuilder.BuildAsync",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "dotnet",
"symbol": "LibTmux.Workspace.WorkspaceBuildException",
@@ -1400,13 +1372,6 @@
"title": ".NET workspace builder API",
"section": "ports"
},
- {
- "port": "dotnet",
- "symbol": "LibTmux.Workspace.WorkspaceBuildException.PartialResult",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "dotnet",
"symbol": "LibTmux.Workspace.WorkspaceFile",
@@ -2093,13 +2058,6 @@
"title": "Go workspace builder API",
"section": "ports"
},
- {
- "port": "go",
- "symbol": "workspace.Build",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "go",
"symbol": "workspace.Build",
@@ -2198,13 +2156,6 @@
"title": "Go workspace builder API",
"section": "ports"
},
- {
- "port": "go",
- "symbol": "workspace.Parse",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "go",
"symbol": "workspace.Parse",
@@ -2779,13 +2730,6 @@
"title": "Java workspace builder API",
"section": "ports"
},
- {
- "port": "java",
- "symbol": "io.github.libtmux.workspace.Workspace.Workspace",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "java",
"symbol": "io.github.libtmux.workspace.Workspace.Workspace",
@@ -4207,13 +4151,6 @@
"title": "Architecture",
"section": "topics"
},
- {
- "port": "py",
- "symbol": "libtmux.pane.Pane",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "py",
"symbol": "libtmux.pane.Pane",
@@ -4326,13 +4263,6 @@
"title": "Testing with libtmux",
"section": "guides"
},
- {
- "port": "py",
- "symbol": "libtmux.server.Server",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "py",
"symbol": "libtmux.server.Server",
@@ -4466,13 +4396,6 @@
"title": "Workspaces",
"section": "concepts"
},
- {
- "port": "py",
- "symbol": "libtmux.session.Session",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "py",
"symbol": "libtmux.session.Session",
@@ -4557,13 +4480,6 @@
"title": "Workspaces",
"section": "concepts"
},
- {
- "port": "py",
- "symbol": "libtmux.window.Window",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "py",
"symbol": "libtmux.window.Window",
@@ -5026,13 +4942,6 @@
"title": "Options and hooks",
"section": "topics"
},
- {
- "port": "rs",
- "symbol": "freeze.freeze",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "rs",
"symbol": "freeze.freeze",
@@ -6692,13 +6601,6 @@
"title": "Swift workspace builder API",
"section": "ports"
},
- {
- "port": "swift",
- "symbol": "Workspace.decode(json:)",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "swift",
"symbol": "Workspace.decode(json:)",
@@ -6720,13 +6622,6 @@
"title": "Swift workspace builder API",
"section": "ports"
},
- {
- "port": "swift",
- "symbol": "Workspace.decode(yaml:)",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "swift",
"symbol": "Workspace.decode(yaml:)",
@@ -6769,13 +6664,6 @@
"title": "Swift workspace builder API",
"section": "ports"
},
- {
- "port": "swift",
- "symbol": "WorkspaceBuilder.build(_:on:)",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "swift",
"symbol": "WorkspaceBuilder.build(_:on:)",
@@ -6832,13 +6720,6 @@
"title": "Workspaces",
"section": "concepts"
},
- {
- "port": "ts",
- "symbol": "builder.applyWorkspace",
- "page": "/examples/workspace-from-file/",
- "title": "Build a workspace from a file",
- "section": "examples"
- },
{
"port": "ts",
"symbol": "builder.applyWorkspace",
@@ -7513,13 +7394,6 @@
"line": 152,
"why": "not defined in the stated port"
},
- {
- "port": "swift",
- "text": "YAMLWorkspaces",
- "page": "/examples/workspace-from-file/",
- "line": 120,
- "why": "not defined in the stated port"
- },
{
"port": "dotnet",
"text": "IEnumerable.Matching(expression)",
diff --git a/site/src/pages/[...slug].astro b/site/src/pages/[...slug].astro
index a4bb2a7e..e799f38f 100644
--- a/site/src/pages/[...slug].astro
+++ b/site/src/pages/[...slug].astro
@@ -1,6 +1,7 @@
---
import { getCollection, type CollectionEntry } from 'astro:content'
import DocPage from '../components/docs/DocPage.astro'
+import RedirectPage from '../components/docs/RedirectPage.astro'
import McpOverview from '../components/mcp/Overview.astro'
import McpTools from '../components/mcp/Tools.astro'
import ProductApiPage from '../components/api/ProductApiPage.astro'
@@ -70,7 +71,7 @@ interface Props {
toolName?: string
}
const { entry, placeholderLocale, globalPage, version, model, symbol, mcpPort, toolName, redirect } = Astro.props
-if (redirect) return Astro.redirect(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/${redirect}/`, 301)
+const destination = redirect ? `${import.meta.env.BASE_URL.replace(/\/$/, '')}/${redirect}/` : undefined
---
-{globalPage === 'mcp' ? : globalPage === 'tools' ? : mcpPort ? : model && symbol ? : entry && }
+{destination ? : globalPage === 'mcp' ? : globalPage === 'tools' ? : mcpPort ? : model && symbol ? : entry && }
diff --git a/site/src/pages/[port]/index.astro b/site/src/pages/[port]/index.astro
index 25c6c665..75753b53 100644
--- a/site/src/pages/[port]/index.astro
+++ b/site/src/pages/[port]/index.astro
@@ -4,14 +4,15 @@ import { IS_ROOT_BUILD } from '../../lib/site-root'
import { defaultVersionFor } from '../../lib/versions'
import { DEFAULT_LOCALE } from '../../i18n/locales'
import { buildLocale } from '../../i18n/resolve'
+import RedirectPage from '../../components/docs/RedirectPage.astro'
/**
* `//`: a redirect to the port's default version.
*
* The edge function answers this URL with a 302 from its KeyValueStore
* (infra/cloudfront-function.js rule 1), so readers reach this page only
- * when that lookup misses. It takes the shape of Astro's own static redirect
- * (`redirectTemplate`): meta refresh, `noindex`, canonical, visible link.
+ * when that lookup misses. The static fallback preserves query strings and
+ * section links, with a meta refresh for browsers without JavaScript.
*/
export function getStaticPaths() {
// Root build only: every port+version build renders this route too.
@@ -31,19 +32,6 @@ interface Props {
const { port } = Astro.props as Props
const destination = portHomeUrl(port, defaultVersionFor(port.slug))
-const canonical = new URL(destination, Astro.site ?? 'https://libtmux.org').href
---
-
-
-
-
- {port.name} documentation
-
-
-
-
-
- Continue to the {port.name} documentation
-
-
+
diff --git a/site/test/complete-examples.test.ts b/site/test/complete-examples.test.ts
index 56bcddbd..8ea70dcf 100644
--- a/site/test/complete-examples.test.ts
+++ b/site/test/complete-examples.test.ts
@@ -8,7 +8,7 @@ import products from './fixtures/product-examples.json'
import { remarkPortCode, resolvePortCode } from '../src/plugins/remark-port-code.mjs'
import { rehypeCodeTabs } from '../src/plugins/rehype-code-tabs.mjs'
import { docsEntryAvailable, pagePortLinks } from '../src/lib/page-port-links'
-import { docsRoutePath } from '../src/lib/docs-paths'
+import { docsPath, docsRedirects, docsRoutePath } from '../src/lib/docs-paths'
const readPage = (page: string) => readFileSync(new URL(`../src/content/docs/${page}.md`, import.meta.url), 'utf8')
const parsePage = (page: string) => parseFrontmatter(readPage(page), { frontmatter: 'remove' })
@@ -96,4 +96,40 @@ describe('verified complete programs', () => {
expect(links.filter((link) => link.links.length).map((link) => link.port).sort())
.toEqual(receipt.examples.map((example) => example.port).sort())
})
+
+ it('keeps the root workspace runnable and redirects legacy port pages to their owned examples', () => {
+ const example = products.rootWorkspace
+ const { content, frontmatter } = parsePage(example.page)
+ expect(frontmatter.supportedPorts).toEqual([])
+ const blocks = fences(content)
+ expect(blocks.every((block) => ['sh', 'text', 'console'].includes(block.language))).toBe(true)
+ for (const file of example.files) {
+ const matching = blocks.filter((block) => block.title === file.name)
+ expect(matching).toHaveLength(1)
+ expect(sha256(matching[0].code)).toBe(file.sha256)
+ }
+ expect(blocks.filter((block) => block.language === 'console')
+ .map((block) => block.code.replace(/^\$ /gm, '').trim())).toEqual(example.shellRecipe)
+ const docs = [{ id: example.page, data: frontmatter }, ...example.ports.map((port) => {
+ const id = `ports/${port}/workspace/${port === 'py' ? '' : 'internals/'}examples`
+ const { frontmatter: data } = parsePage(id)
+ expect(data.port).toBe(port)
+ expect(data.aliases).toContain(example.page)
+ expect(content).toContain(`/${port}/latest/${docsPath({ id, data })}/`)
+ return { id, data }
+ })]
+ const links = pagePortLinks({ pagePath: example.page, version: 'latest', defaults: {}, docs })
+ expect(links.filter((link) => link.links.length).map((link) => link.port).sort())
+ .toEqual([...example.ports].sort())
+ for (const port of example.ports) {
+ const available = docs.filter((doc) => docsEntryAvailable(doc, port))
+ expect(available).toHaveLength(1)
+ expect(docsRedirects(available, port)).toEqual([
+ { path: example.page, target: docsPath(available[0]) },
+ ])
+ const reverse = pagePortLinks({ pagePath: docsPath(available[0]), portSlug: port, version: 'latest', defaults: {}, docs })
+ expect(reverse.filter((link) => link.links.length).map((link) => link.port).sort())
+ .toEqual([...example.ports].sort())
+ }
+ })
})
diff --git a/site/test/example-sources.test.ts b/site/test/example-sources.test.ts
index a857b246..b6d87a89 100644
--- a/site/test/example-sources.test.ts
+++ b/site/test/example-sources.test.ts
@@ -27,8 +27,9 @@ const CONTENT = join(dirname(fileURLToPath(import.meta.url)), '../src/content/do
* Raise this as examples are converted; it exists to stop the number going
* the other way. It measures source inclusion, not execution coverage.
*/
-// The standalone Go workspace program is covered by complete-examples.test.ts.
-const SOURCED_FLOOR = 13
+// Complete workspace programs replace their shared source excerpts; native
+// receipts and rendering checks live in complete-examples.test.ts.
+const SOURCED_FLOOR = 10
interface Fence {
file: string
diff --git a/site/test/fixtures/product-examples.json b/site/test/fixtures/product-examples.json
index 05a6c7a5..e26683aa 100644
--- a/site/test/fixtures/product-examples.json
+++ b/site/test/fixtures/product-examples.json
@@ -271,5 +271,35 @@
"cargo run --quiet"
]
}
- ]
+ ],
+ "rootWorkspace": {
+ "page": "examples/workspace-from-file",
+ "files": [
+ {
+ "name": "workspace.conf",
+ "sha256": "0d166ecbe8a973649cb10498e69430bca86c647a398a830a8590d8d80f17b1a4"
+ },
+ {
+ "name": "workspace.sh",
+ "sha256": "3c3f550610e34f4b43378700915b189a4a5a37d0987352a94312674a004df411"
+ }
+ ],
+ "tmux": [
+ "3.2a",
+ "3.7c"
+ ],
+ "shellRecipe": [
+ "sh workspace.sh"
+ ],
+ "ports": [
+ "py",
+ "ts",
+ "go",
+ "rs",
+ "java",
+ "dotnet",
+ "cxx",
+ "swift"
+ ]
+ }
}
diff --git a/site/test/product-docs.test.ts b/site/test/product-docs.test.ts
index a1794d4a..2f0ec413 100644
--- a/site/test/product-docs.test.ts
+++ b/site/test/product-docs.test.ts
@@ -396,14 +396,24 @@ describe.skipIf(!SITE_BUILT)('assembled MCP and Workspace Manager docs', () => {
const defaults = manifest().defaultVersion
for (const page of pages()) await inspect(page.path, (document) => {
const links = [...document.querySelectorAll('[data-page-port-switcher] a[href]')]
- const available = PORTS.filter((port) => !port.parentLibrary && sectionsFor(port.slug, page.product).includes(page.section))
- expect(links.length, `${page.path} port counterparts`).toBe(available.length)
- for (const port of PORTS) {
+ const counterparts = new Map(PORTS.flatMap((port) => {
+ // The Python CLI example shares the root task alias with each
+ // native builder example, whose canonical page is under Internals.
+ const section = page.port === 'py' && page.product === 'workspace' && page.section === 'examples'
+ && ['ts', 'rs', 'go', 'java', 'dotnet', 'cxx', 'swift'].includes(port.slug)
+ ? 'internals/examples' : page.section
+ if (port.parentLibrary || !sectionsFor(port.slug, page.product).includes(section)) return []
const version = port.slug === page.port ? page.version : defaults[port.slug]
- const expected = urlFor(`${port.slug}/${version}/${page.product}/${page.section ? `${page.section}/` : ''}`).pathname
- expect(links.some((link) => new URL(link.getAttribute('href')!, urlFor(page.path)).pathname === expected),
- `${page.path} counterpart ${expected}`).toBe(available.includes(port))
- if (!available.includes(port)) {
+ return [[port.slug, urlFor(`${port.slug}/${version}/${page.product}/${section ? `${section}/` : ''}`).pathname]]
+ }))
+ expect(links.length, `${page.path} port counterparts`).toBe(counterparts.size)
+ for (const port of PORTS) {
+ const expected = counterparts.get(port.slug)
+ if (expected) {
+ expect(links.some((link) => new URL(link.getAttribute('href')!, urlFor(page.path)).pathname === expected),
+ `${page.path} counterpart ${expected}`).toBe(true)
+ expect(resolves(expected), `${page.path} counterpart exists at ${expected}`).toBe(true)
+ } else {
const unavailable = [...document.querySelectorAll('[data-page-port-switcher] [aria-disabled="true"]')]
expect(unavailable.some((entry) => entry.textContent.includes(port.name)), `${page.path} unavailable ${port.name}`).toBe(true)
}