1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! `tm projects` command group (DOC-35 §3.1/§6/§10.8, #2115/#2120/#2381).
//!
//! Why: the deterministic CLI half of the project control plane. The verb tree is
//! large (five registry verbs + two nested CRUD subtrees), so it is split per
//! subtree — `registry` (list/register/show/status/config), `deliverables`, and
//! `milestones` — each a sibling file well under the 500-SLOC cap, with this
//! `mod.rs` a thin dispatcher plus the shared clap-arg → domain-type conversions.
//! What: [`projects`] routes a [`ProjectsAction`] to the right subtree handler
//! (unchanged since #2115); [`launch_bare_tui`] is the interactive entry point
//! `main.rs` calls for a bare `tm projects` BEFORE clap parsing even runs
//! (#2118 — see `main.rs`'s module doc for why the interception happens there
//! rather than by relaxing `ProjectsAction` to `Option`), gated by
//! [`should_launch_bare_tui`] on BOTH stdin and stdout being real terminals
//! (not stdout alone — see that function's doc for the footgun this avoids).
//! The `convert` submodule maps the CLI value enums to `trusty_mpm::deliverable`
//! domain types so `cli.rs` stays free of a domain dependency.
//! Test: `cli_parses_projects_*` (parse) in `tests_projects.rs`; the per-subtree
//! rendering/serialization tests live in each submodule.
pub
pub
pub
pub
use crateProjectsAction;
/// Poll interval (ms) for the `tm projects` TUI launched from a bare, TTY
/// invocation (#2118).
///
/// Why: named separately from `Command::Tui`'s / `SessionAction::Tui`'s own
/// `--interval-ms` flags because a bare `tm projects` has no flag surface of
/// its own to read one from; 1500ms matches `tm sessions tui`'s default.
const PROJECT_CTL_INTERVAL_MS: u64 = 1500;
/// Launch the `tm projects` 4-pane TUI for a bare, interactive invocation (#2118).
///
/// Why: `main.rs` intercepts a bare `tm projects` (exactly `["tm", "projects"]`,
/// no further arguments, both stdin AND stdout confirmed real terminals — see
/// [`should_launch_bare_tui`]) BEFORE `Cli::try_parse()` runs — see `main.rs`'s
/// module doc. Doing the interception there (rather than by making
/// `ProjectsAction` an `Option` and branching inside this module's dispatcher)
/// means `tm projects <verb>` and a non-interactive bare `tm projects` both
/// flow through the UNMODIFIED clap definition, so clap's own pre-#2118
/// "requires a subcommand" usage error (exit code 2) for the latter is
/// preserved byte-for-byte — reproducing that error manually was tried and
/// does not exactly match clap's internal formatting (it omits the
/// `[subcommands: …]` hint clap derives from richer internal match context),
/// so delegating to the untouched parse path is the more reliable choice.
/// What: resolves the daemon URL exactly as the normal dispatch path would
/// for an unspecified `--url` — reading `TRUSTY_MPM_URL` directly via
/// [`trusty_mpm::core::explicit_url_from_env`] (a bare invocation runs BEFORE
/// `Cli::try_parse()`, so there is no `--url` flag to read and no clap-parsed
/// `Option<String>` field yet) — and hands off to
/// `trusty_mpm::tui::project_ctl::run`.
///
/// Why (#2487): this call site used to pass `Some(crate::cli::DEFAULT_URL)` —
/// the literal default URL string — relying on the resolvers' old
/// value-equality heuristic to treat it as "no real override" so the gateway
/// probe still ran. That heuristic is gone (every resolver now trusts `Some`
/// unconditionally), and the old call never actually read `TRUSTY_MPM_URL` at
/// all, so a bare `tm projects` TUI silently ignored the env var even when
/// the "normal" `tm projects <verb>` dispatch honoured it. Using the shared
/// env-read helper closes that gap and keeps this path in lockstep with
/// `main.rs`'s dispatch resolution.
/// Test: terminal glue, exercised by launching the TUI; the gate condition
/// itself is [`should_launch_bare_tui`], which IS unit tested.
pub async
/// True when `argv` is exactly a bare `tm projects` invocation (#2118).
///
/// Why: split out of [`should_launch_bare_tui`] so the argv-shape rule and the
/// stream-liveness rule are each independently testable.
/// What: true only when `argv` has exactly two elements and the second is
/// literally `"projects"` — any flag (e.g. `--url ...`) or any verb makes
/// this false, so `tm projects list`, `tm --url … projects`, and every other
/// shape falls through to the unmodified clap parse path unaffected.
/// Test: `tests::is_bare_projects_argv_matches_only_the_exact_bare_form`.
pub
/// True when a bare `tm projects` invocation should launch the interactive TUI.
///
/// Why: launching a full-screen, raw-mode TUI requires BOTH stdin and stdout
/// to be real terminals. The initial #2118 landing checked stdout alone,
/// which left a footgun: a supervisor/wrapper that redirects stdin from
/// `/dev/null` while leaving stdout attached to a pty (systemd, launchd, some
/// CI runners) would still see `stdout.is_terminal() == true` and launch a
/// raw-mode TUI against a dead stdin — with no keyboard path able to ever
/// send `q`/Ctrl-C, only an external signal can kill it. Mirrors
/// `session_picker::should_show_picker`'s established "require both streams"
/// convention in this same codebase. Taking `stdin_tty`/`stdout_tty` as plain
/// `bool` parameters (rather than calling `std::io::stdin()/stdout()` here)
/// keeps this pure and unit-testable without faking a real terminal; `main.rs`
/// supplies the real `is_terminal()` results at the one call site.
/// What: true only when [`is_bare_projects_argv`] AND `stdin_tty` AND
/// `stdout_tty` are all true.
/// Test: `tests::should_launch_bare_tui_requires_both_streams`.
pub
/// Dispatch a `tm projects <action>` invocation to its subtree handler.
///
/// Why: `main.rs` stays a thin bootstrap; all `projects` routing lives here.
/// What: matches the top-level action and forwards to the registry verb handlers
/// or the deliverables/milestones subtree dispatchers, threading the CLI's shared
/// `(reqwest::Client, url)` pair through to the typed `DaemonClient` methods.
/// Test: exercised end-to-end by the daemon integration tests; parse coverage in
/// `tests_projects.rs`.
pub async