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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ concurrency:
cancel-in-progress: false

jobs:
tui:
uses: ./.github/workflows/tui.yml

publish:
needs: tui
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -30,5 +34,10 @@ jobs:
- run: vp run @codeworksh/aikit#build
- run: vp check packages scripts .github
- run: vp run --filter './packages/*' --concurrency-limit 2 typecheck
# One directory per platform package; each package's build stages its binary from here.
- uses: actions/download-artifact@v4
with:
pattern: tui-*
path: packages/tui/dist/bin
- name: Publish all packages under canary
run: node scripts/canary.mjs
13 changes: 13 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,16 @@ jobs:
- run: vp check packages scripts .github
- run: vp run --filter './packages/*' --concurrency-limit 2 typecheck
- run: vp run --filter './packages/*' --concurrency-limit 2 build

tui:
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/tui
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/tui
- run: cargo check --locked
65 changes: 65 additions & 0 deletions .github/workflows/tui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: TUI Binaries

# Builds the Rust TUI for every published platform and uploads one artifact per
# platform package, named after it (`tui-<os>-<arch>`). Publishing workflows call
# this and stage the artifacts under packages/tui/dist/bin before `publish.mjs`.
on:
workflow_call:
workflow_dispatch:
# Catch a broken target in review rather than at publish time.
pull_request:
branches: [canary, dev]
paths:
- packages/tui/**
- .github/workflows/tui.yml

permissions:
contents: read

jobs:
build:
name: build (${{ matrix.package }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: true
matrix:
include:
- package: tui-darwin-arm64
runner: macos-14
target: aarch64-apple-darwin
- package: tui-darwin-x64
runner: macos-14
target: x86_64-apple-darwin
# Linux binaries link musl statically, so one build runs on any distro.
- package: tui-linux-arm64
runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- package: tui-linux-x64
runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
defaults:
run:
working-directory: packages/tui
steps:
- uses: actions/checkout@v6
- if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/tui
key: ${{ matrix.target }}
- run: cargo build --release --locked --target ${{ matrix.target }}
- name: Inspect binary
run: file target/${{ matrix.target }}/release/codework-tui
- if: runner.os == 'Linux'
name: Require a static binary
run: file target/${{ matrix.target }}/release/codework-tui | grep -E "static(-pie)? linked|statically linked"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.package }}
path: packages/tui/target/${{ matrix.target }}/release/codework-tui
if-no-files-found: error
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ models.gen.json
.astro

.alchemy

# Rust build output (packages/tui binary; the launcher loads the built artifact).
target
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,4 @@ Flow:
3. `git push && git push --tag`.
4. `pnpm release:aikit:dry` to preview the tarball, then `pnpm release:aikit` to publish. Use `pnpm release:aikit:dev` for a prerelease under the `dev` dist-tag.

A workspace dependency is published at its manifest version, which must already be on the registry. A dependency with only dev builds (e.g. plugin, when releasing harness) is pinned explicitly, as a version or a dist-tag resolved at publish time — there is no implicit fallback: `pnpm release:harness:dev --dep @codeworksh/plugin@dev`. Release in dependency order: aikit → plugin → harness → cli.
A workspace dependency is published at its manifest version, which must already be on the registry. A dependency with only dev builds (e.g. plugin, when releasing harness) is pinned explicitly, as a version or a dist-tag resolved at publish time — there is no implicit fallback: `pnpm release:harness:dev --dep @codeworksh/plugin@dev`. Release in dependency order: aikit → plugin → harness → tui platform packages (`tui-<os>-<arch>`) → tui → cli. The TUI's platform packages carry prebuilt binaries built by `.github/workflows/tui.yml` (macOS arm64/x64, Linux arm64/x64 static musl; Unix only) and ship in lockstep with `@codeworksh/tui`; `pnpm bump:tui` moves all five versions. Canary (push to `canary`) builds and publishes the whole chain.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"bump:cli": "pnpm --filter @codeworksh/cli run bump",
"bump:harness": "pnpm --filter @codeworksh/harness run bump",
"bump:plugin": "pnpm --filter @codeworksh/plugin run bump",
"bump:tui": "pnpm --filter @codeworksh/tui run bump",
"check": "vp check",
"check:packages": "vp run -r check",
"dev:desktop": "pnpm --filter @codeworksh/desktop dev",
Expand All @@ -36,6 +37,9 @@
"release:plugin": "pnpm --filter @codeworksh/plugin run release",
"release:plugin:dev": "pnpm --filter @codeworksh/plugin run release:dev",
"release:plugin:dry": "pnpm --filter @codeworksh/plugin run release:dry",
"release:tui": "pnpm --filter @codeworksh/tui run release",
"release:tui:dev": "pnpm --filter @codeworksh/tui run release:dev",
"release:tui:dry": "pnpm --filter @codeworksh/tui run release:dry",
"test": "vp test",
"test:packages": "vp run -r test",
"typecheck": "vp run -r --concurrency-limit 2 typecheck"
Expand Down
10 changes: 5 additions & 5 deletions packages/codework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ codework run --sandbox-driver daytona --sandbox-provider-id <remote-id> "Continu

#### Flags:

- `--server <url>` — Connect to a running RPC server, e.g. `ws://127.0.0.1:7433/rpc`.
- `--server <url>` — Connect to a running RPC server, e.g. `ws://127.0.0.1:7433/rpc/v1`.
- `-s, --session <id>` — Continue an existing session.
- `-C, --cwd <path>` — Working directory for a new session.
- `--sandbox-driver <driver>` — Sandbox driver (`local`, `daytona`, `vercel`, `memory`, `sqldb`). Default: `local`.
Expand All @@ -69,9 +69,9 @@ Start a persistent server that owns sessions and sandboxes:

```bash
codework serve --host 127.0.0.1 --port 7433
codework run --server ws://127.0.0.1:7433/rpc "Inspect the failing tests"
codework run --server ws://127.0.0.1:7433/rpc --session <id> "Now fix them"
codework run --server ws://127.0.0.1:7433/rpc --sandbox-id <sandbox-id> "Start another session in this sandbox"
codework run --server ws://127.0.0.1:7433/rpc/v1 "Inspect the failing tests"
codework run --server ws://127.0.0.1:7433/rpc/v1 --session <id> "Now fix them"
codework run --server ws://127.0.0.1:7433/rpc/v1 --sandbox-id <sandbox-id> "Start another session in this sandbox"
```

The default address is `127.0.0.1:7433`; `--port 0` picks an available port and prints the actual URL. Set `--home`, `--database`, and `--user-config-dir` on the server. Model credentials, plugins, and working directories are resolved by the server. A remote run does not boot a local harness.
Expand Down Expand Up @@ -105,7 +105,7 @@ codework auth refresh --openai-codex
codework auth logout --github-copilot

# Authenticate the home owned by a running RPC server
codework auth login --openai-codex --server ws://127.0.0.1:7433/rpc
codework auth login --openai-codex --server ws://127.0.0.1:7433/rpc/v1
```

`--home` keeps authentication alongside the rest of that Codework installation; `--auth-file` overrides the credential
Expand Down
1 change: 1 addition & 0 deletions packages/codework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@agentclientprotocol/sdk": "1.5.1",
"@codeworksh/aikit": "workspace:^",
"@codeworksh/harness": "workspace:*",
"@codeworksh/tui": "workspace:*",
"@effect/platform-node": "4.0.1",
"effect": "4.0.1",
"jsonc-parser": "3.3.1",
Expand Down
54 changes: 51 additions & 3 deletions packages/codework/src/cli/cmd/cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,26 @@ const AuthParams = {
Flag.withDescription("Use GitHub Copilot OAuth (device flow)"),
Flag.withDefault(false),
),
server: Flag.String("server").pipe(Flag.withDescription("RPC server URL (ws://host:port/rpc)"), Flag.optional),
server: Flag.String("server").pipe(Flag.withDescription("RPC server URL (ws://host:port/rpc/v1)"), Flag.optional),
authFile: Flag.String("auth-file").pipe(
Flag.withDescription("Credential file (defaults to <home>/aikit/auth.json)"),
Flag.optional,
),
};

/** Where the background server binds when `tui` or `service` has to start one. */
const ServerAddress = {
host: Flag.String("host").pipe(
Flag.withDescription("Host interface the background server binds"),
Flag.withDefault("127.0.0.1"),
),
port: Flag.Int("port").pipe(
Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 65535 }))),
Flag.withDescription("Port the background server listens on"),
Flag.withDefault(7433),
),
};

const JsonFlag = Flag.Boolean("json").pipe(
Flag.withDescription("Print machine-readable output"),
Flag.withDefault(false),
Expand Down Expand Up @@ -82,7 +95,7 @@ export const Cmd = Spec.make("codework", {
description: "Sign in with a device code, no local port",
},
{
command: "codework auth login --openai-codex --server ws://127.0.0.1:7433/rpc",
command: "codework auth login --openai-codex --server ws://127.0.0.1:7433/rpc/v1",
description: "Sign in the running server",
},
],
Expand All @@ -108,7 +121,7 @@ export const Cmd = Spec.make("codework", {
description: "Start or continue an agent session",
params: {
server: Flag.String("server").pipe(
Flag.withDescription("RPC server URL (ws://host:port/rpc)"),
Flag.withDescription("RPC server URL (ws://host:port/rpc/v1)"),
Flag.optional,
),
prompt: Argument.String("prompt").pipe(Argument.withDescription("Prompt for the agent")),
Expand Down Expand Up @@ -161,6 +174,26 @@ export const Cmd = Spec.make("codework", {
},
],
}),
Spec.make("tui", {
description: "Launch the CodeWork terminal UI against the background server",
params: {
server: Flag.String("server").pipe(
Flag.withDescription("RPC server URL to use instead of the background server (ws://host:port/rpc/v1)"),
Flag.optional,
),
...ServerAddress,
args: Argument.variadic(
Argument.String("args").pipe(Argument.withDescription("Arguments forwarded to the terminal UI")),
),
},
examples: [
{ command: "codework tui", description: "Start or reuse the local server and open the terminal UI" },
{
command: "codework tui --server ws://127.0.0.1:7433/rpc/v1",
description: "Attach the terminal UI to a server you started yourself",
},
],
}),
Spec.make("serve", {
description: "Start the CodeWork RPC server",
params: {
Expand All @@ -173,12 +206,27 @@ export const Cmd = Spec.make("codework", {
Flag.withDescription("Port to listen on (0 for an ephemeral port)"),
Flag.withDefault(7433),
),
service: Flag.Boolean("service").pipe(
Flag.withDescription(
"Run as the background server `codework tui` manages, logging to <home>/log/server.log",
),
Flag.withDefault(false),
),
},
examples: [
{ command: "codework serve", description: "Start the RPC server on 127.0.0.1:7433" },
{ command: "codework serve --port 0", description: "Choose an available local port" },
],
}),
Spec.make("service", {
description: "Manage the background server",
commands: [
Spec.make("start", { description: "Start the background server", params: ServerAddress }),
Spec.make("restart", { description: "Restart the background server", params: ServerAddress }),
Spec.make("status", { description: "Show background server status" }),
Spec.make("stop", { description: "Stop the background server" }),
],
}),
Spec.make("session", {
description: "Manage sessions",
commands: [
Expand Down
40 changes: 27 additions & 13 deletions packages/codework/src/cli/cmd/handlers/serve.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import { Effect, Layer } from "effect";
import { Global } from "@codeworksh/harness/effect";
import { Effect, FileSystem, Layer, Logger } from "effect";
import { Runtime } from "../../../framework/runtime.ts";
import { Registration } from "../../../server/registration.ts";
import { Server } from "../../../server/server.ts";
import { renderError } from "../../error.ts";
import { harnessOptions } from "../../harness.ts";
import { writeError } from "../../output.ts";
import { Cmd } from "../cmd.ts";

const failed = Effect.sync(() => {
process.exitCode = 1;
});

const report = (error: unknown) => writeError(renderError(error)).pipe(Effect.andThen(failed));

export default Runtime.handler(
Cmd.commands.serve,
Effect.fn("CLI.serve")(function* ({ host, port }) {
Effect.fn("CLI.serve")(function* ({ host, port, service }) {
const shared = yield* Cmd.spec;
yield* Layer.launch(Server.layer({ host, port, harness: { ...harnessOptions(shared), watchModels: true } })).pipe(
Effect.catch((error) =>
writeError(renderError(error)).pipe(
Effect.andThen(
Effect.sync(() => {
process.exitCode = 1;
}),
),
),
),
);
const harness = { ...harnessOptions(shared), watchModels: true };
const serve = Layer.launch(Server.layer({ host, port, harness }));
if (!service) return yield* serve.pipe(Effect.catch(report));

// A background server has no terminal: its log, and the reason it stopped, go to a
// file the CLI that started it reads back when startup fails.
const background = Effect.gen(function* () {
const paths = yield* Global.resolve(harness.home === undefined ? {} : { home: harness.home });
const fs = yield* FileSystem.FileSystem;
yield* fs.makeDirectory(paths.log, { recursive: true });
const logger = Logger.formatSimple.pipe(Logger.toFile(Registration.log(paths.log), { flag: "w" }));
yield* serve.pipe(
Effect.catch((error) => Effect.logError(renderError(error).trim()).pipe(Effect.andThen(failed))),
Effect.provide(Logger.layer([logger])),
);
});
yield* background.pipe(Effect.catch(report));
}),
);
17 changes: 17 additions & 0 deletions packages/codework/src/cli/cmd/handlers/service/restart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Effect } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { Discovery } from "../../../../server/discovery.ts";
import { reportFailure } from "../../../error.ts";
import { writeOut } from "../../../output.ts";
import { Service } from "../../../service.ts";
import { Cmd } from "../../cmd.ts";

export default Runtime.handler(
Cmd.commands.service.commands.restart,
Effect.fn("CLI.service.restart")(function* ({ host, port }) {
const options = yield* Service.options(yield* Cmd.spec, { host, port });
yield* Discovery.stop(options.home);
const server = yield* Discovery.ensure({ ...options, replace: true });
yield* writeOut(`${server.url}\n`);
}, Effect.catch(reportFailure)),
);
16 changes: 16 additions & 0 deletions packages/codework/src/cli/cmd/handlers/service/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Effect } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { Discovery } from "../../../../server/discovery.ts";
import { reportFailure } from "../../../error.ts";
import { writeOut } from "../../../output.ts";
import { Service } from "../../../service.ts";
import { Cmd } from "../../cmd.ts";

export default Runtime.handler(
Cmd.commands.service.commands.start,
Effect.fn("CLI.service.start")(function* ({ host, port }) {
const options = yield* Service.options(yield* Cmd.spec, { host, port });
const server = yield* Discovery.ensure({ ...options, replace: true });
yield* writeOut(`${server.url}\n`);
}, Effect.catch(reportFailure)),
);
16 changes: 16 additions & 0 deletions packages/codework/src/cli/cmd/handlers/service/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Effect } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { Discovery } from "../../../../server/discovery.ts";
import { reportFailure } from "../../../error.ts";
import { writeOut } from "../../../output.ts";
import { Service } from "../../../service.ts";
import { Cmd } from "../../cmd.ts";

export default Runtime.handler(
Cmd.commands.service.commands.status,
Effect.fn("CLI.service.status")(function* () {
const paths = yield* Service.home(yield* Cmd.spec);
const server = yield* Discovery.discover(paths.home);
yield* writeOut(`${server?.url ?? "stopped"}\n`);
}, Effect.catch(reportFailure)),
);
14 changes: 14 additions & 0 deletions packages/codework/src/cli/cmd/handlers/service/stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Effect } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { Discovery } from "../../../../server/discovery.ts";
import { reportFailure } from "../../../error.ts";
import { Service } from "../../../service.ts";
import { Cmd } from "../../cmd.ts";

export default Runtime.handler(
Cmd.commands.service.commands.stop,
Effect.fn("CLI.service.stop")(function* () {
const paths = yield* Service.home(yield* Cmd.spec);
yield* Discovery.stop(paths.home);
}, Effect.catch(reportFailure)),
);
Loading
Loading