feat: route product commands through backend targets
This commit is contained in:
@@ -15,6 +15,7 @@ client = { workspace = true }
|
||||
memory = { workspace = true }
|
||||
manifest = { workspace = true }
|
||||
worker = { workspace = true }
|
||||
workspace-api = { workspace = true }
|
||||
session-store = { workspace = true }
|
||||
session-analytics = { workspace = true }
|
||||
ticket = { workspace = true }
|
||||
|
||||
@@ -55,14 +55,14 @@ impl CliCommand {
|
||||
CliCommand::DefaultTui
|
||||
| CliCommand::Workers
|
||||
| CliCommand::Resume
|
||||
| CliCommand::Panel => CliConnectionRequirement::ConnectionAware,
|
||||
| CliCommand::Panel
|
||||
| CliCommand::Ticket
|
||||
| CliCommand::Objective => CliConnectionRequirement::ConnectionAware,
|
||||
CliCommand::Login => CliConnectionRequirement::BackendOnly,
|
||||
CliCommand::Keys
|
||||
| CliCommand::SetupModel
|
||||
| CliCommand::WorkerRuntime
|
||||
| CliCommand::WorkerCleanup
|
||||
| CliCommand::Ticket
|
||||
| CliCommand::Objective
|
||||
| CliCommand::Plugin
|
||||
| CliCommand::Mcp
|
||||
| CliCommand::MemoryLint
|
||||
@@ -259,6 +259,8 @@ mod tests {
|
||||
CliCommand::Workers,
|
||||
CliCommand::Resume,
|
||||
CliCommand::Panel,
|
||||
CliCommand::Ticket,
|
||||
CliCommand::Objective,
|
||||
] {
|
||||
assert_eq!(
|
||||
command.connection_requirement(),
|
||||
@@ -277,8 +279,6 @@ mod tests {
|
||||
CliCommand::SetupModel,
|
||||
CliCommand::WorkerRuntime,
|
||||
CliCommand::WorkerCleanup,
|
||||
CliCommand::Ticket,
|
||||
CliCommand::Objective,
|
||||
CliCommand::Plugin,
|
||||
CliCommand::Mcp,
|
||||
CliCommand::MemoryLint,
|
||||
|
||||
+127
-34
@@ -34,10 +34,16 @@ enum Mode {
|
||||
MemoryLint(LintCliOptions),
|
||||
Mcp(mcp_cli::McpCliCommand),
|
||||
Plugin(plugin_cli::PluginCliCommand),
|
||||
Objective(objective_cli::ObjectiveCli),
|
||||
Objective {
|
||||
cli: objective_cli::ObjectiveCli,
|
||||
target: client::ResolvedTarget,
|
||||
},
|
||||
Session(session_cli::SessionCli),
|
||||
WorkerCleanup(worker_cleanup_cli::WorkerCleanupCli),
|
||||
Ticket(ticket_cli::TicketCli),
|
||||
Ticket {
|
||||
cli: ticket_cli::TicketCli,
|
||||
target: client::ResolvedTarget,
|
||||
},
|
||||
Login {
|
||||
backend_url: String,
|
||||
no_wait: bool,
|
||||
@@ -119,19 +125,25 @@ async fn main() -> ExitCode {
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::Objective(cli) => match objective_cli::run(cli) {
|
||||
Ok(output) => {
|
||||
print!("{}", output.stdout);
|
||||
match output.status {
|
||||
objective_cli::ObjectiveCliStatus::Success => ExitCode::SUCCESS,
|
||||
objective_cli::ObjectiveCliStatus::Failure => ExitCode::FAILURE,
|
||||
Mode::Objective { cli, target } => {
|
||||
match tokio::task::spawn_blocking(move || objective_cli::run(cli, target)).await {
|
||||
Ok(Ok(output)) => {
|
||||
print!("{}", output.stdout);
|
||||
match output.status {
|
||||
objective_cli::ObjectiveCliStatus::Success => ExitCode::SUCCESS,
|
||||
objective_cli::ObjectiveCliStatus::Failure => ExitCode::FAILURE,
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("yoi objective: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi objective: execution task failed: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi objective: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
}
|
||||
Mode::Session(cli) => match session_cli::run(cli) {
|
||||
Ok(output) => {
|
||||
print!("{}", output.stdout);
|
||||
@@ -158,19 +170,25 @@ async fn main() -> ExitCode {
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::Ticket(cli) => match ticket_cli::run(cli) {
|
||||
Ok(output) => {
|
||||
print!("{}", output.stdout);
|
||||
match output.status {
|
||||
ticket_cli::TicketCliStatus::Success => ExitCode::SUCCESS,
|
||||
ticket_cli::TicketCliStatus::Failure => ExitCode::FAILURE,
|
||||
Mode::Ticket { cli, target } => {
|
||||
match tokio::task::spawn_blocking(move || ticket_cli::run(cli, target)).await {
|
||||
Ok(Ok(output)) => {
|
||||
print!("{}", output.stdout);
|
||||
match output.status {
|
||||
ticket_cli::TicketCliStatus::Success => ExitCode::SUCCESS,
|
||||
ticket_cli::TicketCliStatus::Failure => ExitCode::FAILURE,
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("yoi ticket: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi ticket: execution task failed: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("yoi ticket: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
}
|
||||
Mode::WorkerRuntime(args) => worker::entrypoint::run_cli_from("yoi worker", args).await,
|
||||
Mode::Keys => tui::keys::launch().await,
|
||||
Mode::SetupModel => tui::setup_model::launch().await,
|
||||
@@ -357,10 +375,18 @@ fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
||||
return Ok(Mode::WorkerRuntime(args[1..].to_vec()));
|
||||
}
|
||||
"objective" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Objective)?;
|
||||
let objective_cli = objective_cli::parse_objective_args(&args[1..])
|
||||
let workspace_root = current_dir()?;
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::Objective,
|
||||
&target_selection,
|
||||
&workspace_root,
|
||||
)?
|
||||
.resolve()
|
||||
.map_err(|error| ParseError(error.to_string()))?;
|
||||
let cli = objective_cli::parse_objective_args(&args[1..])
|
||||
.map_err(|e| ParseError(e.to_string()))?;
|
||||
return Ok(Mode::Objective(objective_cli));
|
||||
return Ok(Mode::Objective { cli, target });
|
||||
}
|
||||
"session" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Session)?;
|
||||
@@ -369,10 +395,18 @@ fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
||||
return Ok(Mode::Session(session_cli));
|
||||
}
|
||||
"ticket" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Ticket)?;
|
||||
let ticket_cli =
|
||||
let workspace_root = current_dir()?;
|
||||
let target = resolve_tui_target(
|
||||
connection_resolver,
|
||||
CliCommand::Ticket,
|
||||
&target_selection,
|
||||
&workspace_root,
|
||||
)?
|
||||
.resolve()
|
||||
.map_err(|error| ParseError(error.to_string()))?;
|
||||
let cli =
|
||||
ticket_cli::parse_ticket_args(&args[1..]).map_err(|e| ParseError(e.to_string()))?;
|
||||
return Ok(Mode::Ticket(ticket_cli));
|
||||
return Ok(Mode::Ticket { cli, target });
|
||||
}
|
||||
"plugin" => {
|
||||
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Plugin)?;
|
||||
@@ -1660,8 +1694,8 @@ Local commands:
|
||||
worker [WORKER_OPTIONS] Run the local Worker runtime CLI
|
||||
worker delete <NAME> Delete local Worker records
|
||||
worker prune Prune old local Worker records
|
||||
ticket <COMMAND> Manage Tickets through the local workspace authority
|
||||
objective <COMMAND> Manage Objectives through the local workspace authority
|
||||
ticket <COMMAND> Manage Tickets through the selected target
|
||||
objective <COMMAND> Manage Objectives through the selected target
|
||||
plugin <COMMAND> Build/check/list/show plugins
|
||||
mcp <COMMAND> Inspect configured MCP servers
|
||||
memory lint Lint local memory files
|
||||
@@ -2105,11 +2139,67 @@ backend = "shared"
|
||||
#[test]
|
||||
fn parse_ticket_subcommand_uses_ticket_mode() {
|
||||
match parse_args_from(["ticket", "doctor"]).unwrap() {
|
||||
Mode::Ticket(ticket_cli::TicketCli::Command(ticket_cli::TicketCommand::Doctor)) => {}
|
||||
Mode::Ticket {
|
||||
cli: ticket_cli::TicketCli::Command(ticket_cli::TicketCommand::Doctor),
|
||||
target: client::ResolvedTarget::Local,
|
||||
} => {}
|
||||
_ => panic!("expected Ticket doctor mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_backend_ticket_keeps_resolved_workspace_target() {
|
||||
let resolver = FixedCliConnectionResolver {
|
||||
backend_url: "http://fake-backend.example",
|
||||
};
|
||||
let args = vec![
|
||||
"--backend".to_string(),
|
||||
"http://ignored-by-fixed-resolver.example".to_string(),
|
||||
"--workspace-id".to_string(),
|
||||
"workspace-a".to_string(),
|
||||
"ticket".to_string(),
|
||||
"doctor".to_string(),
|
||||
];
|
||||
|
||||
match parse_args_slice_with_connection_resolver(&args, &resolver).unwrap() {
|
||||
Mode::Ticket {
|
||||
cli: ticket_cli::TicketCli::Command(ticket_cli::TicketCommand::Doctor),
|
||||
target:
|
||||
client::ResolvedTarget::Backend {
|
||||
base_url,
|
||||
workspace_id,
|
||||
},
|
||||
} => {
|
||||
assert_eq!(base_url, "http://fake-backend.example");
|
||||
assert_eq!(workspace_id, "workspace-a");
|
||||
}
|
||||
other => panic!("expected Backend Ticket mode, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_backend_objective_keeps_resolved_workspace_target() {
|
||||
let resolver = FixedCliConnectionResolver {
|
||||
backend_url: "http://fake-backend.example",
|
||||
};
|
||||
let args = vec![
|
||||
"--backend".to_string(),
|
||||
"http://ignored-by-fixed-resolver.example".to_string(),
|
||||
"--workspace-id".to_string(),
|
||||
"workspace-a".to_string(),
|
||||
"objective".to_string(),
|
||||
"doctor".to_string(),
|
||||
];
|
||||
|
||||
match parse_args_slice_with_connection_resolver(&args, &resolver).unwrap() {
|
||||
Mode::Objective {
|
||||
cli: objective_cli::ObjectiveCli::Command(objective_cli::ObjectiveCommand::Doctor),
|
||||
target: client::ResolvedTarget::Backend { workspace_id, .. },
|
||||
} => assert_eq!(workspace_id, "workspace-a"),
|
||||
other => panic!("expected Backend Objective mode, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_session_analyze_uses_session_mode() {
|
||||
match parse_args_from(["session", "analyze", "/tmp/session.jsonl", "--json"]).unwrap() {
|
||||
@@ -2124,7 +2214,10 @@ backend = "shared"
|
||||
#[test]
|
||||
fn parse_ticket_help_uses_ticket_mode() {
|
||||
match parse_args_from(["ticket", "--help"]).unwrap() {
|
||||
Mode::Ticket(ticket_cli::TicketCli::Help) => {}
|
||||
Mode::Ticket {
|
||||
cli: ticket_cli::TicketCli::Help,
|
||||
target: client::ResolvedTarget::Local,
|
||||
} => {}
|
||||
_ => panic!("expected Ticket help mode"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use chrono::Utc;
|
||||
use client::{BackendWorkspaceProductClient, ResolvedTarget};
|
||||
use project_record::{allocate_record_id, unix_epoch_millis_now, validate_record_id};
|
||||
use serde::Deserialize;
|
||||
use ticket::config::TicketConfig;
|
||||
@@ -167,11 +168,100 @@ pub fn parse_objective_args(args: &[String]) -> Result<ObjectiveCli, ObjectiveCl
|
||||
Ok(ObjectiveCli::Command(command))
|
||||
}
|
||||
|
||||
pub fn run(cli: ObjectiveCli) -> Result<ObjectiveCliOutput, ObjectiveCliError> {
|
||||
let workspace = std::env::current_dir().map_err(|error| {
|
||||
ObjectiveCliError::new(format!("failed to resolve current directory: {error}"))
|
||||
})?;
|
||||
run_in_workspace(cli, &workspace)
|
||||
pub fn run(
|
||||
cli: ObjectiveCli,
|
||||
target: ResolvedTarget,
|
||||
) -> Result<ObjectiveCliOutput, ObjectiveCliError> {
|
||||
match target {
|
||||
ResolvedTarget::Local => {
|
||||
let workspace = std::env::current_dir().map_err(|error| {
|
||||
ObjectiveCliError::new(format!("failed to resolve current directory: {error}"))
|
||||
})?;
|
||||
run_in_workspace(cli, &workspace)
|
||||
}
|
||||
ResolvedTarget::Backend {
|
||||
base_url,
|
||||
workspace_id,
|
||||
} => {
|
||||
let backend = BackendWorkspaceProductClient::new(base_url, workspace_id)
|
||||
.map_err(|error| ObjectiveCliError::new(error.to_string()))?;
|
||||
run_with_backend(cli, &backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_with_backend(
|
||||
cli: ObjectiveCli,
|
||||
backend: &BackendWorkspaceProductClient,
|
||||
) -> Result<ObjectiveCliOutput, ObjectiveCliError> {
|
||||
match cli {
|
||||
ObjectiveCli::Help => Ok(success(help_text().to_string())),
|
||||
ObjectiveCli::Command(ObjectiveCommand::Create(options)) => {
|
||||
let title = options.title.trim();
|
||||
if title.is_empty() {
|
||||
return Err(ObjectiveCliError::new("create --title must not be empty"));
|
||||
}
|
||||
let objective = backend
|
||||
.create_objective(&workspace_api::ObjectiveCreateRequest {
|
||||
title: title.to_string(),
|
||||
body_md: objective_body_template(),
|
||||
state: "active".to_string(),
|
||||
linked_tickets: options.linked_tickets,
|
||||
})
|
||||
.map_err(|error| ObjectiveCliError::new(error.to_string()))?;
|
||||
Ok(success(format!("created\t{}\n", objective.id)))
|
||||
}
|
||||
ObjectiveCli::Command(ObjectiveCommand::List(options)) => {
|
||||
let response = backend
|
||||
.list_objectives(BackendWorkspaceProductClient::default_product_list_limit())
|
||||
.map_err(|error| ObjectiveCliError::new(error.to_string()))?;
|
||||
let mut stdout = String::from("state\tid\ttitle\tupdated_at\tlinked_tickets\n");
|
||||
for objective in response.items {
|
||||
let state = ObjectiveState::parse(&objective.state);
|
||||
if !list_state_matches(options.state, state) {
|
||||
continue;
|
||||
}
|
||||
stdout.push_str(&format!(
|
||||
"{}\t{}\t{}\t{}\t{}\n",
|
||||
objective.state,
|
||||
objective.id,
|
||||
objective.title,
|
||||
objective.updated_at.unwrap_or_default(),
|
||||
objective.linked_tickets.join(",")
|
||||
));
|
||||
}
|
||||
Ok(success(stdout))
|
||||
}
|
||||
ObjectiveCli::Command(ObjectiveCommand::Show { id }) => {
|
||||
let objective = backend
|
||||
.show_objective(&id)
|
||||
.map_err(|error| ObjectiveCliError::new(error.to_string()))?;
|
||||
let mut stdout = String::new();
|
||||
stdout.push_str(&format!("# {}\n\n", objective.title));
|
||||
stdout.push_str(&format!("State: {}\n", objective.state));
|
||||
stdout.push_str(&format!("ID: {}\n", objective.id));
|
||||
stdout.push_str(&format!(
|
||||
"Updated: {}\n\n## item.md\n\n",
|
||||
objective.updated_at.unwrap_or_default()
|
||||
));
|
||||
stdout.push_str(&objective.body);
|
||||
if !stdout.ends_with('\n') {
|
||||
stdout.push('\n');
|
||||
}
|
||||
Ok(success(stdout))
|
||||
}
|
||||
ObjectiveCli::Command(ObjectiveCommand::Doctor) => {
|
||||
let response = backend
|
||||
.list_objectives(BackendWorkspaceProductClient::default_product_list_limit())
|
||||
.map_err(|error| ObjectiveCliError::new(error.to_string()))?;
|
||||
for objective in response.items {
|
||||
backend
|
||||
.show_objective(&objective.id)
|
||||
.map_err(|error| ObjectiveCliError::new(error.to_string()))?;
|
||||
}
|
||||
Ok(success("doctor: ok\n".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_in_workspace(
|
||||
@@ -453,15 +543,21 @@ fn list_state_matches(filter: ObjectiveListState, state: Option<ObjectiveState>)
|
||||
}
|
||||
}
|
||||
|
||||
fn objective_body_template() -> String {
|
||||
"## Goal\n\nTBD\n\n## Motivation / background\n\nTBD\n\n## Strategy / design direction\n\nTBD\n\n## Success criteria / exit conditions\n\n- TBD\n\n## Decision context\n\n- TBD\n"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn render_objective_item(title: &str, linked_tickets: &[String]) -> String {
|
||||
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
format!(
|
||||
"---\ntitle: {}\nstate: {}\ncreated_at: {}\nupdated_at: {}\nlinked_tickets: {}\n---\n\n## Goal\n\nTBD\n\n## Motivation / background\n\nTBD\n\n## Strategy / design direction\n\nTBD\n\n## Success criteria / exit conditions\n\n- TBD\n\n## Decision context\n\n- TBD\n\n",
|
||||
"---\ntitle: {}\nstate: {}\ncreated_at: {}\nupdated_at: {}\nlinked_tickets: {}\n---\n\n{}\n",
|
||||
yaml_string(title),
|
||||
yaml_string(ObjectiveState::Active.as_str()),
|
||||
yaml_string(&now),
|
||||
yaml_string(&now),
|
||||
yaml_string_array(linked_tickets)
|
||||
yaml_string_array(linked_tickets),
|
||||
objective_body_template()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -587,7 +683,7 @@ fn success(stdout: String) -> ObjectiveCliOutput {
|
||||
}
|
||||
|
||||
fn help_text() -> &'static str {
|
||||
"yoi objective\n\nUsage:\n yoi objective create --title <TITLE> [--ticket <TICKET_ID> ...]\n yoi objective list [--state active|paused|done|archived|all]\n yoi objective show <OBJECTIVE_ID>\n yoi objective doctor\n\nObjective records are lightweight project records stored as .yoi/objectives/<objective-id>/item.md. Linked Tickets must be canonical opaque Ticket IDs; Objective links are non-blocking context, not Ticket dependencies.\n"
|
||||
"yoi objective\n\nUsage:\n yoi objective create --title <TITLE> [--ticket <TICKET_ID> ...]\n yoi objective list [--state active|paused|done|archived|all]\n yoi objective show <OBJECTIVE_ID>\n yoi objective doctor\n\nBackend targets use the Workspace-scoped Objective API selected by the shared client Target. Explicit local targets preserve the repository-file Objective backend. Linked Tickets must be canonical opaque Ticket IDs; Objective links are non-blocking context, not Ticket dependencies.\n"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use client::{BackendWorkspaceProductClient, ResolvedTarget};
|
||||
use ticket::config::{
|
||||
TICKET_CONFIG_RELATIVE_PATH, TicketConfig, WORKSPACE_SETTINGS_RELATIVE_PATH,
|
||||
ticket_config_scaffold,
|
||||
@@ -205,11 +206,32 @@ pub fn parse_ticket_args(args: &[String]) -> Result<TicketCli, TicketCliError> {
|
||||
Ok(TicketCli::Command(command))
|
||||
}
|
||||
|
||||
pub fn run(cli: TicketCli) -> Result<TicketCliOutput, TicketCliError> {
|
||||
let workspace = std::env::current_dir().map_err(|error| {
|
||||
TicketCliError::new(format!("failed to resolve current directory: {error}"))
|
||||
})?;
|
||||
run_in_workspace(cli, &workspace)
|
||||
pub fn run(cli: TicketCli, target: ResolvedTarget) -> Result<TicketCliOutput, TicketCliError> {
|
||||
match target {
|
||||
ResolvedTarget::Local => {
|
||||
let workspace = std::env::current_dir().map_err(|error| {
|
||||
TicketCliError::new(format!("failed to resolve current directory: {error}"))
|
||||
})?;
|
||||
run_in_workspace(cli, &workspace)
|
||||
}
|
||||
ResolvedTarget::Backend {
|
||||
base_url,
|
||||
workspace_id,
|
||||
} => match cli {
|
||||
TicketCli::Help => Ok(TicketCliOutput {
|
||||
status: TicketCliStatus::Success,
|
||||
stdout: help_text().to_string(),
|
||||
}),
|
||||
TicketCli::Command(TicketCommand::Init | TicketCommand::ImportLocal) => Err(
|
||||
TicketCliError::new("ticket init/import-local require an explicit local target"),
|
||||
),
|
||||
TicketCli::Command(command) => {
|
||||
let backend = BackendWorkspaceProductClient::new(base_url, workspace_id)
|
||||
.map_err(|error| TicketCliError::new(error.to_string()))?;
|
||||
run_backend_command(command, &backend)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_in_workspace(
|
||||
@@ -234,23 +256,30 @@ fn run_command(
|
||||
TicketCommand::ImportLocal => import_local(workspace),
|
||||
command => {
|
||||
let backend = backend_for_workspace(workspace)?;
|
||||
match command {
|
||||
TicketCommand::Create(options) => create(backend.as_ref(), options),
|
||||
TicketCommand::List(options) => list(backend.as_ref(), options),
|
||||
TicketCommand::Show { query } => show(backend.as_ref(), query),
|
||||
TicketCommand::Comment(options) => comment(backend.as_ref(), options),
|
||||
TicketCommand::State(options) => state(backend.as_ref(), options),
|
||||
TicketCommand::Close(options) => close(backend.as_ref(), options),
|
||||
TicketCommand::Relation(options) => relation(backend.as_ref(), options),
|
||||
TicketCommand::Doctor => doctor(backend.as_ref()),
|
||||
TicketCommand::Init | TicketCommand::ImportLocal => {
|
||||
unreachable!("handled before backend setup")
|
||||
}
|
||||
}
|
||||
run_backend_command(command, backend.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_backend_command(
|
||||
command: TicketCommand,
|
||||
backend: &dyn TicketBackend,
|
||||
) -> Result<TicketCliOutput, TicketCliError> {
|
||||
match command {
|
||||
TicketCommand::Create(options) => create(backend, options),
|
||||
TicketCommand::List(options) => list(backend, options),
|
||||
TicketCommand::Show { query } => show(backend, query),
|
||||
TicketCommand::Comment(options) => comment(backend, options),
|
||||
TicketCommand::State(options) => state(backend, options),
|
||||
TicketCommand::Close(options) => close(backend, options),
|
||||
TicketCommand::Relation(options) => relation(backend, options),
|
||||
TicketCommand::Doctor => doctor(backend),
|
||||
TicketCommand::Init | TicketCommand::ImportLocal => Err(TicketCliError::new(
|
||||
"ticket init/import-local require an explicit local target",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(workspace: &Path) -> Result<TicketCliOutput, TicketCliError> {
|
||||
let legacy_config_path = workspace.join(TICKET_CONFIG_RELATIVE_PATH);
|
||||
if legacy_config_path.exists() {
|
||||
@@ -1153,7 +1182,7 @@ fn default_author() -> String {
|
||||
}
|
||||
|
||||
fn help_text() -> &'static str {
|
||||
"yoi ticket\n\nUsage:\n yoi ticket init\n yoi ticket import-local\n yoi ticket create --title <title>\n yoi ticket list [--state active|all|planning|ready|queued|inprogress|done|closed[,..]] [--limit <n>]\n yoi ticket show <id>\n yoi ticket comment <id> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket state <id> <planning|ready|queued|inprogress|closed>\n yoi ticket close <id> (--resolution <text>|--file <path>)\n yoi ticket relation add --ticket <id> --kind <depends_on|blocks|related|supersedes|duplicate_of> --target <id> [--note <text>]\n yoi ticket relation list [--ticket <id>] [--kind <kind>]\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n Tickets are stored in the workspace SQLite DB under the Yoi data directory.\n `yoi ticket import-local` imports the legacy .yoi/tickets backend root configured in .yoi/workspace.toml.\n `yoi ticket init` writes explicit fixed role profiles and optional [ticket].language into .yoi/workspace.toml, but does not create .yoi/tickets.\n"
|
||||
"yoi ticket\n\nUsage:\n yoi ticket init\n yoi ticket import-local\n yoi ticket create --title <title>\n yoi ticket list [--state active|all|planning|ready|queued|inprogress|done|closed[,..]] [--limit <n>]\n yoi ticket show <id>\n yoi ticket comment <id> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket state <id> <planning|ready|queued|inprogress|closed>\n yoi ticket close <id> (--resolution <text>|--file <path>)\n yoi ticket relation add --ticket <id> --kind <depends_on|blocks|related|supersedes|duplicate_of> --target <id> [--note <text>]\n yoi ticket relation list [--ticket <id>] [--kind <kind>]\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nTargets:\n Backend targets use the Workspace-scoped Ticket API selected by the shared client Target.\n Explicit local targets use the workspace SQLite backend. `init` and `import-local` are local-only.\n `yoi ticket import-local` imports the legacy .yoi/tickets backend root configured in .yoi/workspace.toml.\n `yoi ticket init` writes explicit fixed role profiles and optional [ticket].language into .yoi/workspace.toml, but does not create .yoi/tickets.\n"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user