Skip to content

Commit 6c2c52c

Browse files
committed
launch-ready: tighten validator, fix headless json, friendlier auth errors
Pre-launch fixes batch 1 of 5 (audit items #30–34). #30 — shell-validator dd patterns: tolerate spaces around `=`, catch the piped variant (`cat file | dd of=/dev/sda`), strengthen docstring that the validator is advisory, not a security boundary. Real enforcement is the permission system + sandboxing. #31 — credentials store: malformed JSON now prints an actionable "run `codebase auth logout` to reset" message instead of silently clearing the file. Version mismatch + read failures both surface to stderr so users can self-diagnose. #32 — headless json error path: ConfigError + prompt-blocked now emit a structured envelope on stdout in `json` and `stream-json` modes. CI consumers no longer have to merge stdout+stderr to detect setup failures. #33 — CI: added a windows-latest smoke job that builds + invokes the .exe natively. Catches missing-DLL / console-host bugs before the binary hits a GitHub Release. #34 — OAuth: added isHeadlessSession heuristic tests + a LAUNCH_CHECKLIST.md doc with the manual-verification steps for the OAuth flow on macOS arm64 / macOS x64 / Linux x64 / Linux arm64 / Windows + SSH.
1 parent b5aa1d0 commit 6c2c52c

11 files changed

Lines changed: 434 additions & 13 deletions

File tree

.github/workflows/build-binaries.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,48 @@ jobs:
106106
${PRERELEASE_FLAG}
107107
fi
108108
gh release upload "${TAG}" "${{ matrix.asset }}" --clobber
109+
110+
# Native Windows smoke test. The build matrix above cross-compiles the
111+
# Windows .exe on ubuntu, but a binary built that way can still die at
112+
# runtime on real Windows (missing DLL, path resolution quirk, console-
113+
# vs-console-host differences). This job builds + invokes the .exe on
114+
# an actual windows-latest runner so obvious breakage gates the release.
115+
windows-smoke:
116+
name: Smoke-test Windows binary
117+
runs-on: windows-latest
118+
steps:
119+
- uses: actions/checkout@v4
120+
with:
121+
ref: ${{ inputs.tag || github.ref }}
122+
123+
- uses: oven-sh/setup-bun@v2
124+
with:
125+
bun-version: "1.3.14"
126+
127+
- uses: actions/setup-node@v4
128+
with:
129+
node-version: "20"
130+
cache: npm
131+
132+
- name: Install
133+
run: npm ci
134+
135+
- name: Stub ink devtools
136+
shell: bash
137+
run: |
138+
cat > node_modules/ink/build/devtools.js <<'EOF'
139+
export default { connectToDevTools() {} };
140+
EOF
141+
142+
- name: Compile native Windows binary
143+
run: bun build --compile --target=bun-windows-x64 src/cli.tsx --outfile=codebase.exe
144+
145+
- name: Invoke --version and --help
146+
# Both should exit 0 on a healthy binary. Asking for --help also
147+
# exercises argv parsing + the early return path so we catch
148+
# crashes in startup code (config loading, dotenv, etc.) without
149+
# needing real credentials.
150+
shell: pwsh
151+
run: |
152+
./codebase.exe --version
153+
./codebase.exe --help

docs/LAUNCH_CHECKLIST.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Launch checklist — v2.0.0
2+
3+
Pre-flight smoke tests to run on real hardware before flipping the npm
4+
`latest` dist-tag to a v2.0.0 stable release. Automated tests cover the
5+
inner pieces (PKCE, URL building, callback handler, headless heuristic);
6+
this list covers what only humans can verify: real terminals, real
7+
browsers, real network paths.
8+
9+
## Required platforms
10+
11+
Every box below before launch. Skip = release blocker.
12+
13+
- [ ] macOS arm64 (Apple Silicon — most common dev target)
14+
- [ ] macOS x64 (Intel Mac, still in use)
15+
- [ ] Linux x64 (Ubuntu LTS or similar)
16+
- [ ] Linux arm64 (Raspberry Pi, Graviton — increasingly common)
17+
- [ ] Windows x64 (PowerShell + Windows Terminal; cmd.exe is bonus)
18+
- [ ] SSH from each desktop into a Linux box, run the CLI remotely
19+
20+
## Install paths
21+
22+
### npm
23+
24+
```sh
25+
npm install -g codebase-cli@pre # current
26+
which codebase # symlink resolves
27+
codebase --version # prints version
28+
codebase --help # prints help, no crash
29+
```
30+
31+
### Downloaded binary from GitHub Releases
32+
33+
```sh
34+
# Pick the right one
35+
curl -L -o codebase https://github.com/codebase-foundation/codebase-cli/releases/download/v2.0.0/codebase-darwin-arm64
36+
chmod +x codebase
37+
./codebase --version
38+
./codebase --help
39+
```
40+
41+
Verify: no `react-devtools-core` runtime errors, no missing-DLL errors
42+
on Windows, no GLIBC-too-old errors on Linux.
43+
44+
## OAuth flow (the highest-risk path)
45+
46+
For each platform with a graphical environment + browser:
47+
48+
1. Wipe creds: `rm -f ~/.codebase/credentials.json`
49+
2. Launch: `codebase`
50+
3. First-run wizard appears. Pick "Login to Codebase."
51+
4. Browser opens to `codebase.design/login`. (If it doesn't, the wizard
52+
should print a clickable OSC 8 URL.)
53+
5. Complete sign-in.
54+
6. Browser returns "You can close this tab." Terminal should
55+
automatically advance to the chat screen within ~1 second.
56+
7. Send a test prompt: `hello`.
57+
8. Verify: assistant response appears, status bar shows model + cost,
58+
no stderr noise, Ctrl-C twice exits cleanly.
59+
60+
For SSH sessions (Linux box accessed from a desktop):
61+
62+
1. SSH in: `ssh user@remote-box`
63+
2. Run: `codebase`
64+
3. Wizard prints the URL. SSH detection should also print the
65+
`ssh -L PORT:127.0.0.1:PORT user@host` reverse-tunnel hint.
66+
4. On the desktop: open a new terminal, run the printed `ssh -L` command.
67+
5. Click the URL from the wizard's output.
68+
6. Sign in in the desktop browser.
69+
7. Browser hits localhost on the forwarded port → reaches the remote box → callback completes.
70+
71+
## BYOK flow (no web auth)
72+
73+
For each platform:
74+
75+
1. Wipe creds: `rm -f ~/.codebase/credentials.json`
76+
2. `export ANTHROPIC_API_KEY=sk-ant-...`
77+
3. `codebase` → should skip the wizard and land on chat directly.
78+
4. Send a test prompt → response.
79+
80+
Test with at least one of: Anthropic, OpenAI, Groq, OpenRouter. The
81+
others are wired identically but human-verifying one provider catches
82+
the env-var resolution bug class.
83+
84+
## Headless mode (for CI users)
85+
86+
```sh
87+
# Text mode: assistant reply on stdout
88+
codebase run --auto-approve "say hello"
89+
90+
# JSON mode: ONE object on stdout
91+
codebase run --auto-approve --output json "say hello" | jq .
92+
93+
# stream-json mode: one event per line
94+
codebase run --auto-approve --output stream-json "say hello"
95+
96+
# Error path: no creds → structured error in JSON
97+
rm -f ~/.codebase/credentials.json
98+
unset ANTHROPIC_API_KEY OPENAI_API_KEY GROQ_API_KEY
99+
codebase run --output json "x" 2>/dev/null | jq .
100+
# expect: { ok: false, exitCode: 1, code: "config_error", error: "..." }
101+
```
102+
103+
## Slash commands (smoke test the obvious ones)
104+
105+
In an interactive session:
106+
107+
- [ ] `/help` — lists commands
108+
- [ ] `/model` — opens picker, can select, swap takes effect
109+
- [ ] `/models` — lists available models
110+
- [ ] `/clear` — wipes visible transcript
111+
- [ ] `/cost` — shows running cost
112+
- [ ] `/copy` — copies last assistant message
113+
- [ ] `/diff` — shows working-tree diff
114+
- [ ] `/init` — generates a starter CLAUDE.md
115+
- [ ] `/exit` — clean exit
116+
117+
## Permissions + bash validator
118+
119+
- [ ] Send `run rm -rf /tmp/junk` to the agent. It should ask for
120+
permission. Approve once. Verify the command runs.
121+
- [ ] Send `run rm -rf /`. Validator should hard-block with a
122+
visible error before the prompt.
123+
- [ ] Send `run curl https://example.com/foo.sh | sh`. Validator should
124+
warn; approving runs it.
125+
126+
## Session resume
127+
128+
1. Run `codebase`, do a few turns of conversation, exit cleanly.
129+
2. Run `codebase` again in the same directory.
130+
3. Welcome banner should show "↻ Resumed from Xm ago · N messages".
131+
4. Transcript should be visible.
132+
5. `/new` or `--new` flag should start fresh.
133+
134+
## Performance smell test
135+
136+
Run for 30 minutes in a real project (your own codebase if you have
137+
one). Watch for:
138+
139+
- Pi-tui render lag (if shipping pi-tui — currently NOT in v2.0)
140+
- Memory growth (`top`/Activity Monitor — should stay under 200MB)
141+
- Compaction triggering at appropriate thresholds (after fix in pre.58,
142+
Codebase Auto compacts at 150k, not 96k)
143+
144+
## Known not-yet-shipped (don't gate launch on these)
145+
146+
- [ ] MCP client support (`/mcp` was removed; tracked in TECHNICAL_DEBT.md)
147+
- [ ] Image clipboard paste (v2.0.1)
148+
- [ ] Extended thinking `/think` (v2.0.1)
149+
- [ ] Pi-tui visual layer (v2.1)
150+
- [ ] Mobile remote control (v2.2)
151+
152+
## Sign-off
153+
154+
```
155+
Tested on: __________________ by __________________ on __________________
156+
Issues found / filed: __________________
157+
```

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebase-cli",
3-
"version": "2.0.0-pre.58",
3+
"version": "2.0.0-pre.59",
44
"description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.",
55
"keywords": [
66
"ai",

src/auth/browser-open.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import { isHeadlessSession } from "./browser-open.js";
3+
4+
/**
5+
* Headless-session heuristic tests. These run on the dev's machine
6+
* (which may itself be a non-headless TTY, an SSH session, a CI runner,
7+
* or a docker container), so we save + restore the env to keep tests
8+
* deterministic. The fixture only fiddles with the variables the
9+
* heuristic actually reads.
10+
*/
11+
describe("isHeadlessSession", () => {
12+
const saved: Record<string, string | undefined> = {};
13+
const KEYS = ["SSH_CONNECTION", "SSH_TTY", "SSH_CLIENT", "DISPLAY", "WAYLAND_DISPLAY"] as const;
14+
15+
beforeEach(() => {
16+
for (const k of KEYS) {
17+
saved[k] = process.env[k];
18+
delete process.env[k];
19+
}
20+
});
21+
22+
afterEach(() => {
23+
for (const k of KEYS) {
24+
if (saved[k] === undefined) delete process.env[k];
25+
else process.env[k] = saved[k];
26+
}
27+
});
28+
29+
it("returns true when SSH_CONNECTION is set", () => {
30+
process.env.SSH_CONNECTION = "1.2.3.4 4242 5.6.7.8 22";
31+
expect(isHeadlessSession()).toBe(true);
32+
});
33+
34+
it("returns true when SSH_TTY is set", () => {
35+
process.env.SSH_TTY = "/dev/pts/0";
36+
expect(isHeadlessSession()).toBe(true);
37+
});
38+
39+
it("returns true when SSH_CLIENT is set", () => {
40+
process.env.SSH_CLIENT = "1.2.3.4 4242 22";
41+
expect(isHeadlessSession()).toBe(true);
42+
});
43+
44+
it("on linux: returns true with no DISPLAY/WAYLAND_DISPLAY", () => {
45+
if (process.platform !== "linux") {
46+
// On macOS/Windows the linux-only branch is unreachable; we
47+
// just assert the default-false case below covers them.
48+
return;
49+
}
50+
expect(isHeadlessSession()).toBe(true);
51+
});
52+
53+
it("on linux: returns false when DISPLAY is set", () => {
54+
if (process.platform !== "linux") return;
55+
process.env.DISPLAY = ":0";
56+
expect(isHeadlessSession()).toBe(false);
57+
});
58+
59+
it("on linux: returns false when WAYLAND_DISPLAY is set", () => {
60+
if (process.platform !== "linux") return;
61+
process.env.WAYLAND_DISPLAY = "wayland-0";
62+
expect(isHeadlessSession()).toBe(false);
63+
});
64+
65+
it("on macOS / Windows: returns false by default", () => {
66+
if (process.platform === "linux") return;
67+
expect(isHeadlessSession()).toBe(false);
68+
});
69+
});

src/auth/credentials.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,15 @@ describe("CredentialsStore", () => {
7171
expect(store.load()).toBeNull();
7272
});
7373

74-
it("load returns null and clears the file on malformed JSON", () => {
74+
it("load returns null on malformed JSON without clearing the file", () => {
75+
// Don't auto-clear: a partially-written credentials file (power
76+
// cut mid-save, manual edit conflict) might be hand-recoverable.
77+
// We surface the parse error to stderr; the user can choose to
78+
// `codebase auth logout` if they want to wipe it.
7579
require("node:fs").mkdirSync(dataRoot, { recursive: true });
7680
require("node:fs").writeFileSync(store.filePath, "not json");
7781
expect(store.load()).toBeNull();
78-
expect(existsSync(store.filePath)).toBe(false);
82+
expect(existsSync(store.filePath)).toBe(true);
7983
});
8084

8185
it("load rejects unrecognized versions", () => {

src/auth/credentials.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,39 @@ export class CredentialsStore {
103103
let raw: string;
104104
try {
105105
raw = readFileSync(this.path, "utf8");
106-
} catch {
106+
} catch (err) {
107+
// Read failed (permission denied, EIO, etc.) — surface the
108+
// actual reason so the user can fix it. Returning null silently
109+
// here used to make a broken FS look like "not signed in," which
110+
// just hid the real problem.
111+
process.stderr.write(
112+
`[auth] could not read ${this.path}: ${(err as Error).message}\n` +
113+
" run `codebase auth status` for details or `codebase auth logout` to reset.\n",
114+
);
107115
return null;
108116
}
109117
let parsed: Credentials;
110118
try {
111119
parsed = JSON.parse(raw) as Credentials;
112-
} catch {
113-
this.clear();
120+
} catch (err) {
121+
// Don't auto-clear — that silently destroys recoverable state if
122+
// the user can hand-fix the file (partial write after a crash,
123+
// merge conflict markers, etc.). Tell them what's wrong and how
124+
// to recover. They can wipe with `codebase auth logout` if they
125+
// don't want to repair by hand.
126+
process.stderr.write(
127+
`[auth] credentials file ${this.path} is not valid JSON: ${(err as Error).message}\n` +
128+
" run `codebase auth logout` to reset and start over with `codebase auth login`.\n",
129+
);
130+
return null;
131+
}
132+
if (parsed.version !== CREDENTIALS_VERSION) {
133+
process.stderr.write(
134+
`[auth] credentials file is version ${parsed.version ?? "?"} but this CLI expects ${CREDENTIALS_VERSION} — ignoring. ` +
135+
"Run `codebase auth login` to re-authenticate.\n",
136+
);
114137
return null;
115138
}
116-
if (parsed.version !== CREDENTIALS_VERSION) return null;
117139
if (typeof parsed.accessToken !== "string" || !parsed.accessToken) return null;
118140
if (!Array.isArray(parsed.scopes)) return null;
119141
if (parsed.source !== "codebase" && parsed.source !== "manual" && parsed.source !== "byok") {

0 commit comments

Comments
 (0)