1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
mod commands;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "arcane", version, about = "Arcane 2D game engine CLI")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Discover and run *.test.ts files in embedded V8
Test {
/// Optional directory or glob pattern (defaults to current directory)
path: Option<String>,
},
/// Open a window and run a game with hot-reload
Dev {
/// Path to the TypeScript entry file (defaults to src/visual.ts)
entry: Option<String>,
/// Enable HTTP inspector on the given port (e.g. --inspector 4321)
#[arg(long)]
inspector: Option<u16>,
/// MCP server port (default: auto-assign a free port, use --no-mcp to disable)
#[arg(long, default_value = "0")]
mcp_port: u16,
/// Disable the MCP server
#[arg(long)]
no_mcp: bool,
},
/// Stdio bridge for MCP (JSON-RPC over stdin/stdout)
Mcp {
/// Path to the TypeScript entry file (defaults to src/visual.ts)
entry: Option<String>,
/// MCP HTTP server port to connect to (default: auto-discover via .arcane/mcp-port)
#[arg(long)]
port: Option<u16>,
},
/// Print a text description of game state (headless)
Describe {
/// Path to the TypeScript entry file
entry: String,
/// Verbosity: minimal, normal, or detailed
#[arg(long)]
verbosity: Option<String>,
},
/// Inspect game state at a specific path (headless)
Inspect {
/// Path to the TypeScript entry file
entry: String,
/// Dot-separated state path (e.g. "player.hp")
path: String,
},
/// Create a new Arcane project from template
New {
/// Project name
name: String,
},
/// Initialize an Arcane project in the current directory
Init,
/// Type-check the project (fast, no tests). Use after every edit.
Check {
/// Optional directory or entry file
path: Option<String>,
},
/// Browse and select CC0 game assets (sprites & sounds)
Catalog {
/// Open a specific pack directly (e.g. "tiny-dungeon" or "digital-audio")
pack_id: Option<String>,
/// Browse sound packs instead of sprites
#[arg(long)]
sounds: bool,
/// Browser application to use (e.g. "safari", "firefox")
#[arg(long)]
browser: Option<String>,
},
/// Capture a screenshot from the running game window
Screenshot {
/// Output file path (e.g. "screenshot.png")
output: String,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Test { path } => commands::test::run(path),
Commands::Dev { entry, inspector, mcp_port, no_mcp } => {
let entry = entry.unwrap_or_else(|| "src/visual.ts".to_string());
let mcp = if no_mcp { None } else { Some(mcp_port) };
commands::dev::run(entry, inspector, mcp)
},
Commands::Mcp { entry, port } => {
let entry = entry.unwrap_or_else(|| "src/visual.ts".to_string());
commands::mcp_bridge::run(entry, port)
},
Commands::Describe { entry, verbosity } => commands::describe::run(entry, verbosity),
Commands::Inspect { entry, path } => commands::inspect::run(entry, path),
Commands::New { name } => commands::new::run(&name),
Commands::Init => commands::init::run(),
Commands::Check { path } => commands::check::run(path),
Commands::Catalog { pack_id, sounds, browser } => commands::catalog::run(pack_id, sounds, browser),
Commands::Screenshot { output } => commands::screenshot::run(output),
}
}