Thank you for your interest in contributing to opencode-cloud! This document provides guidelines and instructions for contributing.
- Rust 1.89+ (for Rust 2024 edition; matches
rust-toolchain.toml) - Node.js 20+
- Bun 1.3.9+
- just (task runner)
- Docker — Docker Desktop or Docker Engine (for building/running the sandbox container)
# Install Rust (if needed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install just
cargo install just
# or: brew install just
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Clone the repository
# GitHub (primary)
git clone https://github.com/pRizz/opencode-cloud.git
# Gitea (mirror)
git clone https://gitea.com/pRizz/opencode-cloud.git
cd opencode-cloud
# Check prerequisites (can run before installing just)
bash scripts/check-dev-prereqs.sh
# First command after clone/worktree init (hooks + deps + submodule bootstrap)
just setup
# Build everything
just build
# Recommended local dev runtime (local submodule + cached sandbox rebuild)
just devSetup reference:
- Rust toolchain:
1.89(fromrust-toolchain.toml) - Bun:
1.3.9+ - First command after clone/worktree init:
just setup - Optional tools (
docker,jq,shellcheck,actionlint,cfn-lint) are required only for specific flows (just dev,just lint, and CloudFormation hook checks).just setupwarns if they are missing. - Rerun
just setupfor new clones/worktrees, if hooks are reset, or if dependency bootstrap looks stale.
# Run all tests
just test
# Run only Rust tests
just test-rust
# Run only Node tests
just test-node# Check linting
just lint
# Auto-format code
just fmtThe root README (README.md) and submodule README (packages/opencode/README.md) have distinct generated badge blocks.
Both are sourced from packages/opencode/packages/fork-ui/src/readme-badge-catalog.ts.
# Regenerate both README badge sections
just sync-readme-badges
# Validate badge sections are in sync (also run by just lint)
just check-readme-badgesDo not hand-edit badge lines between generated marker comments in either README.
We follow Conventional Commits:
type(scope): description
[optional body]
[optional footer]
feat: A new featurefix: A bug fixdocs: Documentation only changesstyle: Changes that don't affect meaning (formatting, etc.)refactor: Code change that neither fixes a bug nor adds a featureperf: Performance improvementtest: Adding or correcting testschore: Changes to build process or auxiliary tools
feat(cli): add --json flag for machine-readable output
fix(config): handle missing config directory on first run
docs(readme): add installation instructions for Windows
- Fork the repository and create your branch from
main - Make your changes following our coding standards
- Write tests for any new functionality
- Ensure all tests pass:
just test - Ensure linting passes:
just lint - Update documentation if needed
- Submit a pull request with a clear description
opencode-cloud/
├── packages/
│ ├── core/ # Rust core library + NAPI bindings
│ ├── cli-rust/ # Rust CLI binary (source of truth)
│ └── cli-node/ # Node.js CLI wrapper (passthrough)
├── Cargo.toml # Rust workspace root
├── package.json # Node.js workspace root
├── bun.lock # Bun lockfile
└── justfile # Task orchestration
opencode-cloud has two CLI entry points that work together:
-
Rust CLI (
packages/cli-rust) - Source of truth- Standalone binary:
occ - Contains all command logic
- Can be installed via
cargo install opencode-cloud
- Standalone binary:
-
Node CLI (
packages/cli-node) - Transparent passthrough- Wrapper that spawns the Rust binary
- Published to npm as
opencode-cloud - Zero logic - just
spawn(rustBinary, args, { stdio: 'inherit' })
When a user runs npx opencode-cloud start:
- Node CLI (
packages/cli-node/src/index.ts) receives the command - It spawns the Rust binary with all arguments passed through
- Rust CLI (
packages/cli-rust) handles the command - Output flows back through unchanged
This means:
- TTY detection works - Colors and interactive prompts preserve their behavior
- Exit codes propagate - Scripts can rely on proper exit codes
- No duplication - Command logic lives in one place
- Node changes rarely needed - Adding commands only requires Rust updates
To add a new command (e.g., occ shell):
Create packages/cli-rust/src/commands/shell.rs:
use clap::Args;
use anyhow::Result;
#[derive(Args)]
pub struct ShellArgs {
/// Shell to use (default: bash)
#[arg(short, long, default_value = "bash")]
shell: String,
}
pub async fn cmd_shell(args: &ShellArgs, quiet: bool) -> Result<()> {
// Implementation here
todo!()
}Add to packages/cli-rust/src/commands/mod.rs:
mod shell;
pub use shell::{ShellArgs, cmd_shell};Update packages/cli-rust/src/lib.rs:
#[derive(Subcommand)]
enum Commands {
// ... existing commands
/// Open a shell in the container
Shell(commands::ShellArgs),
}In the match cli.command block in lib.rs:
Some(Commands::Shell(args)) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(commands::cmd_shell(&args, cli.quiet))
}# Build both CLIs
just build
# Test Rust CLI directly
./target/release/occ shell --help
# Test Node CLI (uses Rust binary)
./packages/cli-node/bin/occ shell --helpThat's it! No changes needed in packages/cli-node - it automatically passes through the new command.
# Test specific command
cargo test -p cli-rust cmd_shell
# Integration test
./target/release/occ shell --shell zsh
# Verify Node wrapper works
node packages/cli-node/dist/index.js shell --shell zshLet's walk through a complete example of adding occ shell to access the container terminal.
Step 1: Create the command module
touch packages/cli-rust/src/commands/shell.rsStep 2: Implement the command
use anyhow::Result;
use clap::Args;
use opencode_cloud_core::DockerClient;
#[derive(Args)]
pub struct ShellArgs {
/// Shell to use (default: bash)
#[arg(short, long, default_value = "bash")]
shell: String,
/// User to run shell as
#[arg(short, long)]
user: Option<String>,
}
pub async fn cmd_shell(args: &ShellArgs, quiet: bool) -> Result<()> {
let client = DockerClient::new()?;
if !client.is_container_running().await? {
anyhow::bail!("Container is not running. Start it with: occ start");
}
// Exec into container with the specified shell
client.exec_interactive(&args.shell, args.user.as_deref()).await?;
Ok(())
}Step 3-5: Register and build (as shown above)
Now users can run:
occ shell # Default bash
occ shell --shell zsh # Custom shell
npx opencode-cloud shell # Works via Node wrapper too!Before submitting a PR, ensure:
-
just fmt- Code is formatted -
just lint- No linting errors -
just test- All tests pass -
just build- Release build succeeds - New commands documented in code with
///doc comments - Breaking changes noted in PR description
For detailed code style guidelines, see CLAUDE.md.
- Follow standard Rust conventions
- Use
cargo fmtfor formatting - Use
cargo clippyfor linting - Prefer
?for error propagation overunwrap() - Document public APIs with
///comments
- Use strict mode
- Follow ESM conventions
- Keep the Node CLI thin - it should only spawn the Rust binary
- Logic belongs in Rust core, not Node wrapper
Feel free to open an issue for any questions about contributing.