From dfdd52ec1deaa19324f3aa83d9abc1a392f75166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=20=E9=A7=BF=E7=94=AB=20=28Shunsuke=20Hayashi=29?= Date: Sat, 28 Mar 2026 10:06:15 +0900 Subject: [PATCH] feat(collab): add miyabi collab subcommand for Collaborator canvas control Add `miyabi collab` subcommand (Phase 3 of Miyabi canvas integration): miyabi collab list [--json] [--type TYPE] [--count] miyabi collab add [--file PATH] [--pos x,y] [--size w,h] [--idempotent] miyabi collab rm miyabi collab move --pos x,y miyabi collab resize --size w,h miyabi collab viewport [--pan x,y] [--zoom 1.0] miyabi collab status Delegates to ~/.local/bin/collab (collab CLI v0.2.0+). Error handling includes installation hint on binary not found. Co-Authored-By: Claude Sonnet 4.6 --- crates/miyabi-cli/src/main.rs | 170 ++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/crates/miyabi-cli/src/main.rs b/crates/miyabi-cli/src/main.rs index 96d0938..fcde201 100644 --- a/crates/miyabi-cli/src/main.rs +++ b/crates/miyabi-cli/src/main.rs @@ -101,6 +101,78 @@ enum Commands { #[command(subcommand)] command: OpenclawCommand, }, + /// Collaborator canvas control via collab CLI + Collab { + /// Canvas subcommand + #[command(subcommand)] + command: CollabCommand, + }, +} + +/// Collab canvas subcommands — wraps the collab CLI at ~/.local/bin/collab +#[derive(Subcommand)] +enum CollabCommand { + /// List tiles on the canvas + List { + /// Output as JSON array + #[arg(long)] + json: bool, + /// Filter by tile type (note, code, term, image, graph) + #[arg(long)] + r#type: Option, + /// Count only + #[arg(long)] + count: bool, + }, + /// Add a tile to the canvas + Add { + /// Tile type (note, code, term, image, graph) + tile_type: String, + /// File to attach (required for note/code) + #[arg(long)] + file: Option, + /// Position in grid units "x,y" + #[arg(long)] + pos: Option, + /// Size in grid units "w,h" + #[arg(long)] + size: Option, + /// Skip if tile with same file already exists + #[arg(long)] + idempotent: bool, + }, + /// Remove a tile from the canvas + Rm { + /// Tile ID to remove + tile_id: String, + }, + /// Move a tile to a new position + Move { + /// Tile ID to move + tile_id: String, + /// New position in grid units "x,y" + #[arg(long)] + pos: String, + }, + /// Resize a tile + Resize { + /// Tile ID to resize + tile_id: String, + /// New size in grid units "w,h" + #[arg(long)] + size: String, + }, + /// Get or set the canvas viewport + Viewport { + /// Set pan position "x,y" + #[arg(long)] + pan: Option, + /// Set zoom level (e.g. 1.0) + #[arg(long)] + zoom: Option, + }, + /// Show Collaborator connection status + Status, } #[derive(Subcommand)] @@ -816,6 +888,104 @@ async fn main() -> anyhow::Result<()> { } } } + + Some(Commands::Collab { command }) => { + use std::process::Command; + use std::env; + + let collab_bin = { + let home = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + format!("{}/.local/bin/collab", home) + }; + + let mut args: Vec = Vec::new(); + + match command { + CollabCommand::List { json, r#type, count } => { + args.push("tile".to_string()); + args.push("list".to_string()); + if json { args.push("--json".to_string()); } + if count { args.push("--count".to_string()); } + if let Some(t) = r#type { + args.push("--type".to_string()); + args.push(t); + } + } + CollabCommand::Add { tile_type, file, pos, size, idempotent } => { + args.push("tile".to_string()); + args.push("add".to_string()); + args.push(tile_type); + if let Some(f) = file { + args.push("--file".to_string()); + args.push(f); + } + if let Some(p) = pos { + args.push("--pos".to_string()); + args.push(p); + } + if let Some(s) = size { + args.push("--size".to_string()); + args.push(s); + } + if idempotent { args.push("--idempotent".to_string()); } + } + CollabCommand::Rm { tile_id } => { + args.push("tile".to_string()); + args.push("rm".to_string()); + args.push(tile_id); + } + CollabCommand::Move { tile_id, pos } => { + args.push("tile".to_string()); + args.push("move".to_string()); + args.push(tile_id); + args.push("--pos".to_string()); + args.push(pos); + } + CollabCommand::Resize { tile_id, size } => { + args.push("tile".to_string()); + args.push("resize".to_string()); + args.push(tile_id); + args.push("--size".to_string()); + args.push(size); + } + CollabCommand::Viewport { pan, zoom } => { + if pan.is_some() || zoom.is_some() { + args.push("viewport".to_string()); + args.push("set".to_string()); + if let Some(p) = pan { + args.push("--pan".to_string()); + args.push(p); + } + if let Some(z) = zoom { + args.push("--zoom".to_string()); + args.push(z.to_string()); + } + } else { + args.push("viewport".to_string()); + } + } + CollabCommand::Status => { + args.push("status".to_string()); + } + } + + let status = Command::new(&collab_bin) + .args(&args) + .status(); + + match status { + Ok(s) => { + if !s.success() { + std::process::exit(s.code().unwrap_or(1)); + } + } + Err(e) => { + eprintln!("error: failed to run collab CLI ({}): {}", collab_bin, e); + eprintln!(" → Install collab CLI: https://github.com/ShunsukeHayashi/collab-cli"); + std::process::exit(1); + } + } + } } Ok(())