first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -12,6 +12,8 @@ clap = { workspace = true, features = ["derive", "env"] }
|
||||
cfg-if = { workspace = true }
|
||||
humantime.workspace = true
|
||||
jaq-all.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
uuid = { workspace = true }
|
||||
galaxy_core = { path = "../galaxy_core" }
|
||||
@@ -19,6 +21,7 @@ color-print = "0.3"
|
||||
galaxy_util = { path = "../galaxy_util" }
|
||||
clap_complete = "4.5.58"
|
||||
anyhow.workspace = true
|
||||
local_control.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
@@ -33,5 +36,6 @@ plugin_host = []
|
||||
integration_tests = []
|
||||
api_key_authentication = []
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "0.8.0"
|
||||
|
||||
+324
-23
@@ -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`
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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<()> {
|
||||
|
||||
@@ -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}"))
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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=="));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user