From 2b346197de11ee7021477b9e6b9a4819abeddb1e Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Wed, 25 Mar 2026 22:32:11 +1030 Subject: [PATCH 1/2] Adds SKILL definition and print Project Storage type from codea status --- README.md | 2 + SKILL.md | 299 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 + 3 files changed, 306 insertions(+) create mode 100644 SKILL.md diff --git a/README.md b/README.md index aecb371..2e90167 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: diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..7bac349 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,299 @@ +--- +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 repository contains the Rust `codea` CLI. This skill explains how an agent should use that CLI effectively. + +The most important distinction is the target type: + +- `projectStorage = 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. +- `projectStorage = 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 `projectStorage`. + +- If `projectStorage` is `filesystem`, the target is a local macOS app and projects live directly on disk. +- If `projectStorage` is `collections`, the target is using Codea's project repository model and projects should be accessed through `pull` / `push`. + +Also pay attention to `localPath` when present. That indicates the currently running project path on 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 | +| `codea doc --modern` | Show modern docs only | +| `codea doc --legacy` | Show legacy docs only | +| `codea doc --project ` | Filter docs by project runtime | +| `codea search-doc ` | Search docs | +| `codea search-doc --modern` | Search modern docs only | +| `codea search-doc --legacy` | Search legacy docs only | +| `codea search-doc --project ` | Search docs using project runtime | + +## 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..669c6ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -443,6 +443,10 @@ fn status_command(profile: &str) -> Result<()> { 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_storage = state + .get("projectStorage") + .and_then(Value::as_str) + .unwrap_or("collections"); let idle_disabled = state .get("idleTimerDisabled") .and_then(Value::as_bool) @@ -468,6 +472,7 @@ fn status_command(profile: &str) -> Result<()> { if let Some(local_path) = local_path { println!("Local path: {local_path}"); } + println!("Project storage: {project_storage}"); println!( "Idle timer: {}", if idle_disabled { From 0aea4c39ef34dffade6494abdf314309d435c525 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Fri, 27 Mar 2026 23:50:46 +1030 Subject: [PATCH 2/2] Re-adds support for showing both modern and legacy docs with --all flag --- README.md | 4 +-- SKILL.md | 44 ++++++++++++++++++------ src/main.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 2e90167..35110ac 100644 --- a/README.md +++ b/README.md @@ -110,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 index 7bac349..f9bb047 100644 --- a/SKILL.md +++ b/SKILL.md @@ -5,13 +5,13 @@ description: Control Codea on a connected iOS, iPadOS, or macOS device. Use this # Codea Skill -This repository contains the Rust `codea` CLI. This skill explains how an agent should use that CLI effectively. +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: -- `projectStorage = filesystem` +- `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. -- `projectStorage = collections` +- `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 @@ -47,12 +47,19 @@ 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 `projectStorage`. +Before choosing a workflow, query the current target state and check `Project storage`. -- If `projectStorage` is `filesystem`, the target is a local macOS app and projects live directly on disk. -- If `projectStorage` is `collections`, the target is using Codea's project repository model and projects should be accessed through `pull` / `push`. +```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: -Also pay attention to `localPath` when present. That indicates the currently running project path on filesystem-backed targets. +- `Examples/Flappy` or `iCloud/Documents/Foo` for collection-backed targets +- `/path/to/MyGame.codea` for filesystem-backed targets ## Project Naming @@ -246,14 +253,29 @@ codea --wait run /path/to/MyGame.codea | Command | Description | |---------|-------------| | `codea autocomplete ` | Get completions for a Lua prefix | -| `codea doc ` | Show API docs | +| `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 project runtime | -| `codea search-doc ` | Search docs | +| `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 project runtime | +| `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 diff --git a/src/main.rs b/src/main.rs index 669c6ce..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,11 +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) @@ -457,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() @@ -469,10 +476,13 @@ 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: {}", if idle_disabled { @@ -981,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(); @@ -1045,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)? @@ -1161,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,