Skip to content

Commit 715446c

Browse files
committed
feat(list): always display fully-qualified daemon names
Modify pitchfork ls to always show namespace/name format instead of omitting the namespace when there's no local conflict. Also add state_file fallback in resolve_daemon_id() for resolving short daemon IDs globally when no local config matches. When ambiguous, error now lists all matching fully-qualified IDs.
1 parent 51f2e13 commit 715446c

3 files changed

Lines changed: 59 additions & 21 deletions

File tree

src/cli/list.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::Result;
2-
use crate::daemon_id::DaemonId;
32
use crate::daemon_list::get_all_daemons;
43
use crate::daemon_status::DaemonStatus;
54
use crate::ipc::client::IpcClient;
@@ -60,11 +59,8 @@ impl List {
6059
let entries = get_all_daemons(&client).await?;
6160
let global_slugs = PitchforkToml::read_global_slugs();
6261

63-
// Collect all IDs for display name resolution (clone to avoid borrow issues)
64-
let all_ids: Vec<DaemonId> = entries.iter().map(|e| e.id.clone()).collect();
65-
6662
for entry in entries {
67-
let display_name = entry.id.styled_display_name(Some(all_ids.iter()));
63+
let display_name = entry.id.styled_qualified();
6864

6965
let status_text = if entry.is_available {
7066
"available".to_string()

src/pitchfork_toml.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,61 @@ impl PitchforkToml {
395395
.collect();
396396

397397
if matches.is_empty() {
398-
// No config matches. Validate short ID format and return no matches.
398+
// No config matches. Search state file for any daemon with matching short name.
399+
let state_matches = Self::find_in_state_file(user_id);
400+
match state_matches.as_slice() {
401+
[] => {}
402+
[id] => return Ok(vec![id.clone()]),
403+
_ => {
404+
let mut candidates: Vec<String> =
405+
state_matches.iter().map(|id| id.qualified()).collect();
406+
candidates.sort();
407+
return Err(miette::miette!(
408+
"daemon '{}' is ambiguous; matches: {}. Use a qualified daemon ID (namespace/name)",
409+
user_id,
410+
candidates.join(", ")
411+
));
412+
}
413+
}
414+
// No config or state matches. Validate short ID format and return no matches.
399415
let _ = DaemonId::try_new("global", user_id)?;
400416
}
401417
Ok(matches)
402418
}
403419

420+
/// Checks whether a fully-qualified daemon ID exists in the persisted state file.
421+
///
422+
/// Logs a warning if the state file exists but cannot be read or parsed.
423+
fn state_file_contains(id: &DaemonId) -> bool {
424+
match StateFile::read(&*env::PITCHFORK_STATE_FILE) {
425+
Ok(state) => state.daemons.contains_key(id),
426+
Err(e) => {
427+
warn!("cannot read state file: {e}");
428+
false
429+
}
430+
}
431+
}
432+
433+
/// Finds all daemons in the persisted state file whose short name matches `short_name`.
434+
///
435+
/// Logs a warning if the state file exists but cannot be read or parsed.
436+
///
437+
/// Returns the matching `DaemonId`s. The caller must handle zero / one / many cases.
438+
fn find_in_state_file(short_name: &str) -> Vec<DaemonId> {
439+
match StateFile::read(&*env::PITCHFORK_STATE_FILE) {
440+
Ok(state) => state
441+
.daemons
442+
.keys()
443+
.filter(|id| id.name() == short_name)
444+
.cloned()
445+
.collect(),
446+
Err(e) => {
447+
warn!("cannot read state file: {e}");
448+
Vec::new()
449+
}
450+
}
451+
}
452+
404453
/// Resolves a user-provided daemon ID to a qualified DaemonId, preferring the current directory's namespace.
405454
///
406455
/// If the ID is already qualified (contains '/'), parses and returns it.
@@ -511,9 +560,7 @@ impl PitchforkToml {
511560
// Also allow existing ad-hoc daemons (persisted in state file) to be
512561
// referenced by short ID. This keeps commands like status/restart/stop
513562
// working for daemons started via `pitchfork run`.
514-
if let Ok(state) = StateFile::read(&*env::PITCHFORK_STATE_FILE)
515-
&& state.daemons.contains_key(&global_id)
516-
{
563+
if Self::state_file_contains(&global_id) {
517564
return Ok(global_id);
518565
}
519566

tests/test_e2e_namespace.rs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,9 @@ run = "sleep 60"
267267
let _ = env.run_command_in_dir(&["stop", "web"], &project_y);
268268
}
269269

270-
/// Test that list command hides namespace when there's no conflict
270+
/// Test that list command always shows fully-qualified daemon names.
271271
#[test]
272-
fn test_list_hides_namespace_without_conflict() {
272+
fn test_list_always_shows_qualified_names() {
273273
let env = TestEnv::new();
274274
env.ensure_binary_exists().unwrap();
275275

@@ -299,19 +299,14 @@ run = "sleep 60"
299299
let list_str = String::from_utf8_lossy(&list_output.stdout);
300300
println!("list output (no conflict): {list_str}");
301301

302-
// Should show short names (no namespace) since there's no conflict
302+
// Should always show fully-qualified names
303303
assert!(
304-
list_str.contains("unique-api"),
305-
"List should contain unique-api"
304+
list_str.contains("solo-project/unique-api"),
305+
"List should contain fully-qualified solo-project/unique-api"
306306
);
307307
assert!(
308-
list_str.contains("unique-worker"),
309-
"List should contain unique-worker"
310-
);
311-
// Should NOT contain the namespace prefix (no conflict)
312-
assert!(
313-
!list_str.contains("solo-project/"),
314-
"List should NOT show namespace when there's no conflict, got: {list_str}"
308+
list_str.contains("solo-project/unique-worker"),
309+
"List should contain fully-qualified solo-project/unique-worker"
315310
);
316311

317312
// Cleanup

0 commit comments

Comments
 (0)