using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Unity.Pipeline.Models
{
///
/// Request model for /api/exec endpoint.
/// Contains command name and parameters for remote command execution.
///
[Serializable]
class CommandExecutionRequest
{
///
/// Name of the command to execute.
/// Must match a registered [CliCommand] name.
///
/// Optional, because and carry the name in
/// their first token instead; the server assigns it here before anything downstream reads
/// it. Exactly one of the three forms may be present - enforces
/// that.
///
[JsonProperty("command")]
public string Command { get; set; }
///
/// An unparsed command line: the server tokenizes it (see
/// ) and then binds it. For clients that genuinely
/// hold raw text — MCP, a chat box, a pasted line. Mutually exclusive with
/// and .
///
[JsonProperty("commandLine")]
public string CommandLine { get; set; }
///
/// Pre-split tokens, first of which is the command name. Preferred over
/// by any caller that already has argv — on POSIX a CLI never
/// holds the original command STRING, so making it re-quote tokens for the server to
/// re-split would be a lossy round-trip across two independently-versioned quoting
/// dialects. Mutually exclusive with and .
///
[JsonProperty("argv")]
public List Argv { get; set; }
///
/// True when this request arrived in a raw form, so the reply should echo the bound
/// parameters back. [JsonIgnore] is MANDATORY — this type is serialized
/// (ExecResponseSerializer), and an internal flag must never reach the wire.
///
[JsonIgnore]
public bool IsRawForm => CommandLine != null || Argv != null;
///
/// Parameters for the command execution.
/// Contains key-value pairs matching command parameter names.
///
[JsonProperty("parameters")]
public JObject Parameters { get; set; }
///
/// Optional timeout for command execution in milliseconds.
/// Default: 60000 (60 seconds)
///
[JsonProperty("timeout")]
public int? Timeout { get; set; }
///
/// Opt into the full response envelope. When false (default) the server returns a lean,
/// token-efficient reply — compact JSON, the envelope's own null keys omitted, and the
/// always-on metadata (executedAt, command, executionTimeMs) dropped; the command's
/// result payload keeps explicit nulls (see ). Set true to get
/// every envelope field back for debugging/correlation. (AUTHAPI-21)
///
[JsonProperty("verbose")]
public bool Verbose { get; set; }
///
/// When true, drop null keys from the whole reply INCLUDING the command's result payload.
/// Off by default: an absent payload key would otherwise be indistinguishable from a
/// nonexistent or misspelled one. Opt in when the result schema is already known and the
/// nulls are pure bytes — e.g. bulk list reads where a null column repeats per item.
/// Orthogonal to (envelope metadata). (AUTHAPI-21)
///
[JsonProperty("omitNulls")]
public bool OmitNulls { get; set; }
///
/// When true, run the command as a detached job (CLI-335): the response returns a job
/// id immediately and the client collects the result later via GET /api/job?id=…,
/// surviving its own HTTP timeout. Execution still queues through the server's
/// one-command-at-a-time exec gate.
///
[JsonProperty("job")]
public bool Job { get; set; }
///
/// Create a new command execution request.
///
public CommandExecutionRequest()
{
Parameters = new JObject();
}
///
/// Create a command execution request with specified command and parameters.
///
/// Name of the command to execute.
/// Parameters for the command execution.
public CommandExecutionRequest(string command, JObject parameters = null)
{
Command = command ?? throw new ArgumentNullException(nameof(command));
Parameters = parameters ?? new JObject();
}
///
/// Get a parameter value as the specified type.
/// Returns the default value if the parameter is not found or cannot be converted.
///
/// The type to convert the parameter value to.
/// Parameter name.
/// Value to return if the parameter is missing or unconvertible.
/// The parameter's value, or .
public T GetParameter(string name, T defaultValue = default(T))
{
if (Parameters == null || !Parameters.ContainsKey(name))
return defaultValue;
try
{
return Parameters[name].ToObject();
}
catch
{
return defaultValue;
}
}
///
/// Check if a parameter exists in the request.
///
/// Parameter name.
/// True if the parameter is present.
public bool HasParameter(string name)
{
return Parameters != null && Parameters.ContainsKey(name);
}
///
/// Validate the request structure.
///
/// An error message, or null if the request is valid.
public string Validate()
{
// Exactly one of the three request forms. Counting them first keeps every
// combination -- including all three at once -- on one message.
var forms = 0;
if (Command != null) forms++;
if (CommandLine != null) forms++;
if (Argv != null) forms++;
if (forms > 1)
return "Specify exactly one of 'command', 'commandLine' or 'argv'";
if (IsRawForm)
{
// Rejected, not ignored: a silently-dropped payload is indistinguishable from
// one that was never sent, which is the ambiguity the lean envelope already
// legislates against.
if (Parameters != null && Parameters.Count > 0)
return "'parameters' cannot be combined with 'commandLine' or 'argv'; the arguments belong in the command line itself";
if (Argv != null)
{
if (Argv.Count == 0)
return "'argv' must contain at least the command name";
for (var i = 0; i < Argv.Count; i++)
if (Argv[i] == null)
return "'argv' must not contain null elements";
if (string.IsNullOrWhiteSpace(Argv[0]))
return "'argv' must start with the command name";
}
else if (string.IsNullOrWhiteSpace(CommandLine))
{
return "'commandLine' must not be empty";
}
}
else if (string.IsNullOrWhiteSpace(Command))
{
return "Command name is required";
}
if (Timeout.HasValue && Timeout.Value <= 0)
return "Timeout must be a positive number";
return null; // No validation errors
}
}
}