Skip to content

Commit eba1264

Browse files
committed
Make mission log readable without --entry
• [MSN] No active missions on the board • [EXC] Board idle, no stories queued or active • [HLT] 2 warnings, no structural errors detected
1 parent 1a0c1e9 commit eba1264

3 files changed

Lines changed: 119 additions & 8 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Mission log command
2+
3+
use std::fs;
4+
use std::path::Path;
5+
6+
use anyhow::Result;
7+
8+
use keel::domain::model::Board;
9+
use keel::infrastructure::loader::load_board;
10+
use keel::read_model::show_selector::{ShowEntityKind, resolve_show_selector};
11+
12+
/// Show the mission log for a mission with an explicit board directory.
13+
pub fn run_with_dir(board_dir: &Path, id: &str) -> Result<()> {
14+
let content = read_log_with_dir(board_dir, id)?;
15+
print!("{content}");
16+
if !content.ends_with('\n') {
17+
println!();
18+
}
19+
Ok(())
20+
}
21+
22+
/// Read the raw mission log content for a mission.
23+
pub fn read_log_with_dir(board_dir: &Path, id: &str) -> Result<String> {
24+
let board = load_board(board_dir)?;
25+
let resolved_id = resolve_mission_id_with_board(board_dir, &board, id)?;
26+
let mission = board.require_mission(&resolved_id)?;
27+
let log_path = mission.path.parent().unwrap().join("LOG.md");
28+
Ok(fs::read_to_string(log_path)?)
29+
}
30+
31+
/// Resolve a mission ID or HEAD selector for mission-log operations.
32+
pub fn resolve_mission_id(board_dir: &Path, id: &str) -> Result<String> {
33+
let board = load_board(board_dir)?;
34+
resolve_mission_id_with_board(board_dir, &board, id)
35+
}
36+
37+
fn resolve_mission_id_with_board(board_dir: &Path, board: &Board, id: &str) -> Result<String> {
38+
Ok(resolve_show_selector(
39+
board_dir,
40+
board,
41+
ShowEntityKind::Mission,
42+
id,
43+
)?)
44+
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use super::*;
49+
use keel::application::mission_lifecycle::MissionLifecycleService;
50+
use keel::test_helpers::{TestBoardBuilder, TestMission};
51+
52+
#[test]
53+
fn read_log_with_dir_returns_full_log_contents() {
54+
let temp = TestBoardBuilder::new()
55+
.mission(TestMission::new("M1").title("Mission One"))
56+
.build();
57+
58+
MissionLifecycleService::log(temp.path(), "M1", "First entry").unwrap();
59+
MissionLifecycleService::log(temp.path(), "M1", "Second entry").unwrap();
60+
61+
let log = read_log_with_dir(temp.path(), "M1").unwrap();
62+
63+
assert!(log.contains("# Mission One - Decision Log"));
64+
assert!(log.contains("First entry"));
65+
assert!(log.contains("Second entry"));
66+
}
67+
68+
#[test]
69+
fn resolve_mission_id_accepts_head_selectors() {
70+
let temp = TestBoardBuilder::new()
71+
.mission(TestMission::new("M2").title("Mission Two"))
72+
.mission(TestMission::new("M1").title("Mission One"))
73+
.build();
74+
75+
assert_eq!(resolve_mission_id(temp.path(), "HEAD").unwrap(), "M1");
76+
assert_eq!(resolve_mission_id(temp.path(), "HEAD~").unwrap(), "M2");
77+
}
78+
}

crates/keel-cli/src/cli/commands/management/mission/mod.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use clap::{ArgGroup, Subcommand};
77

88
pub mod attach;
99
pub mod list;
10+
pub mod log;
1011
pub mod new;
1112
pub mod next;
1213
pub mod play;
@@ -115,13 +116,13 @@ pub enum MissionAction {
115116
/// Mission ID
116117
id: String,
117118
},
118-
/// Add entry to mission log
119+
/// Show mission log or add entry to mission log
119120
Log {
120-
/// Mission ID
121+
/// Mission ID or HEAD selector (HEAD, HEAD~, HEAD~~, HEAD^)
121122
id: String,
122123
/// Log entry text
123124
#[arg(long)]
124-
entry: String,
125+
entry: Option<String>,
125126
},
126127
/// Digest mission log entries
127128
Digest {
@@ -197,11 +198,17 @@ pub fn run(ctx: &spoke_auth::ExecutionContext, action: MissionAction) -> Result<
197198
MissionAction::Abandon { id } => {
198199
keel::application::mission_lifecycle::MissionLifecycleService::abandon(&board_dir, &id)
199200
}
200-
MissionAction::Log { id, entry } => {
201-
keel::application::mission_lifecycle::MissionLifecycleService::log(
202-
&board_dir, &id, &entry,
203-
)
204-
}
201+
MissionAction::Log { id, entry } => match entry {
202+
Some(entry) => {
203+
let resolved_id = log::resolve_mission_id(&board_dir, &id)?;
204+
keel::application::mission_lifecycle::MissionLifecycleService::log(
205+
&board_dir,
206+
&resolved_id,
207+
&entry,
208+
)
209+
}
210+
None => log::run_with_dir(&board_dir, &id),
211+
},
205212
MissionAction::Digest { id } => {
206213
keel::application::mission_lifecycle::MissionLifecycleService::digest(&board_dir, &id)
207214
}

crates/keel-cli/src/cli_tests.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,32 @@ fn cli_parses_mission_resume() {
443443
));
444444
}
445445

446+
#[test]
447+
fn cli_parses_mission_log_without_entry() {
448+
let cli = Cli::try_parse_from(["board", "mission", "log", "M1"]).unwrap();
449+
assert!(matches!(
450+
cli.command,
451+
Commands::Management(ManagementCommands::Mission {
452+
action: MissionAction::Log { id, entry: None }
453+
}) if id == "M1"
454+
));
455+
}
456+
457+
#[test]
458+
fn cli_parses_mission_log_with_entry() {
459+
let cli =
460+
Cli::try_parse_from(["board", "mission", "log", "M1", "--entry", "Did some work"]).unwrap();
461+
assert!(matches!(
462+
cli.command,
463+
Commands::Management(ManagementCommands::Mission {
464+
action: MissionAction::Log {
465+
id,
466+
entry: Some(entry)
467+
}
468+
}) if id == "M1" && entry == "Did some work"
469+
));
470+
}
471+
446472
#[test]
447473
fn cli_parses_pulse_with_json() {
448474
let matches = crate::cli::build_cli()

0 commit comments

Comments
 (0)