diff --git a/README.md b/README.md index aecb371..35110ac 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ `codea-cli` is a command line tool for working with [Codea](https://codea.io/) runtimes over MCP. +Agent-facing workflow guidance lives in [SKILL.md](/Users/sim/Developer/Open/codea-cli/SKILL.md). + ## What It Does The `codea` binary can: @@ -108,8 +110,8 @@ Environment variables override stored config: `codea new` is target-aware: - if no host is configured, it creates a local filesystem project -- if the connected host reports `projectStorage == "filesystem"`, it creates a local filesystem project. These hosts are typically iPad or iPhones running Codea -- if the connected host reports `projectStorage == "collections"`, it creates the project remotely via MCP. These hosts are the macOS Carbide.app or Codea.app +- if the connected host reports `projectStorage == "filesystem"`, it creates a local filesystem project. These hosts are typically the macOS Carbide.app or Codea.app +- if the connected host reports `projectStorage == "collections"`, it creates the project remotely via MCP. These hosts are typically iPhone or iPad devices running Codea For local project creation, only the `Modern` template is supported. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..f9bb047 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,321 @@ +--- +name: codea +description: Control Codea on a connected iOS, iPadOS, or macOS device. Use this skill when working on Codea projects — pulling code, editing files, pushing changes, running projects, capturing screenshots, and inspecting state via Lua. +--- + +# Codea Skill + +This directory contains the `codea` CLI tool for working with Codea projects on a connected iOS, iPadOS, or macOS device. + +The most important distinction is the target type: + +- `Project storage = filesystem` + Use the local filesystem workflow. This is the macOS Codea / Carbide case. Edit files directly on disk and `run` the project by path. Do not use `pull` / `push` unless you specifically need them for some other reason. +- `Project storage = collections` + Use the repository workflow. This is the iOS / iPadOS Codea case. Pull projects from the device, edit locally, then push changes back. + +## Setup + +Check whether the CLI is already installed: + +```bash +codea --help +``` + +Build it locally if needed: + +```bash +cargo build +./target/debug/codea --help +``` + +Connect to a target: + +```bash +codea discover +codea configure --host 192.168.1.42 --port 18513 +``` + +Or use environment variables: + +```bash +export CODEA_HOST=192.168.1.42 +export CODEA_PORT=18513 +``` + +If `codea` is not on `PATH`, use `./target/debug/codea ...`. + +## Determine The Target Type + +Before choosing a workflow, query the current target state and check `Project storage`. + +```bash +codea status +``` + +- If `Project storage` is `filesystem`, the target is a local macOS app and projects live directly on disk. +- If `Project storage` is `collections`, the target is using Codea's project repository model and projects should be accessed through `pull` / `push`. + +Also pay attention to `Project path` when present. This is the canonical running project identifier: + +- `Examples/Flappy` or `iCloud/Documents/Foo` for collection-backed targets +- `/path/to/MyGame.codea` for filesystem-backed targets + +## Project Naming + +Collection-backed projects are identified as `Collection/Project` or just `Project` if the name is unique: + +```bash +codea pull "Morse" +codea pull "Documents/Morse" +codea pull "iCloud/Documents/Foo" +``` + +Filesystem-backed targets are usually addressed by local path: + +```bash +codea run /path/to/MyGame.codea +codea run /path/to/MyGame +``` + +## Typical Agent Workflow + +Always disable the idle timer at the start of a device session so the target stays awake: + +```bash +codea idle-timer off +``` + +### Filesystem-backed workflow (`projectStorage = filesystem`) + +Use this for local macOS Codea / Carbide targets. + +```bash +# 1. Work directly in the project directory +cd /path/to/MyGame.codea + +# 2. Read and edit files with normal filesystem tools + +# 3. Run the project by path +codea clear-logs +codea logs --follow >> /tmp/codea.log & +codea run /path/to/MyGame.codea +sleep 2 +codea screenshot --output result.png + +# 4. Inspect runtime state +codea exec "print(WIDTH, HEIGHT)" +cat /tmp/codea.log + +# 5. Iterate by editing files on disk, then restart or run again +codea restart +``` + +For filesystem-backed targets, `push` and `pull` are normally unnecessary because the agent can already access the same files directly. + +### Collection-backed workflow (`projectStorage = collections`) + +Use this for iPhone / iPad Codea targets. + +```bash +# 1. Pull the project and its dependencies +codea pull "My Game" + +# 2. Read and edit files locally + +# 3. Push only the changed files when possible +codea push "My Game" Main.lua Player.lua + +# 4. Start logs, run, and inspect +codea clear-logs +codea logs --follow >> /tmp/codea.log & +codea run "My Game" +sleep 3 +codea screenshot --output result.png + +# 5. Execute Lua to inspect state +codea exec "print(health)" + +# 6. Check logs +cat /tmp/codea.log + +# 7. Iterate +codea push "My Game" Main.lua +codea restart +sleep 2 +cat /tmp/codea.log +``` + +### Creating a new project + +`codea new` is target-aware: + +- On filesystem-backed targets it creates a local project on disk. +- On collection-backed targets it creates a project in the target repository. + +Examples: + +```bash +codea new "My Game" +codea new "My Game" --folder +codea new "My Game" --template Modern +codea new "Documents/My Game" +codea new "iCloud/Documents/My Game" +``` + +After creation: + +- On filesystem-backed targets, edit the created directory directly and `run` it by path. +- On collection-backed targets, `pull` it locally, edit, `push`, then `run`. + +## Global Flag: `--wait` + +```bash +codea --wait +``` + +This waits for the Air Code server to respond before running the command. Prefer this over asking the user to manually foreground Codea. + +```bash +codea --wait ls +codea --wait run "My Game" +codea --wait run /path/to/MyGame.codea +``` + +## Commands + +### Device +| Command | Description | +|---------|-------------| +| `codea discover` | Scan the local network for Codea devices and save config | +| `codea configure` | Manually set device host/port | +| `codea status` | Show current device config and live state | + +### Collections +| Command | Description | +|---------|-------------| +| `codea collections ls` | List all collections | +| `codea collections new ` | Create a new local collection | +| `codea collections delete ` | Delete a collection | + +### Projects +| Command | Description | +|---------|-------------| +| `codea ls` | List all projects as `Collection/Project` | +| `codea new ` | Create a new project; local or remote depending on `projectStorage` | +| `codea rename ` | Rename a project | +| `codea move ` | Move a project | +| `codea delete ` | Delete a project | +| `codea runtime ` | Get runtime type | +| `codea runtime ` | Set runtime type | + +### Files +| Command | Description | +|---------|-------------| +| `codea pull [files...]` | Pull project files locally | +| `codea push [files...]` | Push files back to the target | + +### Runtime +| Command | Description | +|---------|-------------| +| `codea run ` | Start a project by repository name or filesystem path | +| `codea stop` | Stop the running project | +| `codea restart` | Restart the running project | +| `codea exec ""` | Execute Lua in the running project | +| `codea exec --file ` | Execute a Lua file | +| `codea pause` | Pause the running project | +| `codea resume` | Resume the running project | +| `codea paused [on\|off]` | Get or set paused state | +| `codea screenshot [--output ]` | Capture a screenshot | +| `codea idle-timer ` | Get or set idle timer | +| `codea logs` | Get log output | +| `codea logs --head N` | Get first N lines | +| `codea logs --tail N` | Get last N lines | +| `codea logs --follow` | Stream logs in real time | +| `codea clear-logs` | Clear the log buffer | + +### Templates +| Command | Description | +|---------|-------------| +| `codea templates ls` | List all templates | +| `codea templates add ` | Add a custom template | +| `codea templates remove ` | Remove a custom template | + +### Dependencies +| Command | Description | +|---------|-------------| +| `codea deps ls ` | List project dependencies | +| `codea deps available ` | List addable dependencies | +| `codea deps add ` | Add a dependency | +| `codea deps remove ` | Remove a dependency | + +### Documentation +| Command | Description | +|---------|-------------| +| `codea autocomplete ` | Get completions for a Lua prefix | +| `codea doc ` | Show API docs for the current runtime context; defaults to the running project's runtime, otherwise `modern` | +| `codea doc --all` | Show both modern and legacy docs | +| `codea doc --modern` | Show modern docs only | +| `codea doc --legacy` | Show legacy docs only | +| `codea doc --project ` | Filter docs by that project's runtime | +| `codea doc --project` | Filter docs by the currently running project's runtime | +| `codea search-doc ` | Search docs for the current runtime context; defaults to the running project's runtime, otherwise `modern` | +| `codea search-doc --all` | Search both modern and legacy docs | +| `codea search-doc --modern` | Search modern docs only | +| `codea search-doc --legacy` | Search legacy docs only | +| `codea search-doc --project ` | Search docs using that project's runtime | +| `codea search-doc --project` | Search docs using the currently running project's runtime | + +When `--all` is given, `doc` and `search-doc` return both modern and legacy entries. + +When no runtime flags are given, `doc` and `search-doc` resolve runtime in this order: + +1. `--project ` +2. bare `--project` using the currently running project +3. the currently running project's runtime automatically +4. `modern` if no project is running + +On filesystem-backed macOS targets, runtime should be treated as `modern`. + +## Pull / Push Details + +`codea pull "My Game"` creates: + +```text +My Game/ + Main.lua + Player.lua + ... + Dependencies/ + PhysicsLib/ + Physics.lua +``` + +`codea push "My Game"` pushes all files in `./My Game/` back, routing `Dependencies//` files to the correct project on the target. + +Use `--output ` with pull and `--input ` with push to specify custom directories. + +## File Loading Order (`Info.plist`) + +Each Codea project contains an `Info.plist` file. The `Buffer Order` array defines the file load order. When adding new `.lua` files, update `Info.plist` and either push it back to the collection-backed target or keep it correct in the local project directory on filesystem-backed targets. + +## Log Monitoring with `--follow` + +The recommended pattern is: + +```bash +codea clear-logs +codea logs --follow >> /tmp/codea.log & +codea run "My Game" + +cat /tmp/codea.log +tail -n 20 /tmp/codea.log +``` + +For filesystem-backed targets, replace `"My Game"` with a local path as needed. + +Kill the background stream when done: + +```bash +kill %1 +``` diff --git a/src/main.rs b/src/main.rs index b8e92be..27e9dcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ mod discover; mod local; mod mcp; -use anyhow::{Result, bail}; +use anyhow::{Result, anyhow, bail}; use clap::{ArgAction, Args, Parser, Subcommand}; use config::{DEFAULT_PORT, ProfileConfig}; use serde_json::{Value, json}; @@ -296,11 +296,13 @@ struct RuntimeArgs { #[derive(Args, Debug)] struct DocArgs { function_name: String, + #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["legacy", "modern", "project"])] + all: bool, #[arg(long, action = ArgAction::SetTrue)] legacy: bool, #[arg(long, action = ArgAction::SetTrue)] modern: bool, - #[arg(long)] + #[arg(long, num_args = 0..=1, default_missing_value = "")] project: Option, #[arg(long, default_value = "default")] profile: String, @@ -309,11 +311,13 @@ struct DocArgs { #[derive(Args, Debug)] struct SearchDocArgs { query: String, + #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["legacy", "modern", "project"])] + all: bool, #[arg(long, action = ArgAction::SetTrue)] legacy: bool, #[arg(long, action = ArgAction::SetTrue)] modern: bool, - #[arg(long)] + #[arg(long, num_args = 0..=1, default_missing_value = "")] project: Option, #[arg(long, default_value = "default")] profile: String, @@ -442,7 +446,12 @@ fn status_command(profile: &str) -> Result<()> { println!(); let project_state = state.get("state").and_then(Value::as_str).unwrap_or("none"); let project_name = state.get("project").and_then(Value::as_str); - let local_path = state.get("localPath").and_then(Value::as_str); + let project_path = state.get("projectPath").and_then(Value::as_str); + let project_storage = state + .get("projectStorage") + .and_then(Value::as_str) + .unwrap_or("collections"); + let runtime = state.get("runtime").and_then(Value::as_str); let idle_disabled = state .get("idleTimerDisabled") .and_then(Value::as_bool) @@ -453,7 +462,9 @@ fn status_command(profile: &str) -> Result<()> { .unwrap_or(false); if project_state == "running" { - let mut label = if let Some(project_name) = project_name { + let mut label = if let Some(project_path) = project_path { + format!("Running: {project_path}") + } else if let Some(project_name) = project_name { format!("Running: {project_name}") } else { "Running".to_string() @@ -465,8 +476,12 @@ fn status_command(profile: &str) -> Result<()> { } else { println!("State: No project running"); } - if let Some(local_path) = local_path { - println!("Local path: {local_path}"); + if let Some(project_path) = project_path { + println!("Project path: {project_path}"); + } + println!("Project storage: {project_storage}"); + if let Some(runtime) = runtime { + println!("Runtime: {runtime}"); } println!( "Idle timer: {}", @@ -976,12 +991,14 @@ fn runtime_command(args: RuntimeArgs, wait: bool) -> Result<()> { } fn doc_command(args: DocArgs, wait: bool) -> Result<()> { - let filter_runtime = resolve_runtime_filter(args.legacy, args.modern)?; let mut client = client_for_profile(&args.profile, wait)?; - let filter_runtime = match (&args.project, filter_runtime) { - (Some(project), None) => Some(client.get_runtime(project)?), - (_, filter_runtime) => filter_runtime, - }; + let filter_runtime = resolve_doc_runtime_filter( + &mut client, + args.all, + args.legacy, + args.modern, + args.project.as_deref(), + )?; let result = client.get_function_help(&args.function_name)?; let mut modern = result.get("modern").cloned(); @@ -1040,12 +1057,14 @@ fn doc_command(args: DocArgs, wait: bool) -> Result<()> { } fn search_doc_command(args: SearchDocArgs, wait: bool) -> Result<()> { - let filter_runtime = resolve_runtime_filter(args.legacy, args.modern)?; let mut client = client_for_profile(&args.profile, wait)?; - let filter_runtime = match (&args.project, filter_runtime) { - (Some(project), None) => Some(client.get_runtime(project)?), - (_, filter_runtime) => filter_runtime, - }; + let filter_runtime = resolve_doc_runtime_filter( + &mut client, + args.all, + args.legacy, + args.modern, + args.project.as_deref(), + )?; let mut results = client .search_docs(&args.query)? @@ -1156,6 +1175,57 @@ fn resolve_runtime_filter(legacy: bool, modern: bool) -> Result> } } +fn resolve_doc_runtime_filter( + client: &mut MCPClient, + all: bool, + legacy: bool, + modern: bool, + project: Option<&str>, +) -> Result> { + if all { + return Ok(None); + } + + if let Some(filter_runtime) = resolve_runtime_filter(legacy, modern)? { + return Ok(Some(filter_runtime)); + } + + if let Some(project) = project { + if project.is_empty() { + let state = client.get_device_state()?; + if state.get("state").and_then(Value::as_str) != Some("running") { + bail!("No project is currently running."); + } + if let Some(runtime) = state.get("runtime").and_then(Value::as_str) { + return Ok(Some(runtime.to_string())); + } + let project_path = state + .get("projectPath") + .and_then(Value::as_str) + .or_else(|| state.get("project").and_then(Value::as_str)) + .ok_or_else(|| anyhow!("No project is currently running."))?; + return Ok(Some(client.get_runtime(project_path)?)); + } + return Ok(Some(client.get_runtime(project)?)); + } + + let state = client.get_device_state()?; + if state.get("state").and_then(Value::as_str) == Some("running") { + if let Some(runtime) = state.get("runtime").and_then(Value::as_str) { + return Ok(Some(runtime.to_string())); + } + if let Some(project_path) = state + .get("projectPath") + .and_then(Value::as_str) + .or_else(|| state.get("project").and_then(Value::as_str)) + { + return Ok(Some(client.get_runtime(project_path)?)); + } + } + + Ok(Some("modern".to_string())) +} + fn pull_project_files( client: &mut MCPClient, project_path: &str,