first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+324 -23
View File
@@ -1,11 +1,18 @@
use std::{fmt, path::PathBuf};
use std::fmt;
use std::path::PathBuf;
use clap::{Args, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use crate::{
config_file::ConfigFileArgs, environment::EnvironmentCreateArgs, mcp::MCPSpec,
model::ModelArgs, scope::ObjectScope, share::ShareArgs, skill::SkillSpec,
};
use crate::SortOrderArg;
use crate::config_file::ConfigFileArgs;
use crate::environment::EnvironmentCreateArgs;
use crate::json_filter::JsonOutput;
use crate::mcp::MCPSpec;
use crate::model::ModelArgs;
use crate::scope::ObjectScope;
use crate::share::ShareArgs;
use crate::skill::SkillSpec;
/// Output format for agent results.
#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq, Default)]
@@ -119,7 +126,8 @@ impl HiddenComputerUseArgs {
}
}
/// The execution harness for an agent run.
#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq, Default)]
#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Harness {
/// Use Warp's built-in MAA infrastructure (default).
#[default]
@@ -134,10 +142,14 @@ pub enum Harness {
/// Delegate to the `gemini` CLI.
#[value(name = "gemini")]
Gemini,
/// Delegate to the `codex` CLI.
#[value(name = "codex")]
Codex,
/// A harness produced by a newer client/server that this client doesn't
/// recognize. Surfaced via deserialization fallbacks (e.g. unknown GraphQL
/// enum values, unknown `harness_type` strings); never selectable from the
/// CLI or harness dropdown.
#[serde(other)]
#[value(skip)]
Unknown,
}
@@ -150,7 +162,7 @@ impl Harness {
pub fn parse_local_child_harness(value: &str) -> Option<Self> {
match Self::parse_orchestration_harness(value) {
Some(harness @ (Self::Claude | Self::OpenCode)) => Some(harness),
Some(harness @ (Self::Claude | Self::OpenCode | Self::Codex)) => Some(harness),
Some(Self::Oz) | Some(Self::Gemini) | Some(Self::Unknown) | None => None,
}
}
@@ -161,24 +173,57 @@ impl Harness {
Self::Claude => "Claude Code",
Self::OpenCode => "OpenCode",
Self::Gemini => "Gemini CLI",
Self::Codex => "Codex",
Self::Unknown => "Unknown",
}
}
/// Parses a harness config-name string (the lowercase name written into
/// `HarnessConfig::harness_type` by the spawner, e.g. `"claude"`, `"gemini"`, `"oz"`)
/// into a [`Harness`] variant. Inverse of [`Harness::config_name`]. Returns `None` for
/// unrecognized names so callers can distinguish a future-server harness from a
/// round-tripped [`Harness::Unknown`]; callers that want to fall back to `Unknown`
/// should `.unwrap_or(Harness::Unknown)`. UI surfaces should treat `Unknown` as a
/// non-Oz, non-runnable harness.
pub fn from_config_name(name: &str) -> Option<Self> {
match name {
"oz" => Some(Harness::Oz),
"claude" => Some(Harness::Claude),
"opencode" => Some(Harness::OpenCode),
"gemini" => Some(Harness::Gemini),
"codex" => Some(Harness::Codex),
"unknown" => Some(Harness::Unknown),
_ => None,
}
}
/// Canonical config name for this harness (the lowercase string written into
/// `HarnessConfig::harness_type`). Inverse of [`Harness::from_config_name`].
/// The exhaustive match here forces every new [`Harness`] variant to declare a
/// canonical name, which prevents `from_config_name` from silently falling back to
/// `Unknown` when a new variant is added.
pub fn config_name(self) -> &'static str {
match self {
Harness::Oz => "oz",
Harness::Claude => "claude",
Harness::OpenCode => "opencode",
Harness::Gemini => "gemini",
Harness::Codex => "codex",
Harness::Unknown => "unknown",
}
}
}
impl fmt::Display for Harness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Harness::Oz => "oz",
Harness::Claude => "claude",
Harness::OpenCode => "opencode",
Harness::Gemini => "gemini",
Harness::Unknown => "unknown",
};
f.write_str(name)
f.write_str(self.config_name())
}
}
#[cfg(test)]
#[path = "agent_tests.rs"]
mod tests;
/// Profile subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum AgentProfileCommand {
@@ -197,7 +242,33 @@ pub enum AgentCommand {
#[command(subcommand)]
Profile(AgentProfileCommand),
/// List all available agents.
List(ListAgentConfigsArgs),
List(AgentListArgs),
/// Get details of an agent.
Get(AgentGetArgs),
/// Create a new agent.
Create(AgentCreateArgs),
/// Update an existing agent.
Update(AgentUpdateArgs),
/// Delete an agent.
Delete(AgentDeleteArgs),
/// List available agent skills.
Skills(ListAgentSkillsArgs),
}
impl AgentCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
AgentCommand::Run(_) => "agent run",
AgentCommand::RunCloud(_) => "agent run-cloud",
AgentCommand::Profile(_) => "agent profile",
AgentCommand::List(_) => "agent list",
AgentCommand::Get(_) => "agent get",
AgentCommand::Create(_) => "agent create",
AgentCommand::Update(_) => "agent update",
AgentCommand::Delete(_) => "agent delete",
AgentCommand::Skills(_) => "agent skills",
}
}
}
#[derive(Debug, Clone, Args)]
@@ -230,8 +301,8 @@ pub struct RunAgentArgs {
///
/// When used with --prompt, the skill provides the base context and the prompt is the task.
///
/// To automate a skill on a schedule, use `oz schedule create --skill <SPEC>`.
#[arg(long = "skill", value_name = "SPEC")]
/// To automate a skill on a schedule, use `oz schedule create --skill <SKILL>`.
#[arg(long = "skill", value_name = "SKILL")]
pub skill: Option<SkillSpec>,
/// Name for this agent task.
@@ -257,6 +328,15 @@ pub struct RunAgentArgs {
/// LEGACY: MCP servers to start before executing the agent, identified by UUID.
#[arg(long = "mcp-server", value_name = "UUID", hide = true)]
pub mcp_servers: Vec<uuid::Uuid>,
/// Fail the run when any requested MCP server fails to start.
///
/// By default, MCP servers that don't start within the startup timeout are
/// skipped and the agent runs without their tools.
#[arg(long = "strict-mcp-startup")]
pub strict_mcp_startup: bool,
/// Maximum time to wait for requested MCP servers to start (e.g. `30s`, `1m`).
#[arg(long = "mcp-startup-timeout", value_name = "DURATION")]
pub mcp_startup_timeout: Option<humantime::Duration>,
/// Cloud environment to use, identified by ID.
#[arg(long = "environment", short = 'e', value_name = "ID")]
pub environment: Option<String>,
@@ -278,6 +358,10 @@ pub struct RunAgentArgs {
#[command(flatten)]
pub snapshot: SnapshotArgs,
/// Identifier for the task that spawned this agent, used to report progress.
///
/// When `--conversation` is omitted, the conversation id is read off the server-side
/// task metadata. Some worker follow-up call sites still pass both flags, so keep
/// accepting the compatibility shape until all producers have been updated.
#[arg(long = "task-id", hide = true, conflicts_with_all = ["prompt", "saved_prompt", "file"])]
pub task_id: Option<String>,
@@ -285,9 +369,25 @@ pub struct RunAgentArgs {
#[arg(long = "sandboxed", hide = true)]
pub sandboxed: bool,
/// IAM role ARN to use for federated AWS Bedrock credentials for this run.
#[arg(long = "bedrock-inference-role", value_name = "ROLE_ARN", hide = true)]
#[arg(
long = "bedrock-inference-role",
value_name = "ROLE_ARN",
requires = "bedrock_role_region",
hide = true
)]
pub bedrock_inference_role: Option<String>,
/// AWS region to use for the STS `AssumeRoleWithWebIdentity` call that
/// mints federated Bedrock credentials. Required together with
/// `--bedrock-inference-role`.
#[arg(
long = "bedrock-role-region",
value_name = "REGION",
requires = "bedrock_inference_role",
hide = true
)]
pub bedrock_role_region: Option<String>,
#[command(flatten)]
pub computer_use: HiddenComputerUseArgs,
@@ -305,6 +405,24 @@ pub struct RunAgentArgs {
/// "claude" delegates to the `claude` CLI.
#[arg(long = "harness", value_name = "HARNESS", default_value_t = Harness::Oz, hide = true)]
pub harness: Harness,
/// Skip the initial LLM turn for this run. Used by the empty-prompt cloud-handoff
/// path so the cloud agent comes up ready for follow-up without hallucinating a
/// response against an empty user message.
///
/// Requires `--idle-on-complete` to also be set: with the initial turn skipped, the
/// driver has nothing to drive a completion event, so the process would exit
/// immediately on success without an idle window for the user's follow-up to arrive.
#[arg(
long = "skip-initial-turn",
hide = true,
requires_all = ["task_id", "idle_on_complete"],
conflicts_with_all = ["prompt", "saved_prompt", "file"]
)]
pub skip_initial_turn: bool,
#[arg(long = "configure-git-credentials-with-github", hide = true, requires_all = ["task_id"])]
pub configure_git_credentials_with_github: bool,
}
impl RunAgentArgs {
@@ -363,8 +481,8 @@ pub struct RunCloudArgs {
///
/// When used with --prompt, the skill provides the base context and the prompt is the task.
///
/// To automate a skill on a schedule, use `oz schedule create --skill <SPEC>`.
#[arg(long = "skill", value_name = "SPEC")]
/// To automate a skill on a schedule, use `oz schedule create --skill <SKILL>`.
#[arg(long = "skill", value_name = "SKILL")]
pub skill: Option<SkillSpec>,
/// Name for this agent task.
@@ -384,6 +502,12 @@ pub struct RunCloudArgs {
/// The environment to run this ambient agent in.
#[command(flatten)]
pub environment: EnvironmentCreateArgs,
/// Runner to use for this agent's compute (docker image, instance size,
/// setup commands), identified by ID. Overrides the environment's default runner.
#[arg(long = "runner", value_name = "ID")]
pub runner: Option<String>,
/// Open the agent's session in Warp once it's available.
#[arg(long = "open")]
pub open: bool,
@@ -395,6 +519,14 @@ pub struct RunCloudArgs {
#[command(flatten)]
pub scope: ObjectScope,
/// UID of the agent to execute this run as.
///
/// This will apply the agent's configuration, such
/// as its skills and base model, and attribute
/// credit usage back to the agent.
#[arg(long = "agent", value_name = "UID")]
pub agent_uid: Option<String>,
/// Where this job should be hosted. Setting "warp" runs it on Warp's infrastructure. Any other
/// value is treated is a self-hosted job and the value will be matched with the self-hosted
/// worker's name.
@@ -435,9 +567,178 @@ pub struct RunCloudArgs {
pub claude_auth_secret: Option<String>,
}
/// Arguments for listing available agents.
/// Sort field for named agents.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum AgentSortByArg {
#[value(name = "name")]
Name,
#[value(name = "created-at")]
CreatedAt,
}
/// Arguments for listing named agents.
#[derive(Debug, Clone, Args)]
pub struct ListAgentConfigsArgs {
pub struct AgentListArgs {
/// Sort field. Only supported for pretty, text, and ndjson output.
#[arg(long = "sort-by", value_enum, value_name = "FIELD")]
pub sort_by: Option<AgentSortByArg>,
/// Sort direction. Only supported for pretty, text, and ndjson output.
#[arg(long = "sort-order", value_enum, value_name = "DIR")]
pub sort_order: Option<SortOrderArg>,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// Arguments for getting a named agent.
#[derive(Debug, Clone, Args)]
pub struct AgentGetArgs {
/// UID of the agent to get.
pub uid: String,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// Arguments for creating a named agent.
#[derive(Debug, Clone, Args)]
pub struct AgentCreateArgs {
/// Name of the agent.
#[arg(long = "name", short = 'n')]
pub name: String,
/// Description of the agent.
#[arg(long = "description")]
pub description: Option<String>,
/// Attach a secret to the agent. Repeat the flag for multiple secrets.
#[arg(long = "secret", value_name = "NAME")]
pub secrets: Vec<String>,
/// Attach a skill to the agent. Repeat the flag for multiple skills.
#[arg(long = "skill", value_name = "SKILL")]
pub skills: Vec<String>,
/// Base model for runs of this agent.
#[arg(long = "base-model", value_name = "MODEL_ID")]
pub base_model: Option<String>,
/// Default cloud environment for runs of this agent.
#[arg(long = "environment", short = 'e', value_name = "ENVIRONMENT_ID")]
pub environment: Option<String>,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// Arguments for updating a named agent.
#[derive(Debug, Clone, Args)]
pub struct AgentUpdateArgs {
/// UID of the agent to update.
pub uid: String,
/// New name for the agent.
#[arg(long = "name", short = 'n')]
pub name: Option<String>,
/// Replacement description for the agent.
#[arg(long = "description", conflicts_with = "remove_description")]
pub description: Option<String>,
/// Remove the agent description.
#[arg(long = "remove-description", conflicts_with = "description")]
pub remove_description: bool,
/// Add a secret to the agent. Repeat the flag for multiple secrets.
#[arg(
long = "add-secret",
value_name = "NAME",
conflicts_with = "remove_all_secrets"
)]
pub add_secrets: Vec<String>,
/// Remove a secret from the agent. Repeat the flag for multiple secrets.
#[arg(
long = "remove-secret",
value_name = "NAME",
conflicts_with = "remove_all_secrets"
)]
pub remove_secrets: Vec<String>,
/// Remove all secrets from the agent.
#[arg(
long = "remove-all-secrets",
conflicts_with_all = ["add_secrets", "remove_secrets"]
)]
pub remove_all_secrets: bool,
/// Add a skill to the agent. Repeat the flag for multiple skills.
#[arg(
long = "add-skill",
value_name = "SKILL",
conflicts_with = "remove_all_skills"
)]
pub add_skills: Vec<String>,
/// Remove a skill from the agent. Repeat the flag for multiple skills.
#[arg(
long = "remove-skill",
value_name = "SKILL",
conflicts_with = "remove_all_skills"
)]
pub remove_skills: Vec<String>,
/// Remove all skills from the agent.
#[arg(
long = "remove-all-skills",
conflicts_with_all = ["add_skills", "remove_skills"]
)]
pub remove_all_skills: bool,
/// Replacement base model for runs executed by this agent.
#[arg(
long = "base-model",
value_name = "MODEL_ID",
conflicts_with = "remove_base_model"
)]
pub base_model: Option<String>,
/// Remove the agent base model.
#[arg(long = "remove-base-model", conflicts_with = "base_model")]
pub remove_base_model: bool,
/// Replacement default cloud environment for runs executed by this agent.
#[arg(
long = "environment",
short = 'e',
value_name = "ENVIRONMENT_ID",
conflicts_with = "remove_environment"
)]
pub environment: Option<String>,
/// Remove the agent default environment.
#[arg(long = "remove-environment", conflicts_with = "environment")]
pub remove_environment: bool,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// Arguments for deleting a named agent.
#[derive(Debug, Clone, Args)]
pub struct AgentDeleteArgs {
/// UID of the agent to delete.
pub uid: String,
}
/// Arguments for listing available agent skills.
#[derive(Debug, Clone, Args)]
pub struct ListAgentSkillsArgs {
/// List skills from a specific GitHub repository.
///
/// Format: `owner/repo` or `https://github.com/owner/repo`
+35
View File
@@ -0,0 +1,35 @@
use super::*;
/// Locks in [`Harness::config_name`] / [`Harness::from_config_name`] as a true inverse pair
/// for every variant that maps to a real, server-recognized harness. If a new variant is
/// added without a matching `from_config_name` arm, this round-trip test will fail.
#[test]
fn harness_config_name_round_trips_for_known_variants() {
for harness in [
Harness::Oz,
Harness::Claude,
Harness::OpenCode,
Harness::Gemini,
Harness::Codex,
] {
assert_eq!(
Harness::from_config_name(harness.config_name()),
Some(harness),
"round-trip failed for {harness:?}",
);
}
}
#[test]
fn harness_from_config_name_returns_none_for_unrecognized() {
assert_eq!(Harness::from_config_name(""), None);
assert_eq!(Harness::from_config_name("not-a-real-harness"), None);
}
#[test]
fn harness_from_config_name_round_trips_unknown() {
assert_eq!(
Harness::from_config_name(Harness::Unknown.config_name()),
Some(Harness::Unknown),
);
}
+111
View File
@@ -0,0 +1,111 @@
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand, ValueEnum};
use crate::SortOrderArg;
use crate::date_time::parse_rfc3339;
use crate::json_filter::JsonOutput;
/// API key-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ApiKeyCommand {
/// List active API keys.
List(ListApiKeysArgs),
/// Create a new API key.
Create(CreateApiKeyArgs),
/// Immediately expire an API key.
#[command(alias = "delete")]
Expire(ExpireApiKeyArgs),
}
impl ApiKeyCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
ApiKeyCommand::List(_) => "api-key list",
ApiKeyCommand::Create(_) => "api-key create",
ApiKeyCommand::Expire(_) => "api-key expire",
}
}
}
#[derive(Debug, Clone, Args)]
pub struct ListApiKeysArgs {
/// Sort field.
#[arg(long = "sort-by", value_enum, value_name = "FIELD")]
pub sort_by: Option<ApiKeySortByArg>,
/// Sort direction.
#[arg(long = "sort-order", value_enum, value_name = "DIR")]
pub sort_order: Option<SortOrderArg>,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
#[derive(Debug, Clone, Args)]
pub struct CreateApiKeyArgs {
/// Name of the API key to create.
pub name: String,
/// UID of the agent to authenticate as.
#[arg(long = "agent", value_name = "UID")]
pub agent_uid: Option<String>,
#[command(flatten)]
pub expiration: ApiKeyExpirationArgs,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// API key expiration arguments. Exactly one expiration decision is required.
#[derive(Debug, Clone, Args)]
#[group(required = true, multiple = false)]
pub struct ApiKeyExpirationArgs {
/// Expire the API key after this duration, such as "30d", "12h", or "90m".
#[arg(long = "expires-in", value_name = "DURATION")]
pub expires_in: Option<humantime::Duration>,
/// Expire the API key at a specific time.
#[arg(long = "expires-at", value_name = "RFC3339", value_parser = parse_rfc3339)]
pub expires_at: Option<DateTime<Utc>>,
/// Create an API key with no expiration.
#[arg(long = "no-expiration")]
pub no_expiration: bool,
}
#[derive(Debug, Clone, Args)]
pub struct ExpireApiKeyArgs {
/// Name or UID of the API key to expire.
#[arg(value_name = "NAME_OR_UID")]
pub key_uid: String,
/// Expire without asking for confirmation.
#[arg(long, default_value_t = false)]
pub force: bool,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// Sort-by values accepted by `--sort-by`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ApiKeySortByArg {
#[value(name = "name")]
Name,
#[value(name = "created-at")]
CreatedAt,
#[value(name = "last-used-at")]
LastUsedAt,
#[value(name = "expires-at")]
ExpiresAt,
#[value(name = "scope")]
Scope,
}
#[cfg(test)]
#[path = "api_key_tests.rs"]
mod tests;
+87
View File
@@ -0,0 +1,87 @@
use clap::Parser;
use super::*;
#[derive(Debug, Parser)]
struct TestApiKey {
#[command(subcommand)]
command: ApiKeyCommand,
}
#[derive(Debug, Parser)]
struct TestCreate {
#[clap(flatten)]
args: CreateApiKeyArgs,
}
fn parse_command(argv: &[&str]) -> ApiKeyCommand {
let mut full = vec!["test"];
full.extend_from_slice(argv);
TestApiKey::try_parse_from(full)
.expect("parse succeeds")
.command
}
fn parse_create(argv: &[&str]) -> CreateApiKeyArgs {
let mut full = vec!["test"];
full.extend_from_slice(argv);
TestCreate::try_parse_from(full)
.expect("parse succeeds")
.args
}
fn parse_create_err(argv: &[&str]) -> clap::Error {
let mut full = vec!["test"];
full.extend_from_slice(argv);
TestCreate::try_parse_from(full).expect_err("parse fails")
}
#[test]
fn create_requires_expiration_decision() {
let err = parse_create_err(&["ci-key"]);
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn create_rejects_multiple_expiration_decisions() {
let err = parse_create_err(&["ci-key", "--expires-in", "30d", "--no-expiration"]);
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn create_accepts_expires_in() {
let args = parse_create(&["ci-key", "--expires-in", "30d", "--agent", "agent-123"]);
assert_eq!(args.name, "ci-key");
assert_eq!(args.agent_uid.as_deref(), Some("agent-123"));
assert!(args.expiration.expires_in.is_some());
assert!(args.expiration.expires_at.is_none());
assert!(!args.expiration.no_expiration);
}
#[test]
fn create_accepts_no_expiration() {
let args = parse_create(&["ci-key", "--no-expiration"]);
assert_eq!(args.name, "ci-key");
assert!(args.expiration.expires_in.is_none());
assert!(args.expiration.expires_at.is_none());
assert!(args.expiration.no_expiration);
}
#[test]
fn create_accepts_rfc3339_expiration() {
let args = parse_create(&["ci-key", "--expires-at", "2026-06-01T12:00:00Z"]);
assert_eq!(args.name, "ci-key");
assert!(args.expiration.expires_in.is_none());
assert!(args.expiration.expires_at.is_some());
assert!(!args.expiration.no_expiration);
}
#[test]
fn delete_is_alias_for_expire() {
let command = parse_command(&["delete", "deploy-key", "--force"]);
let ApiKeyCommand::Expire(args) = command else {
panic!("Expected expire command");
};
assert_eq!(args.key_uid, "deploy-key");
assert!(args.force);
}
+10
View File
@@ -14,6 +14,16 @@ pub enum ArtifactCommand {
Download(DownloadArtifactArgs),
}
impl ArtifactCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
ArtifactCommand::Upload(_) => "artifact upload",
ArtifactCommand::Get(_) => "artifact get",
ArtifactCommand::Download(_) => "artifact download",
}
}
}
#[derive(Debug, Clone, Args)]
#[command(
group(
+1 -1
View File
@@ -1,9 +1,9 @@
use std::io;
use clap_complete::aot::{Shell, generate};
use galaxy_core::channel::ChannelState;
use crate::{Args, binary_name};
use galaxy_core::channel::ChannelState;
/// Generate shell completions for the Warp CLI and write them to stdout.
pub fn generate_to_stdout(shell: Option<Shell>) -> anyhow::Result<()> {
+8
View File
@@ -0,0 +1,8 @@
use chrono::{DateTime, Utc};
/// Parse an RFC 3339 timestamp into a UTC `DateTime`.
pub(crate) fn parse_rfc3339(s: &str) -> Result<DateTime<Utc>, String> {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| format!("invalid RFC 3339 timestamp '{s}': {e}"))
}
+13
View File
@@ -101,6 +101,19 @@ pub enum EnvironmentCommand {
},
}
impl EnvironmentCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
EnvironmentCommand::List => "environment list",
EnvironmentCommand::Image(_) => "environment image",
EnvironmentCommand::Create { .. } => "environment create",
EnvironmentCommand::Delete { .. } => "environment delete",
EnvironmentCommand::Get { .. } => "environment get",
EnvironmentCommand::Update { .. } => "environment update",
}
}
}
/// Common arguments for selecting an environment when creating an integration.
#[derive(Args, Clone, Debug)]
#[group(required = false, multiple = false)]
+9
View File
@@ -15,6 +15,15 @@ pub enum FederateCommand {
IssueGcpToken(IssueGcpTokenArgs),
}
impl FederateCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
FederateCommand::IssueToken(_) => "federate issue-token",
FederateCommand::IssueGcpToken(_) => "federate issue-gcp-token",
}
}
}
#[derive(Debug, Clone, Args)]
#[command(name = "issue-token")]
pub struct IssueTokenArgs {
+28
View File
@@ -14,6 +14,18 @@ pub struct HarnessSupportArgs {
pub command: HarnessSupportCommand,
}
impl HarnessSupportCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
HarnessSupportCommand::Ping => "harness-support ping",
HarnessSupportCommand::ReportArtifact(_) => "harness-support report-artifact",
HarnessSupportCommand::NotifyUser(_) => "harness-support notify-user",
HarnessSupportCommand::FinishTask(_) => "harness-support finish-task",
HarnessSupportCommand::ReportShutdown(_) => "harness-support report-shutdown",
}
}
}
#[derive(Debug, Clone, Subcommand)]
pub enum HarnessSupportCommand {
/// Verify connectivity by fetching and displaying the current run.
@@ -28,6 +40,9 @@ pub enum HarnessSupportCommand {
/// Report task completion or failure, as well as a summary of the task.
FinishTask(FinishTaskArgs),
/// Report that the agent process is shutting down.
ReportShutdown(ReportShutdownArgs),
}
#[derive(Debug, Clone, Args)]
@@ -76,3 +91,16 @@ pub struct FinishTaskArgs {
#[arg(long)]
pub summary: String,
}
#[derive(Debug, Clone, Args)]
pub struct ReportShutdownArgs {
/// Error category for abnormal shutdown (e.g. "oom", "timeout").
/// Omit for clean shutdown.
#[arg(long)]
pub error_category: Option<String>,
/// Human-readable error message for abnormal shutdown.
/// Omit for clean shutdown.
#[arg(long)]
pub error_message: Option<String>,
}
+15 -7
View File
@@ -1,12 +1,10 @@
use clap::{Args, Subcommand};
use crate::{
config_file::ConfigFileArgs,
environment::{EnvironmentCreateArgs, EnvironmentUpdateArgs},
mcp::MCPSpec,
model::ModelArgs,
provider::ProviderType,
};
use crate::config_file::ConfigFileArgs;
use crate::environment::{EnvironmentCreateArgs, EnvironmentUpdateArgs};
use crate::mcp::MCPSpec;
use crate::model::ModelArgs;
use crate::provider::ProviderType;
/// Integration-related subcommands.
#[derive(Debug, Clone, Subcommand)]
@@ -20,6 +18,16 @@ pub enum IntegrationCommand {
List,
}
impl IntegrationCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
IntegrationCommand::Create(_) => "integration create",
IntegrationCommand::Update(_) => "integration update",
IntegrationCommand::List => "integration list",
}
}
}
#[derive(Debug, Clone, Args)]
pub struct CreateIntegrationArgs {
/// Provider to create the integration for.
-1
View File
@@ -5,7 +5,6 @@ use std::ops::Deref;
use std::sync::Arc;
use clap::Args;
use jaq_all::data::{self, DataKind};
use jaq_all::load::FileReportsDisp;
+88 -9
View File
@@ -1,6 +1,7 @@
#![cfg_attr(target_family = "wasm", allow(dead_code))]
use std::{env, fmt, path::Path};
use std::path::Path;
use std::{env, fmt};
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use url::Url;
@@ -16,16 +17,22 @@ mod process_handle;
pub mod artifact;
pub mod scope;
pub mod skill;
mod sort_order;
pub use sort_order::SortOrderArg;
pub mod agent;
pub mod api_key;
pub mod completions;
pub mod config_file;
mod date_time;
pub mod environment;
pub mod federate;
pub mod harness_support;
pub mod integration;
pub mod json_filter;
pub mod local_control;
pub mod mcp;
pub mod memory_store;
pub mod model;
pub mod provider;
pub mod schedule;
@@ -61,6 +68,17 @@ pub struct ParentOpts {
pub handle: Option<process_handle::ProcessHandle>,
}
/// Returns whether an argument requests one of Warp's hidden worker modes.
pub fn is_worker_invocation(arg: &str) -> bool {
let command = WorkerCommand::augment_subcommands(clap::Command::new("worker"));
command.find_subcommand(arg).is_some()
|| arg.strip_prefix("--").is_some_and(|long_flag| {
command
.get_subcommands()
.any(|subcommand| subcommand.get_long_flag() == Some(long_flag))
})
}
/// Hidden worker args used to scope remote-server proxy/daemon sockets by
/// Warp identity without exposing credentials.
#[derive(Debug, Clone, Default, clap::Args)]
@@ -88,7 +106,11 @@ pub struct GlobalOptions {
pub output_format: OutputFormat,
}
/// Command-line argument parser for the main Warp binary. This is used across all channels.
/// Normal argument parser for the shared Warp executable across all channels.
///
/// Oz commands are subcommands of this parser, so invoking an `oz` symlink does
/// not require a mode flag. Warp Control uses its separate [`local_control::ControlArgs`]
/// parser, selected before this parser sees the arguments.
#[derive(Debug, Default, Parser, Clone)]
#[command(
name = "oz",
@@ -243,6 +265,15 @@ impl Args {
}
}
if !FeatureFlag::APIKeyManagement.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "api-key" {
eprintln!("error: unrecognized subcommand 'api-key'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
let command = Self::clap_command();
command.try_get_matches()
@@ -337,18 +368,21 @@ impl Args {
})
});
}
// Hide the message subcommand from help text.
if !FeatureFlag::OrchestrationV2.is_enabled() {
command = command.mut_subcommand("run", |run_cmd| {
run_cmd.mut_subcommand("message", |c| c.hide(true))
});
}
// Hide the artifact subcommand from help text.
if !FeatureFlag::ArtifactCommand.is_enabled() {
command = command.mut_subcommand("artifact", |c| c.hide(true));
}
// Hide the api-key subcommand from help text.
if !FeatureFlag::APIKeyManagement.is_enabled() {
command = command.mut_subcommand("api-key", |c| c.hide(true));
}
// Wire up `--version` / `-V` using the same version metadata used elsewhere in the
// app, so the CLI reports the build's release tag.
command = command.version(version_string());
// Substitute the actual binary name into help output. Ideally clap would do this for us.
let bin_name =
binary_name().unwrap_or_else(|| ChannelState::channel().cli_command_name().to_string());
@@ -495,6 +529,12 @@ pub enum CliCommand {
/// Manage available models.
#[command(subcommand)]
Model(crate::model::ModelCommand),
/// Manage memory stores.
#[command(subcommand, alias = "memory-stores")]
MemoryStore(crate::memory_store::MemoryStoreCommand),
/// Manage memories.
#[command(subcommand)]
Memory(crate::memory_store::MemoryCommand),
/// Log in to Warp.
Login,
@@ -531,6 +571,36 @@ pub enum CliCommand {
/// Manage artifacts.
#[command(subcommand)]
Artifact(crate::artifact::ArtifactCommand),
/// Manage API keys.
#[command(subcommand)]
ApiKey(crate::api_key::ApiKeyCommand),
}
impl CliCommand {
/// Returns the command path used to identify this invocation in tracing.
pub fn as_str_for_tracing(&self) -> &'static str {
match self {
CliCommand::Agent(command) => command.as_str_for_tracing(),
CliCommand::Environment(command) => command.as_str_for_tracing(),
CliCommand::MCP(command) => command.as_str_for_tracing(),
CliCommand::Run(command) => command.as_str_for_tracing(),
CliCommand::Model(command) => command.as_str_for_tracing(),
CliCommand::Login => "login",
CliCommand::Logout => "logout",
CliCommand::Whoami => "whoami",
CliCommand::Provider(command) => command.as_str_for_tracing(),
CliCommand::Integration(command) => command.as_str_for_tracing(),
CliCommand::Schedule(command) => command.as_str_for_tracing(),
CliCommand::Secret(command) => command.as_str_for_tracing(),
CliCommand::Federate(command) => command.as_str_for_tracing(),
CliCommand::HarnessSupport(args) => args.command.as_str_for_tracing(),
CliCommand::Artifact(command) => command.as_str_for_tracing(),
CliCommand::ApiKey(command) => command.as_str_for_tracing(),
CliCommand::MemoryStore(command) => command.as_str_for_tracing(),
CliCommand::Memory(command) => command.as_str_for_tracing(),
}
}
}
/// A subcommand of the main Warp application. This includes all [`WorkerCommand`]s as well as app-specific debugging tools.
@@ -599,7 +669,7 @@ pub struct TerminalServerArgs {
#[derive(Debug, Copy, Clone, clap::ValueEnum)]
pub enum RecoveryMechanism {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
#[value(name = "force-x11")]
X11,
#[value(name = "force-dedicated-gpu")]
@@ -688,6 +758,15 @@ pub fn binary_name() -> Option<String> {
Path::new(&arg0).file_name()?.to_str().map(|s| s.to_owned())
}
/// The version string shown for `--version` / `-V`.
///
/// Sourced from [`ChannelState::app_version`], which is populated from the
/// `GIT_RELEASE_TAG` env var at compile time. Falls back to a placeholder for
/// untagged builds (e.g. local `cargo run`).
pub fn version_string() -> &'static str {
ChannelState::app_version().unwrap_or("<unknown>")
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;
+602 -27
View File
@@ -1,15 +1,28 @@
use super::*;
use clap::Parser;
use std::ffi::OsString;
use clap::Parser;
use super::*;
use crate::agent::{AgentCommand, Harness, OutputFormat};
use crate::artifact::ArtifactCommand;
use crate::environment::{EnvironmentCommand, ImageCommand};
use crate::harness_support::{HarnessSupportCommand, TaskStatus};
use crate::integration::IntegrationCommand;
use crate::memory_store::{MemoryCommand, MemoryStoreCommand};
use crate::schedule::ScheduleSubcommand;
use crate::secret::{CodexMethod, CreateProvider, SecretCommand};
use crate::task::{MessageCommand, TaskCommand};
#[test]
fn identifies_worker_subcommands() {
assert!(is_worker_invocation("minidump-server"));
#[cfg(unix)]
assert!(is_worker_invocation(&terminal_server_subcommand()));
#[cfg(feature = "plugin_host")]
assert!(is_worker_invocation("--plugin-host"));
assert!(!is_worker_invocation("--prompt"));
}
fn set_env_var(name: &str, value: &str) -> Option<OsString> {
let previous = std::env::var_os(name);
// Safety: tests that mutate process environment are marked `serial` so we
@@ -56,6 +69,8 @@ fn agent_run_accepts_hidden_bedrock_inference_role_flag() {
"hello",
"--bedrock-inference-role",
"arn:aws:iam::123456789012:role/test",
"--bedrock-role-region",
"us-east-1",
])
.unwrap();
@@ -70,6 +85,43 @@ fn agent_run_accepts_hidden_bedrock_inference_role_flag() {
run_args.bedrock_inference_role.as_deref(),
Some("arn:aws:iam::123456789012:role/test")
);
assert_eq!(run_args.bedrock_role_region.as_deref(), Some("us-east-1"));
}
#[test]
fn agent_run_rejects_bedrock_inference_role_without_region() {
let err = Args::try_parse_from([
"warp",
"agent",
"run",
"--prompt",
"hello",
"--bedrock-inference-role",
"arn:aws:iam::123456789012:role/test",
])
.expect_err("--bedrock-inference-role must require --bedrock-role-region");
assert!(
err.to_string().contains("--bedrock-role-region"),
"expected error to reference --bedrock-role-region, got: {err}"
);
}
#[test]
fn agent_run_rejects_bedrock_role_region_without_role() {
let err = Args::try_parse_from([
"warp",
"agent",
"run",
"--prompt",
"hello",
"--bedrock-role-region",
"us-east-1",
])
.expect_err("--bedrock-role-region must require --bedrock-inference-role");
assert!(
err.to_string().contains("--bedrock-inference-role"),
"expected error to reference --bedrock-inference-role, got: {err}"
);
}
#[test]
@@ -86,6 +138,246 @@ fn model_list_parses() {
assert!(matches!(model_cmd, crate::model::ModelCommand::List));
}
#[test]
fn memory_store_list_parses() {
let args = Args::try_parse_from(["warp", "memory-store", "list"]).unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory-store list` command");
};
let CliCommand::MemoryStore(memory_store_cmd) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory-store` command");
};
assert!(matches!(memory_store_cmd, MemoryStoreCommand::List));
}
#[test]
fn memory_stores_alias_parses() {
let args = Args::try_parse_from(["warp", "memory-stores", "list"]).unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory-stores list` command");
};
let CliCommand::MemoryStore(memory_store_cmd) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory-stores` alias to parse as memory-store command");
};
assert!(matches!(memory_store_cmd, MemoryStoreCommand::List));
}
#[test]
fn memory_list_parses() {
let args = Args::try_parse_from(["warp", "memory", "list", "store-123"]).unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory list` command");
};
let CliCommand::Memory(MemoryCommand::List(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory list` command");
};
assert_eq!(args.store_uid, "store-123");
}
#[test]
fn memory_store_get_parses() {
let args = Args::try_parse_from(["warp", "memory-store", "get", "store-123"]).unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory-store get` command");
};
let CliCommand::MemoryStore(MemoryStoreCommand::Get(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory-store get` command");
};
assert_eq!(args.store_uid, "store-123");
}
#[test]
fn memory_store_get_store_alias_parses() {
let args = Args::try_parse_from(["warp", "memory-store", "get-store", "store-123"]).unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory-store get-store` command");
};
let CliCommand::MemoryStore(MemoryStoreCommand::Get(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory-store get-store` alias to parse as get command");
};
assert_eq!(args.store_uid, "store-123");
}
#[test]
fn memory_store_update_parses() {
let args = Args::try_parse_from([
"warp",
"memory-store",
"update",
"store-123",
"--description",
"team memory store",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory-store update` command");
};
let CliCommand::MemoryStore(MemoryStoreCommand::Update(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory-store update` command");
};
assert_eq!(args.store_uid, "store-123");
assert_eq!(args.description.as_deref(), Some("team memory store"));
}
#[test]
fn memory_store_update_store_alias_parses() {
let args = Args::try_parse_from([
"warp",
"memory-store",
"update-store",
"store-123",
"--description",
"team memory store",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory-store update-store` command");
};
let CliCommand::MemoryStore(MemoryStoreCommand::Update(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory-store update-store` alias to parse as update command");
};
assert_eq!(args.store_uid, "store-123");
assert_eq!(args.description.as_deref(), Some("team memory store"));
}
#[test]
fn memory_create_parses() {
let args = Args::try_parse_from([
"warp",
"memory",
"create",
"store-123",
"--content",
"remember this",
"--reason",
"manual note",
"--version",
"v1",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory create` command");
};
let CliCommand::Memory(MemoryCommand::Create(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory create` command");
};
assert_eq!(args.store_uid, "store-123");
assert_eq!(args.content, "remember this");
assert_eq!(args.reason, "manual note");
assert_eq!(args.version.as_deref(), Some("v1"));
}
#[test]
fn memory_update_parses() {
let args = Args::try_parse_from([
"warp",
"memory",
"update",
"memory-123",
"--store",
"store-123",
"--content",
"updated memory",
"--reason",
"manual edit",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory update` command");
};
let CliCommand::Memory(MemoryCommand::Update(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory update` command");
};
assert_eq!(args.memory_uid, "memory-123");
assert_eq!(args.store_uid, "store-123");
assert_eq!(args.content, "updated memory");
assert_eq!(args.reason, "manual edit");
}
#[test]
fn memory_delete_parses() {
let args = Args::try_parse_from([
"warp",
"memory",
"delete",
"memory-123",
"--store",
"store-123",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory delete` command");
};
let CliCommand::Memory(MemoryCommand::Delete(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory delete` command");
};
assert_eq!(args.memory_uid, "memory-123");
assert_eq!(args.store_uid, "store-123");
}
#[test]
fn memory_versions_parses() {
let args = Args::try_parse_from([
"warp",
"memory",
"versions",
"memory-123",
"--store",
"store-123",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp memory versions` command");
};
let CliCommand::Memory(MemoryCommand::Versions(args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp memory versions` command");
};
assert_eq!(args.memory_uid, "memory-123");
assert_eq!(args.store_uid, "store-123");
}
#[test]
fn legacy_memory_store_memory_commands_are_rejected() {
for command in [
"list-memories",
"memories",
"create-memory",
"add-memory",
"update-memory",
"edit-memory",
"delete-memory",
"remove-memory",
"list-versions",
"versions",
] {
let err = Args::try_parse_from(["warp", "memory-store", command, "memory-123"])
.expect_err("legacy memory-store memory command should not parse");
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidSubcommand);
}
}
#[test]
fn login_parses() {
let args = Args::try_parse_from(["warp", "login"]).unwrap();
@@ -189,6 +481,72 @@ fn agent_run_accepts_idle_on_complete_duration() {
);
}
#[test]
fn agent_run_accepts_skip_initial_turn_with_task_id_and_idle_on_complete() {
let args = Args::try_parse_from([
"warp",
"agent",
"run",
"--task-id",
"abc",
"--skip-initial-turn",
"--idle-on-complete",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp agent run` command");
};
let CliCommand::Agent(AgentCommand::Run(run_args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp agent run` command");
};
assert_eq!(run_args.task_id.as_deref(), Some("abc"));
assert!(run_args.skip_initial_turn);
assert!(run_args.idle_on_complete.is_some());
}
#[test]
fn agent_run_rejects_skip_initial_turn_without_idle_on_complete() {
// Without `--idle-on-complete`, the driver would exit immediately on Success
// before any follow-up could arrive, defeating the purpose of skip. Pinned at
// the CLI layer so the invariant fails loudly at parse time instead of at
// runtime.
let result = Args::try_parse_from([
"warp",
"agent",
"run",
"--task-id",
"abc",
"--skip-initial-turn",
]);
assert!(
result.is_err(),
"--skip-initial-turn without --idle-on-complete should fail to parse"
);
}
#[test]
fn agent_run_rejects_skip_initial_turn_without_task_id() {
// `--skip-initial-turn` is only meaningful on the server-side prompt path,
// which requires `--task-id`.
let result = Args::try_parse_from([
"warp",
"agent",
"run",
"--prompt",
"hello",
"--skip-initial-turn",
"--idle-on-complete",
]);
assert!(
result.is_err(),
"--skip-initial-turn without --task-id should fail to parse"
);
}
#[test]
fn agent_run_accepts_snapshot_flags() {
let args = Args::try_parse_from([
@@ -275,6 +633,29 @@ fn agent_run_cloud_accepts_model() {
assert_eq!(run_args.model.model.as_deref(), Some("gpt-4o"));
}
#[test]
fn agent_run_cloud_accepts_agent_flag() {
let args = Args::try_parse_from([
"warp",
"agent",
"run-cloud",
"--prompt",
"hello",
"--agent",
"agent_123",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp agent run-cloud` command");
};
let CliCommand::Agent(AgentCommand::RunCloud(run_args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp agent run-cloud` command");
};
assert_eq!(run_args.agent_uid.as_deref(), Some("agent_123"));
}
#[test]
fn agent_run_cloud_accepts_mcp() {
let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
@@ -316,6 +697,36 @@ fn agent_run_cloud_accepts_run_ambient_alias() {
};
}
#[test]
fn agent_update_rejects_conflicting_remove_flags() {
let result = Args::try_parse_from([
"warp",
"agent",
"update",
"agent_123",
"--description",
"new",
"--remove-description",
]);
assert!(result.is_err());
}
#[test]
fn agent_update_rejects_remove_all_secret_deltas() {
let result = Args::try_parse_from([
"warp",
"agent",
"update",
"agent_123",
"--add-secret",
"GITHUB_TOKEN",
"--remove-all-secrets",
]);
assert!(result.is_err());
}
#[test]
fn agent_run_rejects_prompt_and_task_id() {
let result = Args::try_parse_from([
@@ -629,31 +1040,6 @@ fn artifact_help_hides_upload_but_keeps_download_visible() {
assert!(!visible_subcommands.contains(&"upload"));
}
#[test]
fn run_help_hides_message_when_orchestration_v2_disabled() {
galaxy_core::features::mark_initialized();
let mut command = Args::clap_command();
command.build();
let run = command
.find_subcommand("run")
.expect("run subcommand should exist");
let message = run
.find_subcommand("message")
.expect("message subcommand should exist");
assert!(message.is_hide_set());
let visible_subcommands: Vec<_> = run
.get_subcommands()
.filter(|subcommand| !subcommand.is_hide_set())
.map(|subcommand| subcommand.get_name())
.collect();
assert!(!visible_subcommands.contains(&"message"));
}
#[test]
fn raw_command_keeps_message_visible_before_runtime_help_customization() {
let mut command = <Args as clap::CommandFactory>::command();
@@ -1401,6 +1787,30 @@ fn agent_run_cloud_accepts_snapshot_flags() {
);
}
#[test]
fn agent_run_accepts_task_id_with_conversation_for_worker_followups() {
let args = Args::try_parse_from([
"warp",
"agent",
"run",
"--task-id",
"task-123",
"--conversation",
"conv-123",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp agent run` command");
};
let CliCommand::Agent(AgentCommand::Run(run_args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp agent run` command");
};
assert_eq!(run_args.task_id.as_deref(), Some("task-123"));
assert_eq!(run_args.conversation.as_deref(), Some("conv-123"));
}
#[test]
fn agent_run_cloud_accepts_computer_use_flag() {
let args = Args::try_parse_from([
@@ -1538,6 +1948,22 @@ fn harness_parse_local_child_harness_rejects_oz() {
);
}
#[test]
fn harness_parse_orchestration_harness_accepts_codex() {
assert_eq!(
Harness::parse_orchestration_harness("codex"),
Some(Harness::Codex)
);
}
#[test]
fn harness_parse_local_child_harness_accepts_codex() {
assert_eq!(
Harness::parse_local_child_harness("codex"),
Some(Harness::Codex)
);
}
#[test]
fn agent_run_cloud_accepts_claude_auth_secret_with_harness() {
let args = Args::try_parse_from([
@@ -1854,3 +2280,152 @@ fn finish_task_rejects_missing_status() {
]);
assert!(result.is_err());
}
#[test]
fn report_shutdown_clean_parses() {
let args = Args::try_parse_from([
"warp",
"harness-support",
"--run-id",
"run-1",
"report-shutdown",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected harness-support command");
};
let CliCommand::HarnessSupport(hs_args) = boxed_cmd.as_ref() else {
panic!("Expected harness-support command");
};
let HarnessSupportCommand::ReportShutdown(shutdown_args) = &hs_args.command else {
panic!("Expected report-shutdown subcommand");
};
assert!(shutdown_args.error_category.is_none());
assert!(shutdown_args.error_message.is_none());
}
#[test]
fn secret_create_codex_api_key_parses_minimal() {
galaxy_core::features::mark_initialized();
let args = Args::try_parse_from([
"warp",
"secret",
"create",
"codex",
"api-key",
"my-openai-key",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp secret create codex api-key` command");
};
let CliCommand::Secret(SecretCommand::Create(create_args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp secret create` command");
};
let Some(CreateProvider::Codex(codex)) = &create_args.provider else {
panic!("Expected `codex` provider subcommand");
};
let CodexMethod::ApiKey(api_key_args) = &codex.method;
assert_eq!(api_key_args.common.name, "my-openai-key");
assert!(api_key_args.common.description.is_none());
assert!(api_key_args.value.value_file.is_none());
assert!(api_key_args.base_url.is_none());
}
#[test]
fn secret_create_codex_api_key_accepts_base_url_and_value_file() {
galaxy_core::features::mark_initialized();
let args = Args::try_parse_from([
"warp",
"secret",
"create",
"codex",
"api-key",
"my-openai-key",
"--value-file",
"key.txt",
"--base-url",
"https://us.api.openai.com/v1",
"--description",
"OpenAI key for Codex",
"--team",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected `warp secret create codex api-key` command");
};
let CliCommand::Secret(SecretCommand::Create(create_args)) = boxed_cmd.as_ref() else {
panic!("Expected `warp secret create` command");
};
let Some(CreateProvider::Codex(codex)) = &create_args.provider else {
panic!("Expected `codex` provider subcommand");
};
let CodexMethod::ApiKey(api_key_args) = &codex.method;
assert_eq!(api_key_args.common.name, "my-openai-key");
assert_eq!(
api_key_args.common.description.as_deref(),
Some("OpenAI key for Codex")
);
assert!(api_key_args.common.scope.team);
assert!(!api_key_args.common.scope.personal);
assert_eq!(
api_key_args
.value
.value_file
.as_ref()
.and_then(|p| p.to_str()),
Some("key.txt")
);
assert_eq!(
api_key_args.base_url.as_deref(),
Some("https://us.api.openai.com/v1")
);
}
#[test]
fn secret_create_codex_api_key_requires_name() {
galaxy_core::features::mark_initialized();
let result = Args::try_parse_from(["warp", "secret", "create", "codex", "api-key"]);
assert!(result.is_err());
}
#[test]
fn report_shutdown_abnormal_parses() {
let args = Args::try_parse_from([
"warp",
"harness-support",
"--run-id",
"run-1",
"report-shutdown",
"--error-category",
"oom",
"--error-message",
"out of memory",
])
.unwrap();
let Some(Command::CommandLine(boxed_cmd)) = args.command else {
panic!("Expected harness-support command");
};
let CliCommand::HarnessSupport(hs_args) = boxed_cmd.as_ref() else {
panic!("Expected harness-support command");
};
let HarnessSupportCommand::ReportShutdown(shutdown_args) = &hs_args.command else {
panic!("Expected report-shutdown subcommand");
};
assert_eq!(shutdown_args.error_category.as_deref(), Some("oom"));
assert_eq!(
shutdown_args.error_message.as_deref(),
Some("out of memory")
);
}
@@ -0,0 +1,802 @@
//! Implementations for user-facing `warpctrl` command groups.
use local_control::discovery::InstanceRecord;
use local_control::protocol::{
Action, ActionKind, ActionNameParams, BindingNameParams, BooleanValueParams, ColorValueParams,
ControlError, DirectionParams, EmptyParams, ErrorCode, FileOpenParams, KeyParams,
KeyValueParams, PageQueryParams, QueryParams, RenameParams, RequestEnvelope, ResizeParams,
SettingListParams, TabActivateParams, TabActivationMode, TabCloseMode, TabCloseParams,
TabCreateParams, TextParams, ThemeNameParams,
};
use local_control::selection::select_instance;
use serde::Serialize;
use galaxy_core::channel::ChannelState;
use crate::agent::OutputFormat;
use crate::local_control::output::{write_json, write_json_line};
use crate::local_control::selectors::{instance_selector, target_selector};
use crate::local_control::{
ActionCatalogCommand, AppCommand, AppearanceCommand, CapabilityCommand, FileCommand,
InputCommand, InstanceCommand, KeybindingCommand, PaneCommand, SessionCommand, SettingCommand,
SurfaceCommand, SurfaceOpenCommand, SurfaceOpenToggleCommand, SurfaceQueryCommand,
SurfaceSettingsCommand, SurfaceToggleCommand, TabActivateArgs, TabCloseArgs, TabColorCommand,
TabCommand, TargetArgs, ThemeCommand, WindowCommand,
};
pub(super) fn run_surface_command(
command: SurfaceCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
SurfaceCommand::List(args) => {
run_action_with_params(args, ActionKind::SurfaceList, EmptyParams {}, output_format)
}
SurfaceCommand::Settings(command) => match command {
SurfaceSettingsCommand::Open(args) => run_action_with_params(
args.target,
ActionKind::SurfaceSettingsOpen,
PageQueryParams {
page: args.page,
query: args.query,
},
output_format,
),
},
SurfaceCommand::CommandPalette(command) => run_surface_query_command(
command,
ActionKind::SurfaceCommandPaletteOpen,
output_format,
),
SurfaceCommand::CommandSearch(command) => {
run_surface_query_command(command, ActionKind::SurfaceCommandSearchOpen, output_format)
}
SurfaceCommand::ThemePicker(command) => {
run_surface_open_command(command, ActionKind::SurfaceThemePickerOpen, output_format)
}
SurfaceCommand::Keybindings(command) => {
run_surface_open_command(command, ActionKind::SurfaceKeybindingsOpen, output_format)
}
SurfaceCommand::WarpDrive(command) => match command {
SurfaceOpenToggleCommand::Open(args) => run_action_with_params(
args,
ActionKind::SurfaceWarpDriveOpen,
EmptyParams {},
output_format,
),
SurfaceOpenToggleCommand::Toggle(args) => run_action_with_params(
args,
ActionKind::SurfaceWarpDriveToggle,
EmptyParams {},
output_format,
),
},
SurfaceCommand::ResourceCenter(command) => run_surface_toggle_command(
command,
ActionKind::SurfaceResourceCenterToggle,
output_format,
),
SurfaceCommand::AiAssistant(command) => {
run_surface_toggle_command(command, ActionKind::SurfaceAiAssistantToggle, output_format)
}
SurfaceCommand::CodeReview(command) => match command {
SurfaceOpenToggleCommand::Open(args) => run_action_with_params(
args,
ActionKind::SurfaceCodeReviewOpen,
EmptyParams {},
output_format,
),
SurfaceOpenToggleCommand::Toggle(args) => run_action_with_params(
args,
ActionKind::SurfaceCodeReviewToggle,
EmptyParams {},
output_format,
),
},
SurfaceCommand::ProjectExplorer(command) => run_surface_open_command(
command,
ActionKind::SurfaceProjectExplorerOpen,
output_format,
),
SurfaceCommand::GlobalSearch(command) => {
run_surface_open_command(command, ActionKind::SurfaceGlobalSearchOpen, output_format)
}
SurfaceCommand::ConversationList(command) => run_surface_open_command(
command,
ActionKind::SurfaceConversationListOpen,
output_format,
),
SurfaceCommand::LeftPanel(command) => {
run_surface_toggle_command(command, ActionKind::SurfaceLeftPanelToggle, output_format)
}
SurfaceCommand::RightPanel(command) => {
run_surface_toggle_command(command, ActionKind::SurfaceRightPanelToggle, output_format)
}
SurfaceCommand::VerticalTabs(command) => match command {
SurfaceOpenToggleCommand::Open(args) => run_action_with_params(
args,
ActionKind::SurfaceVerticalTabsOpen,
EmptyParams {},
output_format,
),
SurfaceOpenToggleCommand::Toggle(args) => run_action_with_params(
args,
ActionKind::SurfaceVerticalTabsToggle,
EmptyParams {},
output_format,
),
},
SurfaceCommand::AgentManagement(command) => run_surface_open_command(
command,
ActionKind::SurfaceAgentManagementOpen,
output_format,
),
}
}
fn render_human_readable(action: ActionKind, data: &serde_json::Value) -> String {
match action {
ActionKind::AppPing => format!(
"Warp instance {} is reachable (protocol version {})",
value_or_unknown(data, "instance_id"),
value_or_unknown(data, "protocol_version")
),
ActionKind::AppVersion => format!(
"Warp instance {}\nchannel: {}\napp_id: {}\nprotocol_version: {}",
value_or_unknown(data, "instance_id"),
value_or_unknown(data, "channel"),
value_or_unknown(data, "app_id"),
value_or_unknown(data, "protocol_version")
),
ActionKind::TabCreate => format!(
"Created tab {} in window {} (active index {}, tab count {})",
nested_value_or_unknown(data, &["tab", "id"]),
nested_value_or_unknown(data, &["window", "id"]),
nested_value_or_unknown(data, &["tab", "active_index"]),
nested_value_or_unknown(data, &["tab", "count"])
),
ActionKind::PaneSplit => format!(
"Split created pane {}",
nested_value_or_unknown(data, &["pane", "id"])
),
_ => serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string()),
}
}
fn value_or_unknown(data: &serde_json::Value, key: &str) -> String {
nested_value_or_unknown(data, &[key])
}
fn nested_value_or_unknown(data: &serde_json::Value, path: &[&str]) -> String {
let value = path
.iter()
.try_fold(data, |value, key| value.get(*key))
.unwrap_or(&serde_json::Value::Null);
match value {
serde_json::Value::String(value) => value.clone(),
serde_json::Value::Null => "<unknown>".to_owned(),
value => value.to_string(),
}
}
#[cfg(test)]
pub(crate) fn render_human_readable_for_test(
action: ActionKind,
data: &serde_json::Value,
) -> String {
render_human_readable(action, data)
}
pub(super) fn run_instance_command(
command: InstanceCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
InstanceCommand::List => render_instance_list(
local_control::discovery::list_instances(&ChannelState::channel().to_string()),
output_format,
),
InstanceCommand::Inspect(args) => run_action_with_params(
args,
ActionKind::InstanceInspect,
EmptyParams {},
output_format,
),
}
}
/// JSON payload for `warpctrl instance list`.
#[derive(Serialize)]
pub(super) struct InstanceListOutput {
instances: Vec<InstanceSummary>,
}
#[derive(Serialize)]
struct InstanceSummary {
instance_id: String,
pid: u32,
channel: String,
app_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
app_version: Option<String>,
protocol_version: u32,
}
/// Builds the list payload from probed discovery records.
pub(super) fn instance_list_output(records: Vec<InstanceRecord>) -> InstanceListOutput {
InstanceListOutput {
instances: records
.into_iter()
.map(|record| InstanceSummary {
instance_id: record.instance_id.0,
pid: record.pid,
channel: record.channel,
app_id: record.app_id,
app_version: record.app_version,
protocol_version: record.protocol_version,
})
.collect(),
}
}
/// Lists every reachable instance without selecting one. Zero reachable
/// instances is a successful empty list, never an error.
fn render_instance_list(
records: Vec<InstanceRecord>,
output_format: OutputFormat,
) -> Result<(), ControlError> {
let output = instance_list_output(records);
match output_format {
OutputFormat::Json => write_json(&output),
OutputFormat::Ndjson => write_json_line(&output),
OutputFormat::Pretty | OutputFormat::Text => {
if output.instances.is_empty() {
println!("No running Warp instances with local control were found.");
return Ok(());
}
for instance in &output.instances {
println!(
"{} (pid {}, channel {}, app {}, protocol {})",
instance.instance_id,
instance.pid,
instance.channel,
instance.app_id,
instance.protocol_version
);
}
Ok(())
}
}
}
pub(super) fn run_app_command(
command: AppCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
AppCommand::Ping(args) => run_action(args, ActionKind::AppPing, output_format),
AppCommand::Version(args) => run_action(args, ActionKind::AppVersion, output_format),
AppCommand::Active(args) => {
run_action_with_params(args, ActionKind::AppActive, EmptyParams {}, output_format)
}
AppCommand::Focus(args) => run_action(args, ActionKind::AppFocus, output_format),
}
}
pub(super) fn run_action_catalog_command(
command: ActionCatalogCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
ActionCatalogCommand::List => run_action_with_params(
TargetArgs::default(),
ActionKind::ActionList,
EmptyParams {},
output_format,
),
ActionCatalogCommand::Inspect { action } => run_action_with_params(
TargetArgs::default(),
ActionKind::ActionInspect,
ActionNameParams { action },
output_format,
),
}
}
pub(super) fn run_capability_command(
command: CapabilityCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
CapabilityCommand::List => run_action_with_params(
TargetArgs::default(),
ActionKind::CapabilityList,
EmptyParams {},
output_format,
),
CapabilityCommand::Inspect { action } => run_action_with_params(
TargetArgs::default(),
ActionKind::CapabilityInspect,
ActionNameParams { action },
output_format,
),
}
}
pub(super) fn run_window_command(
command: WindowCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
WindowCommand::List(args) => {
run_action_with_params(args, ActionKind::WindowList, EmptyParams {}, output_format)
}
WindowCommand::Inspect(args) => run_action_with_params(
args,
ActionKind::WindowInspect,
EmptyParams {},
output_format,
),
WindowCommand::Create(args) => run_action_with_params(
args.target,
ActionKind::WindowCreate,
TabCreateParams {
tab_type: args.tab_type.map(Into::into),
shell: args.shell,
},
output_format,
),
WindowCommand::Focus(args) => {
run_action_with_params(args, ActionKind::WindowFocus, EmptyParams {}, output_format)
}
WindowCommand::Close(args) => {
run_action_with_params(args, ActionKind::WindowClose, EmptyParams {}, output_format)
}
}
}
pub(super) fn run_tab_command(
command: TabCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
TabCommand::List(args) => {
run_action_with_params(args, ActionKind::TabList, EmptyParams {}, output_format)
}
TabCommand::Inspect(args) => {
run_action_with_params(args, ActionKind::TabInspect, EmptyParams {}, output_format)
}
TabCommand::Create(args) => run_action_with_params(
args.target,
ActionKind::TabCreate,
TabCreateParams {
tab_type: args.tab_type.map(Into::into),
shell: args.shell,
},
output_format,
),
TabCommand::Activate(args) => {
let mode = tab_activation_mode(&args);
run_action_with_params(
args.target,
ActionKind::TabActivate,
TabActivateParams { mode },
output_format,
)
}
TabCommand::Move(args) => run_action_with_params(
args.target,
ActionKind::TabMove,
DirectionParams {
direction: args.direction.into(),
},
output_format,
),
TabCommand::Close(args) => {
let mode = tab_close_mode(&args);
run_action_with_params(
args.target,
ActionKind::TabClose,
TabCloseParams { mode },
output_format,
)
}
TabCommand::Rename(args) => run_action_with_params(
args.target,
ActionKind::TabRename,
RenameParams { title: args.title },
output_format,
),
TabCommand::ResetName(args) => run_action(args, ActionKind::TabResetName, output_format),
TabCommand::Color(command) => match command {
TabColorCommand::Set(args) => run_action_with_params(
args.target,
ActionKind::TabColorSet,
ColorValueParams { color: args.color },
output_format,
),
TabColorCommand::Clear(args) => {
run_action(args, ActionKind::TabColorClear, output_format)
}
},
}
}
pub(super) fn run_pane_command(
command: PaneCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
PaneCommand::List(args) => {
run_action_with_params(args, ActionKind::PaneList, EmptyParams {}, output_format)
}
PaneCommand::Inspect(args) => {
run_action_with_params(args, ActionKind::PaneInspect, EmptyParams {}, output_format)
}
PaneCommand::Split(args) => run_action_with_params(
args.target,
ActionKind::PaneSplit,
DirectionParams {
direction: args.direction.into(),
},
output_format,
),
PaneCommand::Focus(args) => {
run_action_with_params(args, ActionKind::PaneFocus, EmptyParams {}, output_format)
}
PaneCommand::Navigate(args) => run_action_with_params(
args.target,
ActionKind::PaneNavigate,
DirectionParams {
direction: args.direction.into(),
},
output_format,
),
PaneCommand::Resize(args) => run_action_with_params(
args.target,
ActionKind::PaneResize,
ResizeParams {
direction: args.direction.into(),
amount: args.amount,
},
output_format,
),
PaneCommand::Maximize(args) => run_action_with_params(
args,
ActionKind::PaneMaximize,
EmptyParams {},
output_format,
),
PaneCommand::Unmaximize(args) => run_action_with_params(
args,
ActionKind::PaneUnmaximize,
EmptyParams {},
output_format,
),
PaneCommand::Close(args) => {
run_action_with_params(args, ActionKind::PaneClose, EmptyParams {}, output_format)
}
PaneCommand::Rename(args) => run_action_with_params(
args.target,
ActionKind::PaneRename,
RenameParams { title: args.title },
output_format,
),
PaneCommand::ResetName(args) => run_action(args, ActionKind::PaneResetName, output_format),
}
}
pub(super) fn run_session_command(
command: SessionCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
SessionCommand::List(args) => {
run_action_with_params(args, ActionKind::SessionList, EmptyParams {}, output_format)
}
SessionCommand::Inspect(args) => run_action_with_params(
args,
ActionKind::SessionInspect,
EmptyParams {},
output_format,
),
SessionCommand::Activate(args) => run_action_with_params(
args,
ActionKind::SessionActivate,
EmptyParams {},
output_format,
),
SessionCommand::Previous(args) => run_action_with_params(
args,
ActionKind::SessionPrevious,
EmptyParams {},
output_format,
),
SessionCommand::Next(args) => {
run_action_with_params(args, ActionKind::SessionNext, EmptyParams {}, output_format)
}
SessionCommand::ReopenClosed(args) => run_action_with_params(
args,
ActionKind::SessionReopenClosed,
EmptyParams {},
output_format,
),
}
}
pub(super) fn run_input_command(
command: InputCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
InputCommand::Insert(args) => run_action_with_params(
args.target,
ActionKind::InputInsert,
TextParams { text: args.text },
output_format,
),
InputCommand::Replace(args) => run_action_with_params(
args.target,
ActionKind::InputReplace,
TextParams { text: args.text },
output_format,
),
}
}
pub(super) fn run_theme_command(
command: ThemeCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
ThemeCommand::List(args) => {
run_action_with_params(args, ActionKind::ThemeList, EmptyParams {}, output_format)
}
ThemeCommand::Get(args) => {
run_action_with_params(args, ActionKind::ThemeGet, EmptyParams {}, output_format)
}
ThemeCommand::Set(args) => run_action_with_params(
args.target,
ActionKind::ThemeSet,
ThemeNameParams {
theme_name: args.name,
},
output_format,
),
ThemeCommand::SystemSet(args) => run_action_with_params(
args.target,
ActionKind::ThemeSystemSet,
BooleanValueParams {
value: args.enabled,
},
output_format,
),
ThemeCommand::LightSet(args) => run_action_with_params(
args.target,
ActionKind::ThemeLightSet,
ThemeNameParams {
theme_name: args.name,
},
output_format,
),
ThemeCommand::DarkSet(args) => run_action_with_params(
args.target,
ActionKind::ThemeDarkSet,
ThemeNameParams {
theme_name: args.name,
},
output_format,
),
}
}
pub(super) fn run_appearance_command(
command: AppearanceCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
AppearanceCommand::Get(args) => run_action_with_params(
args,
ActionKind::AppearanceGet,
EmptyParams {},
output_format,
),
AppearanceCommand::FontSizeIncrease(args) => {
run_action(args, ActionKind::AppearanceFontSizeIncrease, output_format)
}
AppearanceCommand::FontSizeDecrease(args) => {
run_action(args, ActionKind::AppearanceFontSizeDecrease, output_format)
}
AppearanceCommand::FontSizeReset(args) => {
run_action(args, ActionKind::AppearanceFontSizeReset, output_format)
}
AppearanceCommand::ZoomIncrease(args) => {
run_action(args, ActionKind::AppearanceZoomIncrease, output_format)
}
AppearanceCommand::ZoomDecrease(args) => {
run_action(args, ActionKind::AppearanceZoomDecrease, output_format)
}
AppearanceCommand::ZoomReset(args) => {
run_action(args, ActionKind::AppearanceZoomReset, output_format)
}
}
}
pub(super) fn run_setting_command(
command: SettingCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
SettingCommand::List(args) => run_action_with_params(
args.target,
ActionKind::SettingList,
SettingListParams {
namespace: args.namespace,
},
output_format,
),
SettingCommand::Get(args) => run_action_with_params(
args.target,
ActionKind::SettingGet,
KeyParams { key: args.key },
output_format,
),
SettingCommand::Set(args) => run_action_with_params(
args.target,
ActionKind::SettingSet,
KeyValueParams {
key: args.key,
value: parse_json_value_or_string(args.value),
},
output_format,
),
SettingCommand::Toggle(args) => run_action_with_params(
args.target,
ActionKind::SettingToggle,
KeyParams { key: args.key },
output_format,
),
}
}
pub(super) fn run_keybinding_command(
command: KeybindingCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
KeybindingCommand::List(args) => run_action_with_params(
args,
ActionKind::KeybindingList,
EmptyParams {},
output_format,
),
KeybindingCommand::Get(args) => run_action_with_params(
args.target,
ActionKind::KeybindingGet,
BindingNameParams {
binding_name: args.name,
},
output_format,
),
}
}
pub(super) fn run_file_command(
command: FileCommand,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
FileCommand::Open(args) => run_action_with_params(
args.target,
ActionKind::FileOpen,
FileOpenParams {
path: args.path,
line: args.line,
column: args.column,
new_tab: args.new_tab,
},
output_format,
),
}
}
fn tab_activation_mode(args: &TabActivateArgs) -> TabActivationMode {
if args.previous {
TabActivationMode::Previous
} else if args.next {
TabActivationMode::Next
} else if args.last {
TabActivationMode::Last
} else {
TabActivationMode::Target
}
}
fn tab_close_mode(args: &TabCloseArgs) -> TabCloseMode {
if args.others {
TabCloseMode::Others
} else if args.right_of {
TabCloseMode::RightOf
} else if args.active {
TabCloseMode::Active
} else {
TabCloseMode::Target
}
}
fn run_surface_query_command(
command: SurfaceQueryCommand,
action: ActionKind,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
SurfaceQueryCommand::Open(args) => run_action_with_params(
args.target,
action,
QueryParams { query: args.query },
output_format,
),
}
}
fn run_surface_open_command(
command: SurfaceOpenCommand,
action: ActionKind,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
SurfaceOpenCommand::Open(args) => {
run_action_with_params(args, action, EmptyParams {}, output_format)
}
}
}
fn run_surface_toggle_command(
command: SurfaceToggleCommand,
action: ActionKind,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match command {
SurfaceToggleCommand::Toggle(args) => {
run_action_with_params(args, action, EmptyParams {}, output_format)
}
}
}
fn run_action(
args: TargetArgs,
action: ActionKind,
output_format: OutputFormat,
) -> Result<(), ControlError> {
run_action_with_params(args, action, EmptyParams {}, output_format)
}
fn run_action_with_params<T: Serialize>(
args: TargetArgs,
action: ActionKind,
params: T,
output_format: OutputFormat,
) -> Result<(), ControlError> {
let selector = instance_selector(&args);
let records = local_control::discovery::list_instances(&ChannelState::channel().to_string());
let target = target_selector(&args)?;
let instance = select_instance(&records, &selector)?;
let mut request = RequestEnvelope::new(Action::with_params(action, params)?);
request.target = target;
let response = local_control::client::send_request(&instance, &request)?;
let local_control::protocol::ControlResponse::Ok { data } = response.response else {
return Err(ControlError::new(
ErrorCode::Internal,
"local-control request failed without an error payload",
));
};
match output_format {
OutputFormat::Json => write_json(&data),
OutputFormat::Ndjson => write_json_line(&data),
OutputFormat::Pretty | OutputFormat::Text => {
println!("{}", render_human_readable(action, &data));
Ok(())
}
}
}
fn parse_json_value_or_string(value: String) -> serde_json::Value {
serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value))
}
@@ -0,0 +1,32 @@
//! Shell completion generation for `warpctrl`.
use clap_complete::aot::{Shell, generate};
use local_control::protocol::{ControlError, ErrorCode};
use crate::local_control::ControlArgs;
pub(super) fn generate_completions_to_stdout(shell: Option<Shell>) -> Result<(), ControlError> {
let shell = shell.or_else(Shell::from_env).ok_or_else(|| {
ControlError::new(
ErrorCode::InvalidParams,
"could not determine shell from environment; provide a shell argument",
)
})?;
let mut cmd = ControlArgs::clap_command();
let bin_name = crate::binary_name().unwrap_or_else(|| "warpctrl".to_owned());
generate(shell, &mut cmd, bin_name, &mut std::io::stdout());
Ok(())
}
#[cfg(test)]
pub(crate) fn generate_completion_string(shell: Shell) -> Result<String, ControlError> {
let mut cmd = ControlArgs::clap_command();
let mut output = Vec::new();
generate(shell, &mut cmd, "warpctrl", &mut output);
String::from_utf8(output).map_err(|err| {
ControlError::with_details(
ErrorCode::Internal,
"failed to render local-control completions",
err.to_string(),
)
})
}
+949
View File
@@ -0,0 +1,949 @@
//! Command-line interface for controlling a running local Warp app.
mod commands;
mod completions;
mod output;
mod selectors;
use std::ffi::OsString;
use std::process::ExitCode;
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use clap_complete::aot::Shell;
use commands::{
run_action_catalog_command, run_app_command, run_appearance_command, run_capability_command,
run_file_command, run_input_command, run_instance_command, run_keybinding_command,
run_pane_command, run_session_command, run_setting_command, run_surface_command,
run_tab_command, run_theme_command, run_window_command,
};
use completions::generate_completions_to_stdout;
use output::write_control_error;
use crate::agent::OutputFormat;
/// Hidden flag used by the channel-specific Warp app binary to enter `warpctrl` mode.
pub const CONTROL_MODE_FLAG: &str = "--warpctrl";
/// Parsed top-level arguments for `warpctrl`.
#[derive(Debug, Parser)]
#[command(
name = "warpctrl",
display_name = "warpctrl",
about = "Control a running local Warp app instance"
)]
pub struct ControlArgs {
/// Set the output format.
#[arg(
long = "output-format",
global = true,
value_enum,
default_value_t = OutputFormat::Pretty,
env = "WARP_OUTPUT_FORMAT"
)]
pub output_format: OutputFormat,
#[command(subcommand)]
pub command: ControlCommand,
}
/// Commands that inspect the public action catalog.
#[derive(Debug, Clone, Subcommand)]
pub enum ActionCatalogCommand {
/// List allowlisted catalog actions.
List,
/// Inspect a single allowlisted catalog action.
Inspect {
/// Canonical action name, such as `tab.create` or `surface.settings.open`.
action: String,
},
}
impl ControlArgs {
pub fn from_env() -> Self {
let bin_name = crate::binary_name().unwrap_or_else(|| "warpctrl".to_owned());
Self::try_parse_from_args(std::env::args_os(), bin_name).unwrap_or_else(|err| err.exit())
}
/// Parse Warp Control arguments only when the wrapper-injected mode flag is present.
///
/// Startup calls this before the normal Warp/Oz parser. Arguments through
/// `--warpctrl` are removed, and the remaining arguments are parsed as if
/// the standalone command name were `warpctrl`.
pub fn from_control_mode_env() -> Option<Self> {
Self::try_parse_control_mode_from(std::env::args_os())
.map(|result| result.unwrap_or_else(|err| err.exit()))
}
/// Testable implementation of [`Self::from_control_mode_env`].
pub fn try_parse_control_mode_from<I, T>(args: I) -> Option<Result<Self, clap::Error>>
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let mut stripped_args = vec![OsString::from("warpctrl")];
let mut found_control_mode = false;
for arg in args {
let arg = arg.into();
if !found_control_mode {
if arg.to_str() == Some(CONTROL_MODE_FLAG) {
found_control_mode = true;
}
continue;
}
stripped_args.push(arg);
}
found_control_mode.then(|| Self::try_parse_from_args(stripped_args, "warpctrl"))
}
pub fn clap_command() -> clap::Command {
let bin_name = crate::binary_name().unwrap_or_else(|| "warpctrl".to_owned());
Self::clap_command_for_bin_name(bin_name)
}
fn try_parse_from_args<I, T>(args: I, bin_name: impl Into<String>) -> Result<Self, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let matches = Self::clap_command_for_bin_name(bin_name).try_get_matches_from(args)?;
Self::from_arg_matches(&matches)
}
fn clap_command_for_bin_name(bin_name: impl Into<String>) -> clap::Command {
let bin_name = bin_name.into();
<Self as CommandFactory>::command()
.version(crate::version_string())
.bin_name(bin_name.clone())
.after_help(color_print::cformat!(
r#"<bold><underline>Examples:</underline></bold>
<dim>$</dim> <bold>{bin_name} instance list</bold>
<dim>$</dim> <bold>{bin_name} tab create</bold>
<dim>$</dim> <bold>{bin_name} action list</bold>
<dim>$</dim> <bold>{bin_name} action inspect surface.settings.open</bold>
<bold><underline>Learn more:</underline></bold>
* Use <bold>{bin_name} help</bold> to learn more about each command
* Use <bold>{bin_name} action list</bold> to inspect allowlisted actions
"#
))
}
}
/// Top-level `warpctrl` command groups.
#[derive(Debug, Clone, Subcommand)]
pub enum ControlCommand {
/// Inspect local Warp app instances.
#[command(subcommand)]
Instance(InstanceCommand),
/// Inspect a selected local Warp app.
#[command(subcommand)]
App(AppCommand),
/// Inspect local-control capabilities.
#[command(subcommand)]
Capability(CapabilityCommand),
/// Inspect public action metadata and implementation status.
#[command(subcommand)]
Action(ActionCatalogCommand),
/// Inspect local Warp windows.
#[command(subcommand)]
Window(WindowCommand),
/// Control local Warp tabs.
#[command(subcommand)]
Tab(TabCommand),
/// Inspect local Warp panes.
#[command(subcommand)]
Pane(PaneCommand),
/// Inspect local Warp sessions.
#[command(subcommand)]
Session(SessionCommand),
/// Inspect terminal input state.
#[command(subcommand)]
Input(InputCommand),
/// Inspect Warp themes.
#[command(subcommand)]
Theme(ThemeCommand),
/// Inspect appearance state.
#[command(subcommand)]
Appearance(AppearanceCommand),
/// Inspect allowlisted settings.
#[command(subcommand)]
Setting(SettingCommand),
/// Inspect keybinding metadata.
#[command(subcommand)]
Keybinding(KeybindingCommand),
/// Inspect open file app-state metadata.
#[command(subcommand)]
File(FileCommand),
/// Open or toggle local Warp surfaces.
#[command(subcommand)]
Surface(SurfaceCommand),
/// Generate shell completions for your shell to stdout.
///
/// For bash, add the following to ~/.bashrc:
/// source <(path/to/warpctrl completions bash)
///
/// For zsh, add the following to ~/.zshrc:
/// source <(path/to/warpctrl completions zsh)
///
/// For fish, add the following to ~/.config/fish/config.fish:
/// path/to/warpctrl completions fish | source
///
/// For Powershell, add the following to $PROFILE:
/// path\to\warpctrl completions powershell | Out-String | Invoke-Expression
///
/// If no shell is provided, this defaults to the shell that Warp was run from.
#[command(verbatim_doc_comment)]
Completions {
/// Shell to generate completions for.
#[arg(value_enum)]
shell: Option<Shell>,
},
}
/// Commands that inspect locally discoverable Warp instances.
#[derive(Debug, Clone, Subcommand)]
pub enum InstanceCommand {
/// List locally discoverable Warp instances.
List,
/// Print app, protocol, active target, and action metadata for the selected instance.
Inspect(TargetArgs),
}
/// Commands that inspect the selected Warp app instance.
#[derive(Debug, Clone, Subcommand)]
pub enum AppCommand {
/// Check that the selected local Warp app responds.
Ping(TargetArgs),
/// Print protocol and build identity metadata for the selected local Warp app.
Version(TargetArgs),
/// Print the active window/tab/pane/session chain.
Active(TargetArgs),
/// Focus the selected local Warp app.
Focus(TargetArgs),
}
/// Commands that inspect public local-control capabilities.
#[derive(Debug, Clone, Subcommand)]
pub enum CapabilityCommand {
/// List allowlisted local-control capabilities.
List,
/// Inspect a single local-control capability by canonical action name.
Inspect {
/// Canonical action name, such as `tab.create` or `surface.settings.open`.
action: String,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum WindowCommand {
/// List windows in the selected local Warp app.
List(TargetArgs),
/// Inspect one window in the selected local Warp app.
Inspect(TargetArgs),
/// Create a new window.
Create(TabCreateArgs),
/// Focus a window.
Focus(TargetArgs),
/// Close a window.
Close(TargetArgs),
}
/// Commands that control tabs in the selected Warp app instance.
#[derive(Debug, Clone, Subcommand)]
pub enum TabCommand {
/// List tabs in the selected local Warp app.
List(TargetArgs),
/// Inspect one tab in the selected local Warp app.
Inspect(TargetArgs),
/// Create a new terminal tab in the active window.
Create(TabCreateArgs),
/// Activate a tab.
Activate(TabActivateArgs),
/// Move the active tab.
Move(TabMoveArgs),
/// Close tabs.
Close(TabCloseArgs),
/// Rename a tab.
Rename(RenameArgs),
/// Reset a tab name.
ResetName(TargetArgs),
/// Set or clear a tab color.
#[command(subcommand)]
Color(TabColorCommand),
}
/// Commands that control tab colors.
#[derive(Debug, Clone, Subcommand)]
pub enum TabColorCommand {
/// Set a tab color.
Set(ColorSetArgs),
/// Clear a tab color.
Clear(TargetArgs),
}
/// Commands that inspect local Warp panes.
#[derive(Debug, Clone, Subcommand)]
pub enum PaneCommand {
/// List panes in the selected local Warp app.
List(TargetArgs),
/// Inspect one pane in the selected local Warp app.
Inspect(TargetArgs),
/// Split the active pane.
Split(PaneSplitArgs),
/// Focus a pane.
Focus(TargetArgs),
/// Navigate between panes.
Navigate(PaneNavigateArgs),
/// Resize the active pane.
Resize(PaneResizeArgs),
/// Maximize the active pane.
Maximize(TargetArgs),
/// Unmaximize the active pane.
Unmaximize(TargetArgs),
/// Close the active pane.
Close(TargetArgs),
/// Rename a pane.
Rename(RenameArgs),
/// Reset a pane name.
ResetName(TargetArgs),
}
/// Commands that inspect local Warp sessions.
#[derive(Debug, Clone, Subcommand)]
pub enum SessionCommand {
/// List sessions in the selected local Warp app.
List(TargetArgs),
/// Inspect one session in the selected local Warp app.
Inspect(TargetArgs),
/// Activate a session.
Activate(TargetArgs),
/// Activate the previous session.
Previous(TargetArgs),
/// Activate the next session.
Next(TargetArgs),
/// Reopen the most recently closed session.
ReopenClosed(TargetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum InputCommand {
/// Insert text into the input buffer without submitting it.
Insert(TextTargetArgs),
/// Replace the input buffer without submitting it.
Replace(TextTargetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SurfaceCommand {
/// List available and unavailable tour surfaces.
List(TargetArgs),
/// Open settings surfaces.
#[command(subcommand)]
Settings(SurfaceSettingsCommand),
/// Open the command palette.
#[command(subcommand)]
CommandPalette(SurfaceQueryCommand),
/// Open command search.
#[command(subcommand)]
CommandSearch(SurfaceQueryCommand),
/// Open the theme picker.
#[command(subcommand)]
ThemePicker(SurfaceOpenCommand),
/// Open keybinding settings.
#[command(subcommand)]
Keybindings(SurfaceOpenCommand),
/// Open or toggle Warp Drive.
#[command(subcommand)]
WarpDrive(SurfaceOpenToggleCommand),
/// Toggle the resource center.
#[command(subcommand)]
ResourceCenter(SurfaceToggleCommand),
/// Toggle the AI assistant.
#[command(subcommand)]
AiAssistant(SurfaceToggleCommand),
/// Open or toggle code review.
#[command(subcommand)]
CodeReview(SurfaceOpenToggleCommand),
/// Open the project explorer.
#[command(subcommand)]
ProjectExplorer(SurfaceOpenCommand),
/// Open global search.
#[command(subcommand)]
GlobalSearch(SurfaceOpenCommand),
/// Open the conversation list.
#[command(subcommand)]
ConversationList(SurfaceOpenCommand),
/// Toggle the left panel.
#[command(subcommand)]
LeftPanel(SurfaceToggleCommand),
/// Toggle the right panel.
#[command(subcommand)]
RightPanel(SurfaceToggleCommand),
/// Open or toggle vertical tabs.
#[command(subcommand)]
VerticalTabs(SurfaceOpenToggleCommand),
/// Open agent management.
#[command(subcommand)]
AgentManagement(SurfaceOpenCommand),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SurfaceSettingsCommand {
/// Open Settings, optionally scoped to a page or query.
Open(PageQueryArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SurfaceQueryCommand {
/// Open the surface with an optional seeded query.
Open(QueryArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SurfaceOpenCommand {
/// Open the surface.
Open(TargetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SurfaceOpenToggleCommand {
/// Open the surface.
Open(TargetArgs),
/// Toggle the surface.
Toggle(TargetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SurfaceToggleCommand {
/// Toggle the surface.
Toggle(TargetArgs),
}
/// Commands that inspect Warp themes.
#[derive(Debug, Clone, Subcommand)]
pub enum ThemeCommand {
/// List available themes.
List(TargetArgs),
/// Read current theme state.
Get(TargetArgs),
/// Set the current theme.
Set(ThemeSetArgs),
/// Set whether Warp follows the system theme.
SystemSet(ThemeSystemSetArgs),
/// Set the light theme used when following the system theme.
LightSet(ThemeSetArgs),
/// Set the dark theme used when following the system theme.
DarkSet(ThemeSetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum AppearanceCommand {
/// Read appearance state.
Get(TargetArgs),
/// Increase terminal font size.
FontSizeIncrease(TargetArgs),
/// Decrease terminal font size.
FontSizeDecrease(TargetArgs),
/// Reset terminal font size.
FontSizeReset(TargetArgs),
/// Increase UI zoom.
ZoomIncrease(TargetArgs),
/// Decrease UI zoom.
ZoomDecrease(TargetArgs),
/// Reset UI zoom.
ZoomReset(TargetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum SettingCommand {
/// List allowlisted settings.
List(NamespaceTargetArgs),
/// Read one allowlisted setting.
Get(SettingGetArgs),
/// Set one allowlisted setting.
Set(SettingSetArgs),
/// Toggle one allowlisted boolean setting.
Toggle(SettingToggleArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum KeybindingCommand {
/// List keybindings.
List(TargetArgs),
/// Read one keybinding by name.
Get(KeybindingGetArgs),
}
#[derive(Debug, Clone, Subcommand)]
pub enum FileCommand {
/// Open a file in Warp.
Open(FileOpenArgs),
}
/// Exact selectors for a target within the selected Warp instance.
#[derive(Debug, Clone, Args, Default)]
pub struct TargetArgs {
/// Target a specific local Warp instance id from `warpctrl instance list`.
#[arg(long = "instance", conflicts_with = "pid")]
pub instance: Option<String>,
/// Target a specific local Warp process id.
#[arg(long = "pid", conflicts_with = "instance")]
pub pid: Option<u32>,
/// Target the active window or an opaque window id.
#[arg(long = "window", conflicts_with_all = ["window_index", "window_title"])]
pub window: Option<String>,
/// Target a window by scoped index when the handler supports it.
#[arg(long = "window-index", conflicts_with_all = ["window", "window_title"])]
pub window_index: Option<u32>,
/// Target a window by exact title when the handler supports it.
#[arg(long = "window-title", conflicts_with_all = ["window", "window_index"])]
pub window_title: Option<String>,
/// Target the active tab or an opaque tab id.
#[arg(long = "tab", conflicts_with_all = ["tab_index", "tab_title"])]
pub tab: Option<String>,
/// Target a tab by scoped index when the handler supports it.
#[arg(long = "tab-index", conflicts_with_all = ["tab", "tab_title"])]
pub tab_index: Option<u32>,
/// Target a tab by exact title when the handler supports it.
#[arg(long = "tab-title", conflicts_with_all = ["tab", "tab_index"])]
pub tab_title: Option<String>,
/// Target the active pane or an opaque pane id.
#[arg(long = "pane", conflicts_with = "pane_index")]
pub pane: Option<String>,
/// Target a pane by scoped index when the handler supports it.
#[arg(long = "pane-index", conflicts_with = "pane")]
pub pane_index: Option<u32>,
/// Target the active session or an opaque session id.
#[arg(long = "session")]
pub session: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct TabCreateArgs {
#[arg(long = "type", value_enum)]
pub tab_type: Option<CliTabType>,
#[arg(long = "shell")]
pub shell: Option<String>,
#[command(flatten)]
pub target: TargetArgs,
}
#[derive(Debug, Clone, Args)]
pub struct TabActivateArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "previous", conflicts_with_all = ["next", "last"])]
pub previous: bool,
#[arg(long = "next", conflicts_with_all = ["previous", "last"])]
pub next: bool,
#[arg(long = "last", conflicts_with_all = ["previous", "next"])]
pub last: bool,
}
#[derive(Debug, Clone, Args)]
pub struct TabMoveArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "direction", value_enum)]
pub direction: CliTabMoveDirection,
}
#[derive(Debug, Clone, Args)]
pub struct TabCloseArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "active", conflicts_with_all = ["others", "right_of"])]
pub active: bool,
#[arg(long = "others", conflicts_with_all = ["active", "right_of"])]
pub others: bool,
#[arg(long = "right-of", conflicts_with_all = ["active", "others"])]
pub right_of: bool,
}
#[derive(Debug, Clone, Args)]
pub struct PaneSplitArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "direction", value_enum)]
pub direction: CliCardinalDirection,
}
#[derive(Debug, Clone, Args)]
pub struct PaneNavigateArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "direction", value_enum)]
pub direction: CliDirection,
}
#[derive(Debug, Clone, Args)]
pub struct PaneResizeArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "direction", value_enum)]
pub direction: CliCardinalDirection,
#[arg(long = "amount")]
pub amount: Option<u32>,
}
#[derive(Debug, Clone, Args)]
pub struct TextTargetArgs {
pub text: String,
#[command(flatten)]
pub target: TargetArgs,
}
#[derive(Debug, Clone, Args)]
pub struct PageQueryArgs {
#[arg(long = "page")]
pub page: Option<String>,
#[arg(long = "query")]
pub query: Option<String>,
#[command(flatten)]
pub target: TargetArgs,
}
#[derive(Debug, Clone, Args)]
pub struct QueryArgs {
#[arg(long = "query")]
pub query: Option<String>,
#[command(flatten)]
pub target: TargetArgs,
}
#[derive(Debug, Clone, Args)]
pub struct FileOpenArgs {
pub path: String,
#[arg(long = "line")]
pub line: Option<u32>,
#[arg(long = "column")]
pub column: Option<u32>,
#[arg(long = "new-tab")]
pub new_tab: bool,
#[command(flatten)]
pub target: TargetArgs,
}
#[derive(Debug, Clone, Args)]
pub struct RenameArgs {
#[command(flatten)]
pub target: TargetArgs,
pub title: String,
}
#[derive(Debug, Clone, Args)]
pub struct ColorSetArgs {
#[command(flatten)]
pub target: TargetArgs,
pub color: String,
}
#[derive(Debug, Clone, Args)]
pub struct ThemeSetArgs {
#[command(flatten)]
pub target: TargetArgs,
pub name: String,
}
#[derive(Debug, Clone, Args)]
pub struct ThemeSystemSetArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(action = clap::ArgAction::Set)]
pub enabled: bool,
}
#[derive(Debug, Clone, Args)]
pub struct SettingSetArgs {
#[command(flatten)]
pub target: TargetArgs,
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Args)]
pub struct SettingToggleArgs {
#[command(flatten)]
pub target: TargetArgs,
pub key: String,
}
#[derive(Debug, Clone, Args)]
pub struct NamespaceTargetArgs {
#[command(flatten)]
pub target: TargetArgs,
#[arg(long = "namespace")]
pub namespace: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct SettingGetArgs {
#[command(flatten)]
pub target: TargetArgs,
/// Allowlisted setting key.
pub key: String,
}
#[derive(Debug, Clone, Args)]
pub struct KeybindingGetArgs {
#[command(flatten)]
pub target: TargetArgs,
/// Keybinding action name.
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CliTabType {
Terminal,
Agent,
CloudAgent,
Default,
}
impl From<CliTabType> for local_control::protocol::TabType {
fn from(value: CliTabType) -> Self {
match value {
CliTabType::Terminal => Self::Terminal,
CliTabType::Agent => Self::Agent,
CliTabType::CloudAgent => Self::CloudAgent,
CliTabType::Default => Self::Default,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CliCardinalDirection {
Left,
Right,
Up,
Down,
}
impl From<CliCardinalDirection> for local_control::protocol::Direction {
fn from(value: CliCardinalDirection) -> Self {
match value {
CliCardinalDirection::Left => Self::Left,
CliCardinalDirection::Right => Self::Right,
CliCardinalDirection::Up => Self::Up,
CliCardinalDirection::Down => Self::Down,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CliDirection {
Left,
Right,
Up,
Down,
Previous,
Next,
}
impl From<CliDirection> for local_control::protocol::Direction {
fn from(value: CliDirection) -> Self {
match value {
CliDirection::Left => Self::Left,
CliDirection::Right => Self::Right,
CliDirection::Up => Self::Up,
CliDirection::Down => Self::Down,
CliDirection::Previous => Self::Previous,
CliDirection::Next => Self::Next,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CliTabMoveDirection {
Left,
Right,
Previous,
Next,
}
impl From<CliTabMoveDirection> for local_control::protocol::Direction {
fn from(value: CliTabMoveDirection) -> Self {
match value {
CliTabMoveDirection::Left => Self::Left,
CliTabMoveDirection::Right => Self::Right,
CliTabMoveDirection::Previous => Self::Previous,
CliTabMoveDirection::Next => Self::Next,
}
}
}
pub fn run(args: ControlArgs) -> ExitCode {
ExitCode::from(run_exit_code(args))
}
pub fn run_and_exit(args: ControlArgs) -> ! {
std::process::exit(i32::from(run_exit_code(args)))
}
fn run_exit_code(args: ControlArgs) -> u8 {
let output_format = args.output_format;
match run_inner(args) {
Ok(()) => 0,
Err(error) => {
if let Err(write_error) = write_control_error(&error, output_format) {
eprintln!(
"error: failed to render local-control error: {}",
write_error.message
);
}
1
}
}
}
fn run_inner(args: ControlArgs) -> Result<(), local_control::protocol::ControlError> {
let output_format = args.output_format;
match args.command {
ControlCommand::Instance(command) => run_instance_command(command, output_format),
ControlCommand::App(command) => run_app_command(command, output_format),
ControlCommand::Capability(command) => run_capability_command(command, output_format),
ControlCommand::Action(command) => run_action_catalog_command(command, output_format),
ControlCommand::Window(command) => run_window_command(command, output_format),
ControlCommand::Tab(command) => run_tab_command(command, output_format),
ControlCommand::Pane(command) => run_pane_command(command, output_format),
ControlCommand::Session(command) => run_session_command(command, output_format),
ControlCommand::Input(command) => run_input_command(command, output_format),
ControlCommand::Theme(command) => run_theme_command(command, output_format),
ControlCommand::Appearance(command) => run_appearance_command(command, output_format),
ControlCommand::Setting(command) => run_setting_command(command, output_format),
ControlCommand::Keybinding(command) => run_keybinding_command(command, output_format),
ControlCommand::File(command) => run_file_command(command, output_format),
ControlCommand::Surface(command) => run_surface_command(command, output_format),
ControlCommand::Completions { shell } => generate_completions_to_stdout(shell),
}
}
#[cfg(test)]
pub(crate) use commands::render_human_readable_for_test;
#[cfg(test)]
pub(crate) use completions::generate_completion_string;
#[cfg(test)]
pub(crate) use output::ErrorSummary;
#[cfg(test)]
#[path = "../local_control_tests.rs"]
mod tests;
@@ -0,0 +1,53 @@
//! Output rendering helpers for `warpctrl`.
use std::io::Write as _;
use local_control::protocol::{ControlError, ErrorCode};
use serde::Serialize;
use crate::agent::OutputFormat;
/// JSON/NDJSON error payload emitted by `warpctrl`.
#[derive(Serialize)]
pub(crate) struct ErrorSummary<'a> {
pub ok: bool,
pub error: &'a ControlError,
}
pub(super) fn write_control_error(
error: &ControlError,
output_format: OutputFormat,
) -> Result<(), ControlError> {
match output_format {
OutputFormat::Json => write_json(&ErrorSummary { ok: false, error }),
OutputFormat::Ndjson => write_json_line(&ErrorSummary { ok: false, error }),
OutputFormat::Pretty | OutputFormat::Text => {
eprintln!("error: {}: {}", error.code, error.message);
if let Some(details) = &error.details {
eprintln!("details: {details}");
}
Ok(())
}
}
}
pub(super) fn write_json(value: &impl Serialize) -> Result<(), ControlError> {
let stdout = std::io::stdout();
let mut lock = stdout.lock();
serde_json::to_writer_pretty(&mut lock, value).map_err(write_error)?;
writeln!(&mut lock).map_err(write_error)?;
Ok(())
}
pub(super) fn write_json_line(value: &impl Serialize) -> Result<(), ControlError> {
let stdout = std::io::stdout();
let mut lock = stdout.lock();
serde_json::to_writer(&mut lock, value).map_err(write_error)?;
writeln!(&mut lock).map_err(write_error)?;
Ok(())
}
fn write_error(error: impl std::error::Error) -> ControlError {
ControlError::with_details(
ErrorCode::Internal,
"failed to write local-control output",
error.to_string(),
)
}
@@ -0,0 +1,100 @@
//! CLI argument conversion into shared local-control selectors.
use local_control::protocol::{
ControlError, ErrorCode, PaneSelector, PaneTarget, SessionSelector, SessionTarget, TabSelector,
TabTarget, TargetSelector, WindowSelector, WindowTarget,
};
use local_control::selection::InstanceSelector;
use crate::local_control::TargetArgs;
pub(super) fn instance_selector(args: &TargetArgs) -> InstanceSelector {
if let Some(instance_id) = &args.instance {
return InstanceSelector::Id(local_control::discovery::InstanceId(instance_id.clone()));
}
if let Some(pid) = args.pid {
return InstanceSelector::Pid(pid);
}
InstanceSelector::Active
}
pub(super) fn target_selector(args: &TargetArgs) -> Result<TargetSelector, ControlError> {
Ok(TargetSelector {
window: window_target(args)?,
tab: tab_target(args)?,
pane: pane_target(args)?,
session: session_target(args)?,
})
}
fn window_target(args: &TargetArgs) -> Result<Option<WindowTarget>, ControlError> {
if let Some(window) = &args.window {
if window == "active" {
return Ok(Some(WindowTarget::Active));
}
return Ok(Some(WindowTarget::Id {
id: WindowSelector(window.clone()),
}));
}
if let Some(index) = args.window_index {
return Ok(Some(WindowTarget::Index { index }));
}
if let Some(title) = &args.window_title {
return Ok(Some(WindowTarget::Title {
title: title.clone(),
}));
}
Ok(None)
}
fn tab_target(args: &TargetArgs) -> Result<Option<TabTarget>, ControlError> {
if let Some(tab) = &args.tab {
if tab == "active" {
return Ok(Some(TabTarget::Active));
}
return Ok(Some(TabTarget::Id {
id: TabSelector(tab.clone()),
}));
}
if let Some(index) = args.tab_index {
return Ok(Some(TabTarget::Index { index }));
}
if let Some(title) = &args.tab_title {
return Ok(Some(TabTarget::Title {
title: title.clone(),
}));
}
Ok(None)
}
fn pane_target(args: &TargetArgs) -> Result<Option<PaneTarget>, ControlError> {
if let Some(pane) = &args.pane {
if pane == "active" {
return Ok(Some(PaneTarget::Active));
}
return Ok(Some(PaneTarget::Id {
id: PaneSelector(pane.clone()),
}));
}
if let Some(index) = args.pane_index {
return Ok(Some(PaneTarget::Index { index }));
}
Ok(None)
}
fn session_target(args: &TargetArgs) -> Result<Option<SessionTarget>, ControlError> {
if let Some(session) = &args.session {
if session == "active" {
return Ok(Some(SessionTarget::Active));
}
if session.is_empty() {
return Err(ControlError::new(
ErrorCode::InvalidSelector,
"session selector cannot be empty",
));
}
return Ok(Some(SessionTarget::Id {
id: SessionSelector(session.clone()),
}));
}
Ok(None)
}
@@ -0,0 +1,738 @@
use std::collections::HashSet;
use clap_complete::aot::Shell;
use local_control::protocol::{ActionKind, ControlError, ErrorCode};
use serde_json::json;
use super::*;
#[test]
fn parses_typed_create_and_setting_list_params() {
let args = ControlArgs::try_parse_from([
"warpctrl",
"tab",
"create",
"--type",
"agent",
"--shell",
"zsh",
"--session",
"session_1",
])
.expect("tab create parses");
let ControlCommand::Tab(TabCommand::Create(args)) = args.command else {
panic!("expected tab create command");
};
assert_eq!(args.tab_type, Some(CliTabType::Agent));
assert_eq!(args.shell.as_deref(), Some("zsh"));
assert_eq!(args.target.session.as_deref(), Some("session_1"));
let args =
ControlArgs::try_parse_from(["warpctrl", "setting", "list", "--namespace", "editor"])
.expect("setting list parses");
let ControlCommand::Setting(SettingCommand::List(args)) = args.command else {
panic!("expected setting list command");
};
assert_eq!(args.namespace.as_deref(), Some("editor"));
}
#[test]
fn rejects_conflicting_instance_selectors() {
let err = ControlArgs::try_parse_from([
"warpctrl",
"tab",
"create",
"--instance",
"inst_123",
"--pid",
"123",
])
.expect_err("instance and pid conflict");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn parses_instance_and_pid_selectors() {
let args = ControlArgs::try_parse_from(["warpctrl", "tab", "create", "--instance", "inst_123"])
.expect("instance selector parses");
let ControlCommand::Tab(TabCommand::Create(create)) = args.command else {
panic!("expected tab create command");
};
assert_eq!(create.target.instance.as_deref(), Some("inst_123"));
let args = ControlArgs::try_parse_from(["warpctrl", "app", "ping", "--pid", "123"])
.expect("pid selector parses");
let ControlCommand::App(AppCommand::Ping(target)) = args.command else {
panic!("expected app ping command");
};
assert_eq!(target.pid, Some(123));
}
#[test]
fn surface_list_accepts_instance_selection() {
let args =
ControlArgs::try_parse_from(["warpctrl", "surface", "list", "--instance", "inst_123"])
.expect("surface list instance selector parses");
let ControlCommand::Surface(SurfaceCommand::List(target)) = args.command else {
panic!("expected surface list command");
};
assert_eq!(target.instance.as_deref(), Some("inst_123"));
}
#[test]
fn rejects_excluded_command_routes() {
for args in [
vec!["warpctrl", "history", "list"],
vec!["warpctrl", "block", "list"],
vec!["warpctrl", "block", "inspect", "block_1"],
vec!["warpctrl", "block", "output", "block_1"],
vec!["warpctrl", "input", "get"],
vec!["warpctrl", "input", "clear"],
vec!["warpctrl", "input", "mode", "set", "agent"],
vec!["warpctrl", "input", "run", "pwd"],
vec!["warpctrl", "file", "list"],
vec!["warpctrl", "drive", "list"],
vec!["warpctrl", "auth", "status"],
] {
assert!(ControlArgs::try_parse_from(args).is_err());
}
}
#[test]
fn parses_first_slice_instance_list() {
let args = ControlArgs::try_parse_from(["warpctrl", "instance", "list"])
.expect("instance list parses");
assert!(matches!(
args.command,
ControlCommand::Instance(InstanceCommand::List)
));
}
#[test]
fn parses_first_slice_app_smoke_metadata_commands() {
assert!(ControlArgs::try_parse_from(["warpctrl", "app", "ping"]).is_ok());
assert!(ControlArgs::try_parse_from(["warpctrl", "app", "version"]).is_ok());
assert!(ControlArgs::try_parse_from(["warpctrl", "app", "active"]).is_ok());
assert!(ControlArgs::try_parse_from(["warpctrl", "app", "focus"]).is_ok());
}
#[test]
fn parses_catalog_metadata_commands() {
let args =
ControlArgs::try_parse_from(["warpctrl", "action", "inspect", "surface.settings.open"])
.expect("action inspect parses");
let ControlCommand::Action(ActionCatalogCommand::Inspect { action }) = args.command else {
panic!("expected action inspect command");
};
assert_eq!(action, "surface.settings.open");
assert!(ControlArgs::try_parse_from(["warpctrl", "action", "list"]).is_ok());
assert!(ControlArgs::try_parse_from(["warpctrl", "capability", "list"]).is_ok());
assert!(
ControlArgs::try_parse_from(["warpctrl", "capability", "inspect", "tab.create"]).is_ok()
);
}
#[test]
fn parses_control_mode_args_after_hidden_flag() {
let args = ControlArgs::try_parse_control_mode_from(["warp", "--warpctrl", "tab", "create"])
.expect("control mode flag is present")
.expect("control mode args parse");
assert!(matches!(
args.command,
ControlCommand::Tab(TabCommand::Create(_))
));
}
#[test]
fn ignores_args_without_control_mode_flag() {
assert!(ControlArgs::try_parse_control_mode_from(["warp", "tab", "create"]).is_none());
}
#[test]
fn parses_completion_generation_command() {
let args = ControlArgs::try_parse_from(["warpctrl", "completions", "bash"])
.expect("completions parses");
assert!(matches!(
args.command,
ControlCommand::Completions {
shell: Some(Shell::Bash)
}
));
}
#[test]
fn parses_exact_window_tab_pane_and_session_selectors() {
let args = ControlArgs::try_parse_from([
"warpctrl",
"session",
"inspect",
"--window-title",
"docs",
"--tab-index",
"2",
"--pane",
"pane_1",
"--session",
"session_1",
])
.expect("exact target selectors parse");
let ControlCommand::Session(SessionCommand::Inspect(target)) = args.command else {
panic!("expected session inspect command");
};
assert_eq!(target.window_title.as_deref(), Some("docs"));
assert_eq!(target.tab_index, Some(2));
assert_eq!(target.pane.as_deref(), Some("pane_1"));
assert_eq!(target.session.as_deref(), Some("session_1"));
}
#[test]
fn instance_list_output_serializes_empty_and_populated_lists() {
let empty = serde_json::to_value(commands::instance_list_output(Vec::new()))
.expect("empty list serializes");
assert_eq!(empty, json!({ "instances": [] }));
let record = local_control::discovery::InstanceRecord::for_current_process(
None,
"dev",
"dev.warp.Warp",
Some("v0.1.0".to_owned()),
Vec::new(),
);
let instance_id = record.instance_id.0.clone();
let populated = serde_json::to_value(commands::instance_list_output(vec![record]))
.expect("populated list serializes");
assert_eq!(populated["instances"][0]["instance_id"], json!(instance_id));
assert_eq!(populated["instances"][0]["channel"], json!("dev"));
assert_eq!(populated["instances"][0]["app_id"], json!("dev.warp.Warp"));
assert_eq!(populated["instances"][0]["app_version"], json!("v0.1.0"));
}
#[test]
fn excluded_actions_are_not_allowlisted_catalog_entries() {
for excluded in ["auth.api_key.set", "file.write", "block.list"] {
assert!(
ActionKind::ALL
.iter()
.all(|action| action.as_str() != excluded)
);
}
}
#[test]
fn generated_bash_completions_include_readonly_commands() {
let completions =
generate_completion_string(Shell::Bash).expect("bash completions render to UTF-8");
assert!(completions.contains("instance"));
assert!(completions.contains("action"));
assert!(completions.contains("capability"));
assert!(!completions.contains("stubs-only"));
assert!(completions.contains("window"));
assert!(completions.contains("input"));
assert!(completions.contains("completions"));
assert!(!completions.contains("block"));
}
#[test]
fn every_retained_catalog_action_has_a_parseable_cli_example() {
let mut covered = HashSet::new();
for (kind, argv) in retained_action_examples() {
let args = ControlArgs::try_parse_from(argv)
.unwrap_or_else(|err| panic!("{} parses: {err}", kind.as_str()));
assert_eq!(parsed_action_kind(&args.command), Some(kind));
covered.insert(kind);
}
let expected = ActionKind::ALL.iter().copied().collect::<HashSet<_>>();
let missing = expected
.difference(&covered)
.map(|kind| kind.as_str())
.collect::<Vec<_>>();
assert!(
missing.is_empty(),
"retained catalog actions missing parser examples: {missing:?}"
);
}
#[test]
fn generated_bash_completions_include_mutating_command_groups() {
let completions =
generate_completion_string(Shell::Bash).expect("bash completions render to UTF-8");
assert!(completions.contains("surface"));
assert!(completions.contains("command-palette"));
assert!(completions.contains("warp-drive"));
assert!(completions.contains("resource-center"));
assert!(completions.contains("activate"));
assert!(completions.contains("split"));
assert!(!completions.contains("history"));
assert!(!completions.contains("share-to-team"));
}
#[test]
fn structured_error_output_uses_stable_error_code() {
let error = ControlError::new(ErrorCode::NoInstance, "no local Warp control instances");
let value = serde_json::to_value(ErrorSummary {
ok: false,
error: &error,
})
.expect("error summary serializes");
assert_eq!(value["ok"], json!(false));
assert_eq!(value["error"]["code"], json!("no_instance"));
assert_eq!(
value["error"]["message"],
json!("no local Warp control instances")
);
}
#[test]
fn renders_human_readable_tab_create_output() {
let rendered = render_human_readable_for_test(
local_control::protocol::ActionKind::TabCreate,
&json!({
"tab": {
"id": "tab_123",
"active_index": 2,
"count": 3
},
"window": {
"id": "window_123"
}
}),
);
assert_eq!(
rendered,
"Created tab tab_123 in window window_123 (active index 2, tab count 3)"
);
}
fn retained_action_examples() -> Vec<(ActionKind, Vec<&'static str>)> {
vec![
(
ActionKind::InstanceList,
vec!["warpctrl", "instance", "list"],
),
(
ActionKind::InstanceInspect,
vec!["warpctrl", "instance", "inspect"],
),
(ActionKind::AppPing, vec!["warpctrl", "app", "ping"]),
(ActionKind::AppVersion, vec!["warpctrl", "app", "version"]),
(ActionKind::AppActive, vec!["warpctrl", "app", "active"]),
(ActionKind::AppFocus, vec!["warpctrl", "app", "focus"]),
(
ActionKind::CapabilityList,
vec!["warpctrl", "capability", "list"],
),
(
ActionKind::CapabilityInspect,
vec!["warpctrl", "capability", "inspect", "tab.create"],
),
(ActionKind::WindowList, vec!["warpctrl", "window", "list"]),
(
ActionKind::WindowInspect,
vec!["warpctrl", "window", "inspect"],
),
(
ActionKind::WindowCreate,
vec!["warpctrl", "window", "create"],
),
(ActionKind::WindowFocus, vec!["warpctrl", "window", "focus"]),
(ActionKind::WindowClose, vec!["warpctrl", "window", "close"]),
(ActionKind::TabList, vec!["warpctrl", "tab", "list"]),
(ActionKind::TabInspect, vec!["warpctrl", "tab", "inspect"]),
(ActionKind::TabCreate, vec!["warpctrl", "tab", "create"]),
(ActionKind::TabActivate, vec!["warpctrl", "tab", "activate"]),
(
ActionKind::TabMove,
vec!["warpctrl", "tab", "move", "--direction", "next"],
),
(ActionKind::TabClose, vec!["warpctrl", "tab", "close"]),
(
ActionKind::TabRename,
vec!["warpctrl", "tab", "rename", "docs"],
),
(
ActionKind::TabResetName,
vec!["warpctrl", "tab", "reset-name"],
),
(
ActionKind::TabColorSet,
vec!["warpctrl", "tab", "color", "set", "red"],
),
(
ActionKind::TabColorClear,
vec!["warpctrl", "tab", "color", "clear"],
),
(ActionKind::PaneList, vec!["warpctrl", "pane", "list"]),
(ActionKind::PaneInspect, vec!["warpctrl", "pane", "inspect"]),
(
ActionKind::PaneSplit,
vec!["warpctrl", "pane", "split", "--direction", "right"],
),
(ActionKind::PaneFocus, vec!["warpctrl", "pane", "focus"]),
(
ActionKind::PaneNavigate,
vec!["warpctrl", "pane", "navigate", "--direction", "next"],
),
(
ActionKind::PaneResize,
vec![
"warpctrl",
"pane",
"resize",
"--direction",
"right",
"--amount",
"4",
],
),
(
ActionKind::PaneMaximize,
vec!["warpctrl", "pane", "maximize"],
),
(
ActionKind::PaneUnmaximize,
vec!["warpctrl", "pane", "unmaximize"],
),
(ActionKind::PaneClose, vec!["warpctrl", "pane", "close"]),
(
ActionKind::PaneRename,
vec!["warpctrl", "pane", "rename", "server"],
),
(
ActionKind::PaneResetName,
vec!["warpctrl", "pane", "reset-name"],
),
(ActionKind::SessionList, vec!["warpctrl", "session", "list"]),
(
ActionKind::SessionInspect,
vec!["warpctrl", "session", "inspect"],
),
(
ActionKind::SessionActivate,
vec!["warpctrl", "session", "activate"],
),
(
ActionKind::SessionPrevious,
vec!["warpctrl", "session", "previous"],
),
(ActionKind::SessionNext, vec!["warpctrl", "session", "next"]),
(
ActionKind::SessionReopenClosed,
vec!["warpctrl", "session", "reopen-closed"],
),
(
ActionKind::InputInsert,
vec!["warpctrl", "input", "insert", "hello"],
),
(
ActionKind::InputReplace,
vec!["warpctrl", "input", "replace", "hello"],
),
(ActionKind::ThemeList, vec!["warpctrl", "theme", "list"]),
(ActionKind::ThemeGet, vec!["warpctrl", "theme", "get"]),
(
ActionKind::ThemeSet,
vec!["warpctrl", "theme", "set", "Dracula"],
),
(
ActionKind::ThemeSystemSet,
vec!["warpctrl", "theme", "system-set", "true"],
),
(
ActionKind::ThemeLightSet,
vec!["warpctrl", "theme", "light-set", "Light"],
),
(
ActionKind::ThemeDarkSet,
vec!["warpctrl", "theme", "dark-set", "Dark"],
),
(
ActionKind::AppearanceGet,
vec!["warpctrl", "appearance", "get"],
),
(
ActionKind::AppearanceFontSizeIncrease,
vec!["warpctrl", "appearance", "font-size-increase"],
),
(
ActionKind::AppearanceFontSizeDecrease,
vec!["warpctrl", "appearance", "font-size-decrease"],
),
(
ActionKind::AppearanceFontSizeReset,
vec!["warpctrl", "appearance", "font-size-reset"],
),
(
ActionKind::AppearanceZoomIncrease,
vec!["warpctrl", "appearance", "zoom-increase"],
),
(
ActionKind::AppearanceZoomDecrease,
vec!["warpctrl", "appearance", "zoom-decrease"],
),
(
ActionKind::AppearanceZoomReset,
vec!["warpctrl", "appearance", "zoom-reset"],
),
(ActionKind::SettingList, vec!["warpctrl", "setting", "list"]),
(
ActionKind::SettingGet,
vec!["warpctrl", "setting", "get", "font_size"],
),
(
ActionKind::SettingSet,
vec!["warpctrl", "setting", "set", "font_size", "14"],
),
(
ActionKind::SettingToggle,
vec!["warpctrl", "setting", "toggle", "autosuggestions"],
),
(
ActionKind::KeybindingList,
vec!["warpctrl", "keybinding", "list"],
),
(
ActionKind::KeybindingGet,
vec!["warpctrl", "keybinding", "get", "copy"],
),
(ActionKind::ActionList, vec!["warpctrl", "action", "list"]),
(
ActionKind::ActionInspect,
vec!["warpctrl", "action", "inspect", "tab.create"],
),
(ActionKind::SurfaceList, vec!["warpctrl", "surface", "list"]),
(
ActionKind::SurfaceSettingsOpen,
vec!["warpctrl", "surface", "settings", "open"],
),
(
ActionKind::SurfaceCommandPaletteOpen,
vec!["warpctrl", "surface", "command-palette", "open"],
),
(
ActionKind::SurfaceCommandSearchOpen,
vec!["warpctrl", "surface", "command-search", "open"],
),
(
ActionKind::SurfaceThemePickerOpen,
vec!["warpctrl", "surface", "theme-picker", "open"],
),
(
ActionKind::SurfaceKeybindingsOpen,
vec!["warpctrl", "surface", "keybindings", "open"],
),
(
ActionKind::SurfaceWarpDriveOpen,
vec!["warpctrl", "surface", "warp-drive", "open"],
),
(
ActionKind::SurfaceWarpDriveToggle,
vec!["warpctrl", "surface", "warp-drive", "toggle"],
),
(
ActionKind::SurfaceResourceCenterToggle,
vec!["warpctrl", "surface", "resource-center", "toggle"],
),
(
ActionKind::SurfaceAiAssistantToggle,
vec!["warpctrl", "surface", "ai-assistant", "toggle"],
),
(
ActionKind::SurfaceCodeReviewOpen,
vec!["warpctrl", "surface", "code-review", "open"],
),
(
ActionKind::SurfaceCodeReviewToggle,
vec!["warpctrl", "surface", "code-review", "toggle"],
),
(
ActionKind::SurfaceProjectExplorerOpen,
vec!["warpctrl", "surface", "project-explorer", "open"],
),
(
ActionKind::SurfaceGlobalSearchOpen,
vec!["warpctrl", "surface", "global-search", "open"],
),
(
ActionKind::SurfaceConversationListOpen,
vec!["warpctrl", "surface", "conversation-list", "open"],
),
(
ActionKind::SurfaceLeftPanelToggle,
vec!["warpctrl", "surface", "left-panel", "toggle"],
),
(
ActionKind::SurfaceRightPanelToggle,
vec!["warpctrl", "surface", "right-panel", "toggle"],
),
(
ActionKind::SurfaceVerticalTabsOpen,
vec!["warpctrl", "surface", "vertical-tabs", "open"],
),
(
ActionKind::SurfaceVerticalTabsToggle,
vec!["warpctrl", "surface", "vertical-tabs", "toggle"],
),
(
ActionKind::SurfaceAgentManagementOpen,
vec!["warpctrl", "surface", "agent-management", "open"],
),
(
ActionKind::FileOpen,
vec!["warpctrl", "file", "open", "/tmp/example.txt"],
),
]
}
fn parsed_action_kind(command: &ControlCommand) -> Option<ActionKind> {
match command {
ControlCommand::Instance(command) => match command {
InstanceCommand::List => Some(ActionKind::InstanceList),
InstanceCommand::Inspect(_) => Some(ActionKind::InstanceInspect),
},
ControlCommand::App(command) => match command {
AppCommand::Ping(_) => Some(ActionKind::AppPing),
AppCommand::Version(_) => Some(ActionKind::AppVersion),
AppCommand::Active(_) => Some(ActionKind::AppActive),
AppCommand::Focus(_) => Some(ActionKind::AppFocus),
},
ControlCommand::Capability(command) => match command {
CapabilityCommand::List => Some(ActionKind::CapabilityList),
CapabilityCommand::Inspect { .. } => Some(ActionKind::CapabilityInspect),
},
ControlCommand::Action(command) => match command {
ActionCatalogCommand::List => Some(ActionKind::ActionList),
ActionCatalogCommand::Inspect { .. } => Some(ActionKind::ActionInspect),
},
ControlCommand::Window(command) => match command {
WindowCommand::List(_) => Some(ActionKind::WindowList),
WindowCommand::Inspect(_) => Some(ActionKind::WindowInspect),
WindowCommand::Create(_) => Some(ActionKind::WindowCreate),
WindowCommand::Focus(_) => Some(ActionKind::WindowFocus),
WindowCommand::Close(_) => Some(ActionKind::WindowClose),
},
ControlCommand::Tab(command) => match command {
TabCommand::List(_) => Some(ActionKind::TabList),
TabCommand::Inspect(_) => Some(ActionKind::TabInspect),
TabCommand::Create(_) => Some(ActionKind::TabCreate),
TabCommand::Activate(_) => Some(ActionKind::TabActivate),
TabCommand::Move(_) => Some(ActionKind::TabMove),
TabCommand::Close(_) => Some(ActionKind::TabClose),
TabCommand::Rename(_) => Some(ActionKind::TabRename),
TabCommand::ResetName(_) => Some(ActionKind::TabResetName),
TabCommand::Color(command) => match command {
TabColorCommand::Set(_) => Some(ActionKind::TabColorSet),
TabColorCommand::Clear(_) => Some(ActionKind::TabColorClear),
},
},
ControlCommand::Pane(command) => match command {
PaneCommand::List(_) => Some(ActionKind::PaneList),
PaneCommand::Inspect(_) => Some(ActionKind::PaneInspect),
PaneCommand::Split(_) => Some(ActionKind::PaneSplit),
PaneCommand::Focus(_) => Some(ActionKind::PaneFocus),
PaneCommand::Navigate(_) => Some(ActionKind::PaneNavigate),
PaneCommand::Resize(_) => Some(ActionKind::PaneResize),
PaneCommand::Maximize(_) => Some(ActionKind::PaneMaximize),
PaneCommand::Unmaximize(_) => Some(ActionKind::PaneUnmaximize),
PaneCommand::Close(_) => Some(ActionKind::PaneClose),
PaneCommand::Rename(_) => Some(ActionKind::PaneRename),
PaneCommand::ResetName(_) => Some(ActionKind::PaneResetName),
},
ControlCommand::Session(command) => match command {
SessionCommand::List(_) => Some(ActionKind::SessionList),
SessionCommand::Inspect(_) => Some(ActionKind::SessionInspect),
SessionCommand::Activate(_) => Some(ActionKind::SessionActivate),
SessionCommand::Previous(_) => Some(ActionKind::SessionPrevious),
SessionCommand::Next(_) => Some(ActionKind::SessionNext),
SessionCommand::ReopenClosed(_) => Some(ActionKind::SessionReopenClosed),
},
ControlCommand::Input(command) => match command {
InputCommand::Insert(_) => Some(ActionKind::InputInsert),
InputCommand::Replace(_) => Some(ActionKind::InputReplace),
},
ControlCommand::Theme(command) => match command {
ThemeCommand::List(_) => Some(ActionKind::ThemeList),
ThemeCommand::Get(_) => Some(ActionKind::ThemeGet),
ThemeCommand::Set(_) => Some(ActionKind::ThemeSet),
ThemeCommand::SystemSet(_) => Some(ActionKind::ThemeSystemSet),
ThemeCommand::LightSet(_) => Some(ActionKind::ThemeLightSet),
ThemeCommand::DarkSet(_) => Some(ActionKind::ThemeDarkSet),
},
ControlCommand::Appearance(command) => match command {
AppearanceCommand::Get(_) => Some(ActionKind::AppearanceGet),
AppearanceCommand::FontSizeIncrease(_) => Some(ActionKind::AppearanceFontSizeIncrease),
AppearanceCommand::FontSizeDecrease(_) => Some(ActionKind::AppearanceFontSizeDecrease),
AppearanceCommand::FontSizeReset(_) => Some(ActionKind::AppearanceFontSizeReset),
AppearanceCommand::ZoomIncrease(_) => Some(ActionKind::AppearanceZoomIncrease),
AppearanceCommand::ZoomDecrease(_) => Some(ActionKind::AppearanceZoomDecrease),
AppearanceCommand::ZoomReset(_) => Some(ActionKind::AppearanceZoomReset),
},
ControlCommand::Setting(command) => match command {
SettingCommand::List(_) => Some(ActionKind::SettingList),
SettingCommand::Get(_) => Some(ActionKind::SettingGet),
SettingCommand::Set(_) => Some(ActionKind::SettingSet),
SettingCommand::Toggle(_) => Some(ActionKind::SettingToggle),
},
ControlCommand::Keybinding(command) => match command {
KeybindingCommand::List(_) => Some(ActionKind::KeybindingList),
KeybindingCommand::Get(_) => Some(ActionKind::KeybindingGet),
},
ControlCommand::File(command) => match command {
FileCommand::Open(_) => Some(ActionKind::FileOpen),
},
ControlCommand::Surface(command) => match command {
SurfaceCommand::List(_) => Some(ActionKind::SurfaceList),
SurfaceCommand::Settings(command) => match command {
SurfaceSettingsCommand::Open(_) => Some(ActionKind::SurfaceSettingsOpen),
},
SurfaceCommand::CommandPalette(command) => match command {
SurfaceQueryCommand::Open(_) => Some(ActionKind::SurfaceCommandPaletteOpen),
},
SurfaceCommand::CommandSearch(command) => match command {
SurfaceQueryCommand::Open(_) => Some(ActionKind::SurfaceCommandSearchOpen),
},
SurfaceCommand::ThemePicker(command) => match command {
SurfaceOpenCommand::Open(_) => Some(ActionKind::SurfaceThemePickerOpen),
},
SurfaceCommand::Keybindings(command) => match command {
SurfaceOpenCommand::Open(_) => Some(ActionKind::SurfaceKeybindingsOpen),
},
SurfaceCommand::WarpDrive(command) => match command {
SurfaceOpenToggleCommand::Open(_) => Some(ActionKind::SurfaceWarpDriveOpen),
SurfaceOpenToggleCommand::Toggle(_) => Some(ActionKind::SurfaceWarpDriveToggle),
},
SurfaceCommand::ResourceCenter(command) => match command {
SurfaceToggleCommand::Toggle(_) => Some(ActionKind::SurfaceResourceCenterToggle),
},
SurfaceCommand::AiAssistant(command) => match command {
SurfaceToggleCommand::Toggle(_) => Some(ActionKind::SurfaceAiAssistantToggle),
},
SurfaceCommand::CodeReview(command) => match command {
SurfaceOpenToggleCommand::Open(_) => Some(ActionKind::SurfaceCodeReviewOpen),
SurfaceOpenToggleCommand::Toggle(_) => Some(ActionKind::SurfaceCodeReviewToggle),
},
SurfaceCommand::ProjectExplorer(command) => match command {
SurfaceOpenCommand::Open(_) => Some(ActionKind::SurfaceProjectExplorerOpen),
},
SurfaceCommand::GlobalSearch(command) => match command {
SurfaceOpenCommand::Open(_) => Some(ActionKind::SurfaceGlobalSearchOpen),
},
SurfaceCommand::ConversationList(command) => match command {
SurfaceOpenCommand::Open(_) => Some(ActionKind::SurfaceConversationListOpen),
},
SurfaceCommand::LeftPanel(command) => match command {
SurfaceToggleCommand::Toggle(_) => Some(ActionKind::SurfaceLeftPanelToggle),
},
SurfaceCommand::RightPanel(command) => match command {
SurfaceToggleCommand::Toggle(_) => Some(ActionKind::SurfaceRightPanelToggle),
},
SurfaceCommand::VerticalTabs(command) => match command {
SurfaceOpenToggleCommand::Open(_) => Some(ActionKind::SurfaceVerticalTabsOpen),
SurfaceOpenToggleCommand::Toggle(_) => Some(ActionKind::SurfaceVerticalTabsToggle),
},
SurfaceCommand::AgentManagement(command) => match command {
SurfaceOpenCommand::Open(_) => Some(ActionKind::SurfaceAgentManagementOpen),
},
},
ControlCommand::Completions { .. } => None,
}
}
+8
View File
@@ -11,6 +11,14 @@ pub enum MCPCommand {
List,
}
impl MCPCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
MCPCommand::List => "mcp list",
}
}
}
/// Represents an MCP server specification from CLI input.
///
/// This is a lightweight representation - full parsing happens in the app layer
+4 -2
View File
@@ -1,7 +1,9 @@
use super::*;
use clap::builder::TypedValueParser;
use std::ffi::OsStr;
use clap::builder::TypedValueParser;
use super::*;
fn parse_mcp_spec(value: &str) -> Result<MCPSpec, clap::Error> {
let cmd = clap::Command::new("test");
let parser = MCPSpecParser;
+145
View File
@@ -0,0 +1,145 @@
use clap::{Args, Subcommand};
/// Memory store related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum MemoryStoreCommand {
/// List memory stores.
List,
/// Get details of a single memory store.
#[command(name = "get", alias = "get-store")]
Get(GetStoreArgs),
/// Update a memory store's description.
#[command(name = "update", alias = "update-store", visible_alias = "edit-store")]
Update(UpdateStoreArgs),
/// List agents attached to a memory store.
#[command(name = "list-store-agents", visible_alias = "store-agents")]
ListStoreAgents(ListStoreAgentsArgs),
}
/// Memory related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum MemoryCommand {
/// List memories in a memory store.
List(ListMemoriesArgs),
/// Create a manual memory in a memory store.
#[command(visible_alias = "add")]
Create(CreateMemoryArgs),
/// Update a memory in a memory store, creating a new version.
#[command(visible_alias = "edit")]
Update(UpdateMemoryArgs),
/// Delete a memory from a memory store.
#[command(visible_alias = "remove")]
Delete(DeleteMemoryArgs),
/// List version history for a memory.
Versions(ListVersionsArgs),
}
#[derive(Debug, Clone, Args)]
pub struct ListMemoriesArgs {
/// UID of the memory store.
pub store_uid: String,
}
#[derive(Debug, Clone, Args)]
pub struct CreateMemoryArgs {
/// UID of the memory store.
pub store_uid: String,
/// Memory content.
#[arg(long = "content", short = 'c')]
pub content: String,
/// Reason for creating this memory.
#[arg(long = "reason", short = 'r')]
pub reason: String,
/// Optional version string for this memory.
#[arg(long = "version")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct UpdateMemoryArgs {
/// UID of the memory to update.
pub memory_uid: String,
/// UID of the memory store that contains this memory.
#[arg(long = "store", short = 's')]
pub store_uid: String,
/// Updated memory content.
#[arg(long = "content", short = 'c')]
pub content: String,
/// Reason for updating this memory.
#[arg(long = "reason", short = 'r')]
pub reason: String,
/// Optional version label for this update. Server picks a UUID when omitted.
#[arg(long = "version")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct DeleteMemoryArgs {
/// UID of the memory to delete.
pub memory_uid: String,
/// UID of the memory store that contains this memory.
#[arg(long = "store", short = 's')]
pub store_uid: String,
}
#[derive(Debug, Clone, Args)]
pub struct GetStoreArgs {
/// UID of the memory store.
pub store_uid: String,
}
#[derive(Debug, Clone, Args)]
pub struct UpdateStoreArgs {
/// UID of the memory store.
pub store_uid: String,
/// Updated description for the memory store. Pass an empty string to clear.
#[arg(long = "description", short = 'd')]
pub description: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct ListStoreAgentsArgs {
/// UID of the memory store.
pub store_uid: String,
}
#[derive(Debug, Clone, Args)]
pub struct ListVersionsArgs {
/// UID of the memory to inspect.
pub memory_uid: String,
/// UID of the memory store that contains this memory.
#[arg(long = "store", short = 's')]
pub store_uid: String,
}
impl MemoryStoreCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
MemoryStoreCommand::List => "memory-store list",
MemoryStoreCommand::Get(_) => "memory-store get",
MemoryStoreCommand::Update(_) => "memory-store update",
MemoryStoreCommand::ListStoreAgents(_) => "memory-store list-store-agents",
}
}
}
impl MemoryCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
MemoryCommand::List(_) => "memory list",
MemoryCommand::Create(_) => "memory create",
MemoryCommand::Update(_) => "memory update",
MemoryCommand::Delete(_) => "memory delete",
MemoryCommand::Versions(_) => "memory versions",
}
}
}
+8
View File
@@ -7,6 +7,14 @@ pub enum ModelCommand {
List,
}
impl ModelCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
ModelCommand::List => "model list",
}
}
}
/// Shared CLI args for selecting a base model.
#[derive(Debug, Clone, Args, Default)]
pub struct ModelArgs {
+2 -1
View File
@@ -1,4 +1,5 @@
use std::{ffi::c_void, str::FromStr};
use std::ffi::c_void;
use std::str::FromStr;
use windows::Win32::Foundation::HANDLE;
+9
View File
@@ -7,6 +7,15 @@ pub enum ProviderCommand {
List,
}
impl ProviderCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
ProviderCommand::Setup(_) => "provider setup",
ProviderCommand::List => "provider list",
}
}
}
// If we want these at the top level, we can also set provider as a top level subcommand:
#[derive(Debug, Clone, ValueEnum)]
#[value(rename_all = "snake_case")]
+19 -10
View File
@@ -1,13 +1,11 @@
use clap::{Args, Subcommand};
use crate::{
config_file::ConfigFileArgs,
environment::{EnvironmentCreateArgs, EnvironmentUpdateArgs},
mcp::MCPSpec,
model::ModelArgs,
scope::ObjectScope,
skill::SkillSpec,
};
use crate::config_file::ConfigFileArgs;
use crate::environment::{EnvironmentCreateArgs, EnvironmentUpdateArgs};
use crate::mcp::MCPSpec;
use crate::model::ModelArgs;
use crate::scope::ObjectScope;
use crate::skill::SkillSpec;
/// `ScheduleCommand` has a slightly unusual definition because we allow `oz schedule` as
// a shorthand for `oz schedule create`.
@@ -22,6 +20,17 @@ pub struct ScheduleCommand {
}
impl ScheduleCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self.subcommand() {
Some(ScheduleSubcommand::Create(_)) | None => "schedule create",
Some(ScheduleSubcommand::List) => "schedule list",
Some(ScheduleSubcommand::Get(_)) => "schedule get",
Some(ScheduleSubcommand::Update(_)) => "schedule update",
Some(ScheduleSubcommand::Pause(_)) => "schedule pause",
Some(ScheduleSubcommand::Unpause(_)) => "schedule unpause",
Some(ScheduleSubcommand::Delete(_)) => "schedule delete",
}
}
/// Get the specific scheduling subcommand. Returns `None` if using the `oz schedule` creation shorthand.
pub fn subcommand(&self) -> Option<&ScheduleSubcommand> {
self.subcommand.as_ref()
@@ -116,7 +125,7 @@ pub struct CreateScheduleArgs {
///
/// When used with --prompt, the skill provides the base context and the prompt is the user task.
/// This is useful for running recurring workflows like code reviews, dependency updates, or reports.
#[arg(long = "skill", value_name = "SPEC")]
#[arg(long = "skill", value_name = "SKILL")]
pub skill: Option<SkillSpec>,
/// Where this job should be hosted.
@@ -188,7 +197,7 @@ pub struct UpdateScheduleArgs {
///
/// Skills are searched in `.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, and `.codex/skills/` directories.
/// The skill is resolved at runtime in the agent's cloud environment.
#[arg(long = "skill", value_name = "SPEC", conflicts_with = "remove_skill")]
#[arg(long = "skill", value_name = "SKILL", conflicts_with = "remove_skill")]
pub skill: Option<SkillSpec>,
/// Remove the skill from this scheduled agent.
+53 -2
View File
@@ -1,4 +1,5 @@
use std::{fmt, path::PathBuf};
use std::fmt;
use std::path::PathBuf;
use clap::{Args, Subcommand, ValueEnum};
@@ -9,7 +10,8 @@ use crate::scope::ObjectScope;
pub enum SecretCommand {
/// Create a new secret.
///
/// Use `oz secret create anthropic api-key <NAME>` to create a Claude/Anthropic auth secret.
/// Use `oz secret create claude api-key <NAME>` to create a Claude/Anthropic auth secret,
/// or `oz secret create codex api-key <NAME>` to create a Codex/OpenAI auth secret.
Create(CreateSecretArgs),
/// Delete a secret.
Delete(DeleteSecretArgs),
@@ -22,6 +24,17 @@ pub enum SecretCommand {
List(ListSecretsArgs),
}
impl SecretCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
SecretCommand::Create(_) => "secret create",
SecretCommand::Delete(_) => "secret delete",
SecretCommand::Update(_) => "secret update",
SecretCommand::List(_) => "secret list",
}
}
}
#[derive(Debug, Clone, Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct CreateSecretArgs {
@@ -51,7 +64,10 @@ pub struct CreateSecretArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum CreateProvider {
/// Create a Claude/Anthropic auth secret.
#[command(name = "claude")]
Anthropic(AnthropicCreateArgs),
/// Create a Codex/OpenAI auth secret.
Codex(CodexCreateArgs),
}
#[derive(Debug, Clone, Args)]
@@ -113,6 +129,37 @@ pub struct BedrockApiKeyArgs {
pub region: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct CodexCreateArgs {
#[command(subcommand)]
pub method: CodexMethod,
}
/// Codex credential type.
#[derive(Debug, Clone, Subcommand)]
pub enum CodexMethod {
/// Direct OpenAI API key.
#[command(name = "api-key")]
ApiKey(OpenAiApiKeyArgs),
}
/// Arguments for creating an OpenAI API key secret used by the Codex harness.
#[derive(Debug, Clone, Args)]
pub struct OpenAiApiKeyArgs {
#[clap(flatten)]
pub common: CommonSecretCreateArgs,
#[clap(flatten)]
pub value: ValueArgs,
/// Optional base URL for the OpenAI API (e.g. a regional endpoint like
/// `https://us.api.openai.com/v1`). When omitted in interactive mode the
/// CLI prompts for it; pressing Enter at the prompt skips it. When omitted
/// in non-interactive mode the harness uses the provider's default endpoint.
#[arg(long = "base-url")]
pub base_url: Option<String>,
}
/// Arguments for creating an Anthropic Bedrock access key secret.
#[derive(Debug, Clone, Args)]
pub struct BedrockAccessKeyArgs {
@@ -190,6 +237,9 @@ pub enum SecretType {
// Not exposed via the CLI `--type` flag; constructed internally for provider subcommands.
#[value(skip)]
AnthropicBedrockApiKey,
// Not exposed via the CLI `--type` flag; constructed internally for provider subcommands.
#[value(skip)]
OpenaiApiKey,
}
impl fmt::Display for SecretType {
@@ -198,6 +248,7 @@ impl fmt::Display for SecretType {
SecretType::RawValue => write!(f, "raw-value"),
SecretType::AnthropicApiKey => write!(f, "anthropic-api-key"),
SecretType::AnthropicBedrockApiKey => write!(f, "anthropic-bedrock-api-key"),
SecretType::OpenaiApiKey => write!(f, "openai-api-key"),
}
}
}
+4 -2
View File
@@ -1,7 +1,9 @@
use super::*;
use clap::builder::TypedValueParser;
use std::ffi::OsStr;
use clap::builder::TypedValueParser;
use super::*;
fn parse_share_request(value: &str) -> Result<ShareRequest, clap::Error> {
let cmd = clap::Command::new("test");
let parser = ShareRequestParser;
+2 -1
View File
@@ -1,5 +1,6 @@
use std::fmt;
use std::path::Path;
use std::{fmt, str::FromStr};
use std::str::FromStr;
/// A skill specifier that can reference a skill in a specific repo or search the current directory.
///
+10
View File
@@ -0,0 +1,10 @@
use clap::ValueEnum;
/// Sort-order values accepted by `--sort-order`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SortOrderArg {
#[value(name = "asc")]
Asc,
#[value(name = "desc")]
Desc,
}
+16 -19
View File
@@ -1,6 +1,8 @@
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand, ValueEnum};
use crate::SortOrderArg;
use crate::date_time::parse_rfc3339;
use crate::json_filter::JsonOutput;
/// Task-related subcommands.
@@ -18,6 +20,17 @@ pub enum TaskCommand {
Message(MessageCommand),
}
impl TaskCommand {
pub(crate) fn as_str_for_tracing(&self) -> &'static str {
match self {
TaskCommand::List(_) => "run list",
TaskCommand::Get(_) => "run get",
TaskCommand::Conversation(_) => "run conversation",
TaskCommand::Message(_) => "run message",
}
}
}
/// Conversation-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ConversationCommand {
@@ -141,8 +154,8 @@ pub struct ListTasksArgs {
#[arg(long = "environment", value_name = "ENV_ID")]
pub environment: Option<String>,
/// Filter by skill specification (e.g. `owner/repo:path/to/SKILL.md`).
#[arg(long = "skill", value_name = "SPEC")]
/// Filter by skill (e.g. `owner/repo:path/to/SKILL.md`).
#[arg(long = "skill", value_name = "SKILL")]
pub skill: Option<String>,
/// Filter to runs created by a specific scheduled agent.
@@ -187,7 +200,7 @@ pub struct ListTasksArgs {
/// Sort direction.
#[arg(long = "sort-order", value_enum, value_name = "DIR")]
pub sort_order: Option<RunSortOrderArg>,
pub sort_order: Option<SortOrderArg>,
/// Opaque pagination cursor from a previous list response.
///
@@ -201,13 +214,6 @@ pub struct ListTasksArgs {
pub json_output: JsonOutput,
}
/// Parse an RFC 3339 timestamp into a UTC `DateTime`.
fn parse_rfc3339(s: &str) -> Result<DateTime<Utc>, String> {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| format!("invalid RFC 3339 timestamp '{s}': {e}"))
}
/// Run state values accepted by `--state`. Repeatable; multiple values match any of them.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum RunStateArg {
@@ -289,15 +295,6 @@ pub enum RunSortByArg {
Agent,
}
/// Sort-order values accepted by `--sort-order`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum RunSortOrderArg {
#[value(name = "asc")]
Asc,
#[value(name = "desc")]
Desc,
}
#[derive(Debug, Clone, Args)]
pub struct TaskGetArgs {
/// The task ID to get status for.
+1 -1
View File
@@ -123,7 +123,7 @@ fn all_filter_flags_parse() {
);
assert_eq!(args.query.as_deref(), Some("oz run"));
assert_eq!(args.sort_by, Some(RunSortByArg::CreatedAt));
assert_eq!(args.sort_order, Some(RunSortOrderArg::Asc));
assert_eq!(args.sort_order, Some(SortOrderArg::Asc));
assert_eq!(args.cursor.as_deref(), Some("abcd=="));
}