Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1e9370e
refactor: remove unused compiler options from tsconfig files
jesse23 Mar 21, 2026
8432a30
docs: update CLI specification with ADR reference and add pull reques…
jesse23 Mar 21, 2026
7ccf4c2
feat: implement automated release process with semantic-release
jesse23 Mar 21, 2026
00dbb9f
feat: enhance release process with npm publishing and clean-publish i…
jesse23 Mar 21, 2026
260a071
feat: update release process to use npm publish and remove clean-publ…
jesse23 Mar 22, 2026
29954f5
fix: use template literal in clean-pkg-scripts
jesse23 Mar 22, 2026
2486ada
test: add integration tests for server endpoints
jesse23 Mar 22, 2026
a4efdf4
test: add integration tests for CLI start/stop
jesse23 Mar 22, 2026
6d51de9
ci: add actions/checkout@v4 step before Copilot review
jesse23 Mar 22, 2026
cfa0b27
ci: remove copilot-review workflow
jesse23 Mar 22, 2026
d3fa276
fix: add TypeScript ambient module declaration for @lydell/node-pty
jesse23 Mar 22, 2026
4afd418
refactor: move TypeScript declarations from src/env.d.ts to src/pty/n…
jesse23 Mar 22, 2026
677c85b
fix: resolve server entry and fetch error handling in CLI
jesse23 Mar 22, 2026
b5e6856
test: use dynamic port allocation via getFreePort()
jesse23 Mar 22, 2026
3728aab
fix: update release pipeline to use npm publish
jesse23 Mar 22, 2026
ce4e07e
docs: update ADR status to Accepted and fix wording
jesse23 Mar 22, 2026
bc97036
fix: restore package.json on exit via process handlers
jesse23 Mar 22, 2026
bbbe019
fix: implement package.json strip/restore on publish
jesse23 Mar 22, 2026
b942474
chore: rename BACKUP_PATH from package.json.publish-backup to package…
jesse23 Mar 22, 2026
8a8665f
fix: wrap restore block in try/catch to handle missing backup gracefully
jesse23 Mar 22, 2026
5a1812a
fix: rename npm scripts and guard backup strip against stale .bak files
jesse23 Mar 22, 2026
9b284d1
fix: use 127.0.0.1 instead of localhost in CLI and build
jesse23 Mar 22, 2026
1bcc558
test: replace Bun.sleep with waitForServerDown polling
jesse23 Mar 22, 2026
0141cf5
chore: add Node.js engine requirement and update release process
jesse23 Mar 22, 2026
3451bfe
fix: update server log message to use 127.0.0.1 instead of localhost
jesse23 Mar 22, 2026
966aed2
fix: add trailing newline to CLI banner string
jesse23 Mar 22, 2026
ad9043c
fix: check serverEntry exists before spawning, exit 1 with error mess…
jesse23 Mar 22, 2026
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
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Description

<!-- What does this PR do and why? -->

## ADRs

<!-- List any ADRs introduced or implemented by this PR. Remove if not applicable. -->
-
18 changes: 0 additions & 18 deletions .github/workflows/copilot-review.yml

This file was deleted.

12 changes: 10 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ on:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- run: bun install --frozen-lockfile
- run: bun run build
- name: Publish (dummy)
run: echo "TODO - publish step"
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bunx semantic-release
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dist/

# npm publish artifacts
*.tgz
package.json.bak

# Logs
*.log
Expand Down
19 changes: 19 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# source
src/
scripts/

# config
tsconfig*.json
biome.json
.releaserc.json
Makefile

# publish backup
package.json.bak

# docs
docs/

# misc
.github/
.agents/
10 changes: 10 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}
895 changes: 895 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions docs/adrs/002.cli.start-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# ADR 002: CLI — `wtty start` / `wtty stop`

**SPEC:** [cli](../specs/cli.md)
**Status:** Accepted
**Date:** 2026-03-21
Comment thread
jesse23 marked this conversation as resolved.

---

## Context

wtty currently has one entry point: `src/server.ts`, run directly via `bun run src/server.ts` or `tsx src/server.ts`. There is no CLI, no daemon mode, and no way to stop the server other than `Ctrl-C` in the terminal that started it.

The goal for this slice is `npx wtty start` / `npx wtty stop` — the minimal lifecycle commands. `start` forks the server as a background daemon and exits. `stop` terminates that daemon.

Two sub-questions drive this ADR:

1. **How does `start` fork a background process?** Node.js `child_process.spawn` with `detached: true` + `stdio: 'ignore'` + `unref()` — the parent exits, the child keeps running.
2. **How does `stop` terminate the server?** `POST /api/server/stop` — the server handles its own shutdown. No PID file, no signals, works identically on Mac, Linux, and Windows.

## Decision

Add a CLI entry point (`src/cli.ts`) and a `POST /api/server/stop` endpoint to `src/server.ts`. No new runtime dependencies — Commander.js is deferred until there are enough subcommands to justify it.

**`wtty start`**:
- Spawns the server with `detached: true`, `stdio: 'ignore'`, `unref()`.
- Immediately prints `wtty started` and exits. Does not wait for the server to be ready.

**`wtty stop`**:
- Sends `POST http://localhost:PORT/api/server/stop`.
- If the request succeeds: prints `wtty stopped`.
- If the request fails (connection refused): prints `wtty is not running`.

**`server.ts` change**: Add `POST /api/server/stop` — kills all PTY sessions, closes the WebSocket server, and calls `process.exit(0)`. The server owns its own shutdown on all platforms.

## Considered Options

### Option A: HTTP stop endpoint (chosen)

`wtty stop` sends `POST /api/server/stop`. Server cleans up and exits itself.

- **Pros**: Fully cross-platform — no signals, no PID file. Server owns its cleanup. Connection refused = not running, no separate state to track. Consistent with the existing HTTP surface of the server.
- **Cons**: Requires the server to be responsive. If the server is hung, HTTP stop won't work — acceptable as a degraded edge case; a hard `kill` fallback can be added later if needed.

### Option B: PID file + SIGTERM

Write the server PID to `~/.wtty/server.pid`. `stop` reads it and sends `SIGTERM`.

- **Pros**: Works without an HTTP round-trip. PID file is human-readable.
- **Cons**: SIGTERM on Windows calls `TerminateProcess()` — hard kill, server cleanup handler never runs. Stale PID files if the server crashes. Requires PID file management in both the CLI and server. More state to track.

### Option C: Commander.js from the start

Add Commander.js now and model `start`/`stop` as commands.

- **Pros**: Consistent with the CLI spec's stated direction. Auto-generates `--help`.
- **Cons**: Adds a dependency for two `if` branches. Commander.js is justified when there are multiple subcommands with options — for this slice it's overhead. Deferred to the session management slice.

### Option D: Shell script wrapper

A `wtty.sh` that does `node dist/server.js &`.

- **Pros**: Trivial.
- **Cons**: Platform-specific (no Windows). Doesn't compose with `npx`.

## Consequences

**Good**: `npx wtty start` works from any directory. Daemon runs in background, terminal is free. `npx wtty stop` works identically on Mac, Linux, and Windows — no signals, no PID files, no platform-specific code. Server owns its cleanup. No new runtime dependencies.

**Bad**: If the server is hung, `wtty stop` fails silently — acceptable for this slice; hard-kill fallback deferred. Port is hardcoded at 2346 — will become configurable when the config file slice lands.

## Implementation Notes

### `package.json` changes

```json
{
"bin": {
"wtty": "./dist/cli.js"
}
}
```

Add `src/cli.ts` as a second build entrypoint alongside `src/server.ts`.

### File layout

```
src/
cli.ts ← new: wtty start / wtty stop
server.ts ← add: POST /api/server/stop endpoint
```

### `src/cli.ts` sketch

```ts
const command = process.argv[2];
const BASE_URL = `http://localhost:${PORT}`;

if (command === 'start') {
const child = spawn(process.execPath, [serverEntry], {
detached: true,
stdio: 'ignore',
});
child.unref();
console.log('wtty started');
} else if (command === 'stop') {
try {
await fetch(`${BASE_URL}/api/server/stop`, { method: 'POST' });
console.log('wtty stopped');
} catch {
console.log('wtty is not running');
}
} else {
console.error('Usage: wtty start | wtty stop');
process.exit(1);
}
```

### `POST /api/server/stop` in `server.ts`

```ts
if (req.method === 'POST' && pathname === '/api/server/stop') {
res.writeHead(200);
res.end('stopping');
for (const [ws, session] of sessions.entries()) {
session.pty.kill();
ws.close();
}
wss.close();
httpServer.close(() => process.exit(0));
return;
}
```

### Daemon spawn

```ts
const child = spawn(process.execPath, [serverEntry], {
detached: true,
stdio: 'ignore',
});
child.unref();
```

`process.execPath` is the Node.js or Bun binary that launched the CLI — ensures the server runs under the same runtime.

## Related Decisions

- [ADR 001 — Bootstrap](001.wtty.bootstrap.md): `src/server.ts` is the server entry point extended by this ADR
- [ADR 003 — Release Process](003.release-process.semantic-release.md): CLI entry point is part of the binary artifact built and released
141 changes: 141 additions & 0 deletions docs/adrs/003.release-process.semantic-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# ADR 003: Release Process — semantic-release on GitHub

**SPEC:** [release-process](../specs/release-process.md)
**Status:** Accepted
**Date:** 2026-03-21
Comment thread
jesse23 marked this conversation as resolved.

---

## Context

wtty has a stub release workflow (`.github/workflows/release.yml`) that runs `bun run build` then `echo "TODO - publish step"`. No versioning, no published artifacts.

wtty targets developers who already have Node.js or Bun installed. Distribution via `npx wtty` is the natural fit — no binary downloads needed. A release means publishing to npm and creating a GitHub Release with release notes. The version should be derived automatically from what changed, not set manually.

The cfx repo solved the same versioning problem with semantic-release (ADR 004): conventional commit prefixes in PR/MR titles drive version bumps post-merge, git tags are the sole version record, no release commits. That pattern maps directly to wtty, with `@semantic-release/npm` added since wtty publishes to the public npm registry.

## Decision

Use **semantic-release** with `@semantic-release/npm` and `@semantic-release/github`, running post-merge on `main`. Developers write a conventional commit prefix in the PR title. GitHub squash merge uses the PR title as the commit message on `main`. semantic-release reads that message, determines the version bump, publishes to npm, creates a GitHub Release with release notes, and pushes a git tag. No manual versioning, no release commits, no extra files per PR.

## Considered Options

### Option A: semantic-release post-merge on main, tag-only (chosen)

- **Pros**: Zero developer overhead beyond PR title. npm publish + GitHub Release + release notes fully automated. Battle-tested. No version conflicts between concurrent PRs. No release commits — clean one-commit-per-PR history on `main`. `GITHUB_TOKEN` is provided automatically. Only `NPM_TOKEN` needs manual setup.
- **Cons**: `package.json` version doesn't reflect the released version (use `0.0.0-development` placeholder). No in-repo `CHANGELOG.md` (release notes live on GitHub Releases). Requires conventional commit discipline in PR titles (mitigate: squash merge uses PR title — one line to get right).

### Option B: Manual GitHub Release

- **Pros**: Full control over release notes. No tooling.
- **Cons**: Manual — error-prone, inconsistent, easy to forget.

### Option C: release-it

- **Pros**: Simpler config surface, base version frozen on prerelease.
- **Cons**: wtty has no prerelease channel need. semantic-release's automatic version derivation from commit type is the better fit — release-it requires a manual bump type or extra config to replicate this.

### Option D: Changesets

- **Pros**: Human-written changelogs, 1 dependency.
- **Cons**: Extra file per PR, developer must run `npx changeset`, duplicates PR title. Same reasons rejected in cfx ADR 004.

## Consequences

**Good**: Zero developer overhead — write a conventional PR title, everything else is automated. Publishes to npm. GitHub Release auto-created with release notes. Clean `main` history. Follows semantic-release's own recommended approach (tag-only, no commit-back).

**Bad**: `package.json` version stays at `0.0.0-development` in repo. No `CHANGELOG.md` in repo. Requires conventional commit discipline in PR titles. `NPM_TOKEN` must be set as a GitHub Actions secret.

## Implementation Notes

### Plugin Chain

```json
{
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}
```

`@semantic-release/npm` publishes to npm atomically within semantic-release's publish phase — before `@semantic-release/github` creates the tag and GitHub Release. This ensures a failed npm publish prevents the tag from being created (no partial releases). `prepack`/`postpack` scripts in `package.json` temporarily strip `scripts` and `devDependencies` before the package is packed, then restore them after. `@semantic-release/git` and `@semantic-release/changelog` intentionally omitted (tag-only).

`prepack`/`postpack` in `package.json`:

```json
"prepack": "bun scripts/clean-pkg-scripts.ts strip",
"postpack": "bun scripts/clean-pkg-scripts.ts restore"
```

What consumers receive:

```json
{
"name": "wtty",
"bin": { "wtty": "./dist/cli.js" },
"files": ["dist"],
"type": "module",
"dependencies": { ... }
}
```

### Release Workflow

```yaml
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- run: bun install --frozen-lockfile
- run: bun run build
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bunx semantic-release
```

- `fetch-depth: 0` — semantic-release needs full git history to find previous tags
- `persist-credentials: false` — semantic-release manages its own git auth via `GITHUB_TOKEN`
- `permissions: contents: write` — `GITHUB_TOKEN` needs to push tags and create releases
- `@semantic-release/npm` publishes first, then `@semantic-release/github` creates the tag — no partial release risk
- `NPM_TOKEN` — must be set as a GitHub Actions secret. Generate from npmjs.com → Access Tokens → Automation token.

### Why Tag-Only (No Release Commits)

Omitting `@semantic-release/git` and `@semantic-release/changelog`:

- No branch push permissions needed — `GITHUB_TOKEN` only needs to push tags
- Clean history — one commit per PR, no interleaved `chore(release):` commits
- No `[skip ci]` dance — no recursive pipeline triggering
- Fewer dependencies — 1 devDependency instead of 3+

### `package.json` Version Placeholder

```json
{ "version": "0.0.0-development" }
```

Git tags are the source of truth for the released version.

## Related Decisions

- [ADR 001 — Bootstrap](001.wtty.bootstrap.md): Established the build pipeline that the release workflow extends
- [ADR 002 — CLI start/stop](002.cli.start-stop.md): CLI entry point distributed via `npx wtty`
2 changes: 1 addition & 1 deletion docs/specs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ The CLI communicates with the server exclusively over HTTP to localhost — no U

| Feature | Description | ADR | Done? |
|---------|-------------|-----|-------|
| Server lifecycle | `wtty start`, `wtty stop`, `wtty restart`, `wtty status` — daemon control via PID file + HTTP | | ⬜ |
| Server lifecycle | `wtty start`, `wtty stop`, `wtty restart`, `wtty status` — daemon control via PID file + HTTP | [ADR 002](../adrs/002.cli.start-stop.md) | ⬜ |
| Session management | `wtty session create/list/kill` — thin wrappers over the session REST API | — | ⬜ |
Loading
Loading