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
40 changes: 33 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ machines.
- [Quick Setup](#quick-setup)
- [Symlink Philosophy](#symlink-philosophy)
- [Features](#features)
- [WSL Clipboard Integration](#wsl-clipboard-integration)
- [Sync and health checks](#sync-and-health-checks)
- [AI agent skills](#ai-agent-skills)
- [Neovim Configuration](#neovim-configuration)
- [Terminal Environment](#terminal-environment)
- [Terminal Environment: tmux, zsh, bun](#terminal-environment-tmux-zsh-bun)
- [Codex config](#codex-config)
- [WSL Clipboard Integration](#wsl-clipboard-integration)
- [Requirements](#requirements)
- [Testing](#testing)

Expand Down Expand Up @@ -75,23 +78,46 @@ The `symlinks.sh` script handles creating all necessary symbolic links to connec

## Features

### WSL Clipboard Integration
- Custom `pbcopy` and `pbpaste` commands that work with Windows clipboard
- Neovim configured to use system clipboard across WSL/Windows boundary
- Automatically removes Windows line endings when pasting
### Sync and health checks

`just sync` runs the established shell bootstrap, applies portable Codex
defaults, and synchronizes managed AI skills. `just health` does not write: it
checks required commands and reports Codex or skills-sync drift with a nonzero
exit status.

### AI agent skills

`just skills-sync --run` treats `$HOME/.cursor/skills` as the canonical runtime
directory and also copies all skills to `$HOME/.agents/skills` for Codex CLI.
The Codex destination is created and marked as managed on first use; an
existing unrelated non-empty directory is rejected rather than overwritten.

See the [Codex skills documentation](https://developers.openai.com/codex/concepts/customization#skills).

### Neovim Configuration
- Light/dark theme toggle (Catppuccin/OneDark)
- Treesitter with support for modern languages including Astro
- LSP with auto-installation of language servers
- Harpoon, Telescope, and other navigation enhancements

### Terminal Environment
### Terminal Environment: tmux, zsh, bun
- Tmux with Dracula theme and plugin manager
- Zsh configured for Node.js, Rust, Go, and Python development
- Bun JavaScript/TypeScript runtime integration
- `tree-sitter-cli` support for Neovim parser installation on the `main` branch

### Codex config

[`codex/config.ts`](codex/config.ts) maintains portable defaults in
`$HOME/.codex/config.toml` while preserving local project, MCP, and onboarding
state. Run `bun run codex/config.ts` for its usage and options.

### WSL Clipboard Integration
- Custom `pbcopy` and `pbpaste` commands that work with Windows clipboard
- Neovim configured to use system clipboard across WSL/Windows boundary
- Automatically removes Windows line endings when pasting


## Requirements

- Ubuntu 24.04 (or compatible) on WSL2
Expand Down
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions codex/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Codex Config

`config.ts` maintains portable defaults for the Codex CLI. It writes the
runtime file at `$HOME/.codex/config.toml` while preserving local state.

## Managed settings

The TypeScript `dotfileConfig` object owns general preferences: personality,
model, reasoning effort, trust level, approval policy, sandbox mode, and
`tui.vim_mode_default`. Edit that object to change the backed-up defaults.

Project-specific trust, MCP servers, TUI onboarding state, authentication,
history, caches, databases, and logs are intentionally not backed up here.

See the [Codex config reference](https://developers.openai.com/codex/config-reference).

## Usage

```bash
# Show help
bun run codex/config.ts

# Preview or check drift
bun run codex/config.ts --dry-run
bun run codex/config.ts --check

# Apply portable defaults
bun run codex/config.ts --run
```
165 changes: 165 additions & 0 deletions codex/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { describe, expect, test } from "bun:test"
import { parse, type TomlTable } from "smol-toml"

import { applyConfig, dotfileConfig, mergeRuntimeConfig } from "./config.ts"

const scriptPath = join(import.meta.dir, "config.ts")

const runConfig = async (
homeDir: string,
args: string[] = [],
): Promise<{ exitCode: number; stdout: string; stderr: string }> => {
const proc = Bun.spawn(["bun", scriptPath, ...args], {
cwd: join(import.meta.dir, ".."),
env: { ...process.env, HOME: homeDir },
stderr: "pipe",
stdout: "pipe",
})
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
])

return { exitCode, stdout, stderr }
}

describe("codex config", () => {
test("overwrites portable defaults and preserves local tables", () => {
const merged = mergeRuntimeConfig({
personality: "other",
model: "other-model",
tui: {
vim_mode_default: false,
model_availability_nux: { "gpt-5.6-sol": 4 },
},
projects: { "/tmp/project": { trust_level: "trusted" } },
mcp_servers: { private: { command: "private-command" } },
})

expect(merged).toMatchObject(dotfileConfig)
expect(merged.tui).toEqual({
vim_mode_default: true,
model_availability_nux: { "gpt-5.6-sol": 4 },
})
expect(merged.projects).toEqual({
"/tmp/project": { trust_level: "trusted" },
})
expect(merged.mcp_servers).toEqual({
private: { command: "private-command" },
})
})

test("creates the runtime config and is idempotent", async () => {
const root = await mkdtemp(join(tmpdir(), "codex-config-test-"))

try {
const runtimePath = join(root, ".codex/config.toml")
expect(await applyConfig({ runtimePath, quiet: true })).toBe(true)
const firstText = await Bun.file(runtimePath).text()
expect(parse(firstText) as TomlTable).toEqual(dotfileConfig)
expect(await applyConfig({ runtimePath, quiet: true })).toBe(false)
expect(await Bun.file(runtimePath).text()).toBe(firstText)
} finally {
await rm(root, { recursive: true, force: true })
}
})

test("preserves local config when writing", async () => {
const root = await mkdtemp(join(tmpdir(), "codex-config-merge-test-"))

try {
const runtimePath = join(root, ".codex/config.toml")
await mkdir(join(root, ".codex"), { recursive: true })
await writeFile(
runtimePath,
'[projects."/tmp/project"]\ntrust_level = "trusted"\n\n' +
"[mcp_servers.private]\ncommand = \"private-command\"\n\n" +
"[tui]\nvim_mode_default = false\n\n" +
"[tui.model_availability_nux]\n\"gpt-5.6-sol\" = 4\n",
)

await applyConfig({ runtimePath, quiet: true })
const config = parse(await Bun.file(runtimePath).text()) as TomlTable
expect(config.projects).toEqual({
"/tmp/project": { trust_level: "trusted" },
})
expect(config.mcp_servers).toEqual({
private: { command: "private-command" },
})
expect(config.tui).toEqual({
vim_mode_default: true,
model_availability_nux: { "gpt-5.6-sol": 4 },
})
} finally {
await rm(root, { recursive: true, force: true })
}
})

test("does not rewrite equivalent TOML comments or formatting", async () => {
const root = await mkdtemp(join(tmpdir(), "codex-config-comment-test-"))

try {
const runtimePath = join(root, ".codex/config.toml")
const text = `# Portable preferences\n${[
'personality = "pragmatic"',
'approvals_reviewer = "user"',
'model = "gpt-5.6-terra"',
'model_reasoning_effort = "medium"',
'trust_level = "trusted"',
'approval_policy = "never"',
'sandbox_mode = "danger-full-access"',
"",
"[tui]",
"vim_mode_default = true",
].join("\n")}\n`
await mkdir(join(root, ".codex"), { recursive: true })
await writeFile(runtimePath, text)

expect(await applyConfig({ runtimePath, quiet: true })).toBe(false)
expect(await Bun.file(runtimePath).text()).toBe(text)
} finally {
await rm(root, { recursive: true, force: true })
}
})

test("rejects malformed TOML without changing it", async () => {
const root = await mkdtemp(join(tmpdir(), "codex-config-invalid-test-"))

try {
const runtimePath = join(root, ".codex/config.toml")
const invalidToml = "model = [\n"
await mkdir(join(root, ".codex"), { recursive: true })
await writeFile(runtimePath, invalidToml)
await expect(applyConfig({ runtimePath, quiet: true })).rejects.toThrow()
expect(await Bun.file(runtimePath).text()).toBe(invalidToml)
} finally {
await rm(root, { recursive: true, force: true })
}
})

test("prints help by default and checks drift without writing", async () => {
const root = await mkdtemp(join(tmpdir(), "codex-config-cli-test-"))

try {
const runtimePath = join(root, ".codex/config.toml")
const help = await runConfig(root)
expect(help).toMatchObject({ exitCode: 0, stderr: "" })
expect(help.stdout).toContain("Usage:")
expect(await Bun.file(runtimePath).exists()).toBe(false)

const check = await runConfig(root, ["--check"])
expect(check.exitCode).toBe(1)
expect(check.stdout).toContain("Codex runtime config differs")
expect(await Bun.file(runtimePath).exists()).toBe(false)

const run = await runConfig(root, ["--run", "--quiet"])
expect(run).toEqual({ exitCode: 0, stdout: "", stderr: "" })
} finally {
await rm(root, { recursive: true, force: true })
}
})
})
Loading