Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ From the workspace root:

```sh
cargo run -p decodex --bin decodex -- --help
cargo run -p decodex --bin decodex -- app
cargo run -p decodex --bin decodex -- probe stdio://
cargo run -p decodex --bin decodex -- project list
cargo run -p decodex --bin decodex -- status
Expand Down Expand Up @@ -327,6 +328,8 @@ default `127.0.0.1:8192` runtime.

```sh
DECODEX_APP_SERVER_URL=http://127.0.0.1:57399 open -n target/decodex-app/Decodex.app
DECODEX_APP_SERVER_URL=http://127.0.0.1:57399 cargo run -p decodex --bin decodex -- app \
--bundle target/decodex-app/Decodex.app --new
```

Use hidden `decodex serve --dev --listen-address <ADDR>` only when
Expand All @@ -336,8 +339,11 @@ Linear, dispatch work, or accept `--config`. Decodex App's normal
fallback server is ordinary `decodex serve --listen-address 127.0.0.1:8192`; the CLI
owns the default scheduler cadences. App launch connects to an
existing live default listener instead of starting a duplicate server only when
`DECODEX_APP_SERVER_URL` is unset. For dashboard and App preview UI work, prefer the
single mock server above.
`DECODEX_APP_SERVER_URL` is unset. `decodex app` opens the installed Decodex App by
default and preserves the caller's environment, including any explicit
`DECODEX_APP_SERVER_URL` override. Use `--bundle <APP_BUNDLE>` and `--new` when
previewing a staged app. For dashboard and App preview UI work, prefer the single mock
server above.

The dashboard semantics and local-vs-external state boundary live in
`docs/reference/operator-control-plane.md`.
Expand Down
105 changes: 102 additions & 3 deletions apps/decodex/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub(crate) struct Cli {
impl Cli {
pub(crate) fn run(&self) -> Result<()> {
match &self.command {
Command::App(args) => args.run(),
Command::Commit(args) => args.run(),
Command::Land(args) => args.run(),
Command::Run(args) => args.run(),
Expand Down Expand Up @@ -122,6 +123,21 @@ impl ProjectConfigArgs {
}
}

#[derive(Debug, Args)]
struct AppCommand {
/// Open this Decodex app bundle instead of the installed `Decodex` app.
#[arg(long, value_name = "APP_BUNDLE")]
bundle: Option<PathBuf>,
/// Ask LaunchServices to open a new app instance.
#[arg(short = 'n', long)]
new: bool,
}
impl AppCommand {
fn run(&self) -> Result<()> {
open_decodex_app(self.bundle.as_deref(), self.new)
}
}

#[derive(Debug, Args)]
struct AccountCommand {
#[command(subcommand)]
Expand Down Expand Up @@ -1608,6 +1624,8 @@ impl From<IssueDispatchMode> for AttemptDispatchMode {
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
enum Command {
/// Open the native Decodex App.
App(AppCommand),
/// Create a signed local commit with a `decodex/commit/1` message.
Commit(CommitCommand),
/// Land the current reviewed lane with a GitHub admin merge commit.
Expand Down Expand Up @@ -1865,15 +1883,55 @@ fn styles() -> Styles {
.placeholder(AnsiColor::Green.on_default())
}

#[cfg(any(target_os = "macos", test))]
fn decodex_app_open_args(bundle: Option<&Path>, new: bool) -> Vec<std::ffi::OsString> {
let mut args = Vec::new();

if new {
args.push(std::ffi::OsString::from("-n"));
}

if let Some(bundle) = bundle {
args.push(bundle.as_os_str().to_owned());
} else {
args.push(std::ffi::OsString::from("-a"));
args.push(std::ffi::OsString::from("Decodex"));
}

args
}

#[cfg(target_os = "macos")]
fn open_decodex_app(bundle: Option<&Path>, new: bool) -> Result<()> {
let args = decodex_app_open_args(bundle, new);
let status = std::process::Command::new("/usr/bin/open")
.args(args)
.status()
.map_err(|error| eyre::eyre!("Failed to start `open` for Decodex App: {error}"))?;

if !status.success() {
eyre::bail!("Failed to open Decodex App: `open` exited with {status}");
}

println!("Opened Decodex App.");

Ok(())
}

#[cfg(not(target_os = "macos"))]
fn open_decodex_app(_bundle: Option<&Path>, _new: bool) -> Result<()> {
eyre::bail!("`decodex app` is only supported on macOS");
}

#[cfg(test)]
mod tests {
use std::path::Path;
use std::{ffi::OsString, path::Path};

use clap::Parser;

use crate::cli::{
AccountCommand, AccountSubcommand, AccountUseCommand, AttemptCommand, Cli, Command,
CommitCommand, DiagnoseCommand, EvidenceCommand, IntakeCommand, IntakeGoalCommand,
AccountCommand, AccountSubcommand, AccountUseCommand, AppCommand, AttemptCommand, Cli,
Command, CommitCommand, DiagnoseCommand, EvidenceCommand, IntakeCommand, IntakeGoalCommand,
IntakeIssuesCommand, IntakeSubcommand, LandCommand, LaneCommand, LaneInspectCommand,
LaneInterruptCommand, LaneSteerCommand, LaneSubcommand, LegacyCloseoutRecoveryCommand,
MergedCloseoutRecoveryCommand, ProbeCommand, ProjectCommand, ProjectConfigArgs,
Expand All @@ -1889,6 +1947,47 @@ mod tests {
StatusCommand,
};

#[test]
fn parses_app_command() {
let cli = Cli::parse_from(["decodex", "app"]);

assert!(matches!(cli.command, Command::App(AppCommand { bundle: None, new: false })));
}

#[test]
fn parses_app_bundle_and_new_instance() {
let cli = Cli::parse_from([
"decodex",
"app",
"--bundle",
"target/decodex-app/Decodex.app",
"--new",
]);

assert!(matches!(
cli.command,
Command::App(AppCommand {
bundle: Some(bundle),
new: true,
}) if bundle == Path::new("target/decodex-app/Decodex.app")
));
}

#[test]
fn builds_macos_open_arguments_for_decodex_app() {
assert_eq!(
super::decodex_app_open_args(None, false),
vec![OsString::from("-a"), OsString::from("Decodex")]
);
assert_eq!(
super::decodex_app_open_args(Some(Path::new("target/decodex-app/Decodex.app")), true),
vec![
OsString::from("-n"),
Path::new("target/decodex-app/Decodex.app").as_os_str().to_owned(),
]
);
}

#[test]
fn parses_commit_with_authority_related_and_breaking() {
let cli = Cli::parse_from([
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/operator-control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ for future dispatch, uses the CLI-owned default cadences, and serves the dashboa
account APIs, `GET /api/operator-snapshot`,
`POST /api/linear-scan`, `GET /api/lane/inspect`, and `POST /api/lane/interrupt` from
the single local listener.
Use `decodex app` to open the installed macOS app from the CLI; use
`decodex app --bundle <APP_BUNDLE> --new` for a staged app bundle. The launch preserves
the caller's environment, so `DECODEX_APP_SERVER_URL` remains an explicit App preview
override when set.

`decodex serve` has two hardcoded scheduler cadences:

Expand Down