1use anyhow::Context;
2use core_types::TrustProfileName;
3use owo_colors::OwoColorize;
4use zeroize::Zeroize;
5
6use crate::cli::resolve_workspace_path;
7use crate::cli::{WorkspaceCmd, WorkspaceConfigCmd, WorkspaceListFormat};
8use crate::ipc::{connect, fetch_multi_profile_secrets, parse_profile_specs};
9
10#[cfg(target_os = "linux")]
15fn needs_privilege(path: &std::path::Path) -> bool {
16 let uid = unsafe { libc::getuid() };
17 let mut check = path.to_path_buf();
18 loop {
19 if check.exists() {
20 return std::fs::metadata(&check)
21 .map(|m| {
22 use std::os::unix::fs::MetadataExt;
23 m.uid() != uid
24 })
25 .unwrap_or(true);
26 }
27 if !check.pop() {
28 return true;
29 }
30 }
31}
32
33fn urls_match(a: &str, b: &str) -> bool {
35 fn normalize(url: &str) -> String {
36 url.trim_end_matches('/')
37 .trim_end_matches(".git")
38 .to_lowercase()
39 }
40 normalize(a) == normalize(b)
41}
42
43pub(crate) async fn cmd_workspace(cmd: WorkspaceCmd) -> anyhow::Result<()> {
44 match cmd {
45 WorkspaceCmd::Init { root, user } => {
46 let user =
47 user.unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "user".into()));
48
49 #[cfg(target_os = "linux")]
50 {
51 if !root.exists() && needs_privilege(&root) {
53 eprintln!(
54 "Workspace root '{}' does not exist and requires elevated privileges to create.",
55 root.display()
56 );
57 eprint!("Continue? [y/N] ");
58 use std::io::Write;
59 std::io::stderr().flush()?;
60 let mut answer = String::new();
61 std::io::BufRead::read_line(&mut std::io::stdin().lock(), &mut answer)
62 .context("failed to read confirmation")?;
63 if !matches!(answer.trim().to_lowercase().as_str(), "y" | "yes") {
64 println!("Cancelled.");
65 return Ok(());
66 }
67 }
68
69 use sesame_workspace::platform::WorkspacePlatform;
70 let platform = sesame_workspace::platform::linux::LinuxPlatform;
71 platform
72 .ensure_root(&root)
73 .map_err(|e| anyhow::anyhow!("{e}"))?;
74 }
75
76 #[cfg(not(target_os = "linux"))]
77 {
78 std::fs::create_dir_all(&root).context("failed to create workspace root")?;
79 }
80
81 let user_dir = root.join(&user);
82 std::fs::create_dir_all(&user_dir).context("failed to create user directory")?;
83
84 let mut config = core_config::load_workspace_config().unwrap_or_default();
85 config.settings.root = root.clone();
86 config.settings.user = user.clone();
87 core_config::save_workspace_config(&config).map_err(|e| anyhow::anyhow!("{e}"))?;
88
89 println!("Workspace initialized: {}", user_dir.display());
90 println!(
91 "Config written: {}",
92 core_config::config_dir().join("workspaces.toml").display()
93 );
94 Ok(())
95 }
96
97 WorkspaceCmd::Clone {
98 url,
99 depth,
100 profile,
101 adopt,
102 workspace_init,
103 workspace_update,
104 no_workspace,
105 force,
106 project,
107 include_forks,
108 include_archived,
109 } => {
110 let config = core_config::load_workspace_config().unwrap_or_default();
111 let root = sesame_workspace::config::resolve_root(&config);
112 let user = sesame_workspace::config::resolve_user(&config);
113
114 let parse_url = if project {
117 let trimmed = url.trim_end_matches('/');
118 let without_scheme = trimmed
120 .strip_prefix("https://")
121 .or_else(|| trimmed.strip_prefix("http://"))
122 .unwrap_or(trimmed);
123 let segments = without_scheme.split('/').count();
124 if segments == 2 {
125 format!("{trimmed}/_placeholder")
126 } else {
127 url.clone()
128 }
129 } else {
130 url.clone()
131 };
132
133 let conv = sesame_workspace::convention::parse_url(&parse_url)
134 .map_err(|e| anyhow::anyhow!("{e}"))?;
135
136 if project {
138 let forge =
139 sesame_workspace::forge::forge_for_server(&conv.server).ok_or_else(|| {
140 anyhow::anyhow!(
141 "forge API not supported for server: {} (supported: github.com)",
142 conv.server
143 )
144 })?;
145 let opts = sesame_workspace::forge::ListOptions {
146 include_forks,
147 include_archived,
148 };
149 let repos = forge
150 .list_org_repos(&conv.org, &opts)
151 .map_err(|e| anyhow::anyhow!("{e}"))?;
152
153 eprintln!(
154 "Found {} repositories in {}/{}",
155 repos.len(),
156 conv.server,
157 conv.org,
158 );
159
160 let org_dir = root.join(&user).join(&conv.server).join(&conv.org);
162 if !org_dir.join(".git").is_dir() {
163 let ws_repo_name = &config.settings.workspace_repo;
164 let ws_url =
165 format!("https://{}/{}/{}.git", conv.server, conv.org, ws_repo_name,);
166 if sesame_workspace::git::probe_remote(&ws_url) {
167 eprintln!(
168 "Setting up org workspace from {}/{}/{}.git...",
169 conv.server, conv.org, ws_repo_name,
170 );
171 let ws_conv = sesame_workspace::convention::WorkspaceConvention {
172 server: conv.server.clone(),
173 org: conv.org.clone(),
174 repo: None,
175 is_workspace_git: true,
176 };
177 let ws_target =
178 sesame_workspace::convention::canonical_path(&root, &user, &ws_conv);
179 match sesame_workspace::git::clone_repo(&ws_url, &ws_target, None, force) {
180 Ok(p) => eprintln!(" Workspace initialized: {}", p.display()),
181 Err(e) => eprintln!(" Warning: workspace.git setup failed: {e}"),
182 }
183 }
184 }
185
186 let ws_repo_name = &config.settings.workspace_repo;
188 let repos: Vec<_> = repos
189 .into_iter()
190 .filter(|r| r.name != *ws_repo_name)
191 .collect();
192
193 let mut success = 0usize;
194 let mut skipped = 0usize;
195 let mut failed = 0usize;
196
197 for repo_info in &repos {
198 let repo_url = &repo_info.clone_url;
199 let repo_conv = match sesame_workspace::convention::parse_url(repo_url) {
200 Ok(c) => c,
201 Err(e) => {
202 eprintln!(" Skipping {}: {e}", repo_info.name);
203 failed += 1;
204 continue;
205 }
206 };
207 let repo_target =
208 sesame_workspace::convention::canonical_path(&root, &user, &repo_conv);
209 let repo_path = repo_target.path().to_path_buf();
210
211 if repo_path.exists() && sesame_workspace::git::is_git_repo(&repo_path) {
212 eprintln!(" {} (exists)", repo_info.name.dimmed());
213 skipped += 1;
214 continue;
215 }
216
217 match sesame_workspace::git::clone_repo(repo_url, &repo_target, depth, force) {
218 Ok(_) => {
219 eprintln!(" {} {}", "Cloned".green(), repo_info.name);
220 success += 1;
221 }
222 Err(e) => {
223 let hint = if e.to_string().contains("auth")
224 || e.to_string().contains("401")
225 || e.to_string().contains("403")
226 {
227 " (may be a private repo — check GITHUB_TOKEN)"
228 } else {
229 ""
230 };
231 eprintln!(" {} {}: {e}{hint}", "Failed".red(), repo_info.name,);
232 failed += 1;
233 }
234 }
235 }
236
237 eprintln!("\n{success} cloned, {skipped} skipped (exist), {failed} failed",);
238 return Ok(());
239 }
240
241 let target = sesame_workspace::convention::canonical_path(&root, &user, &conv);
242
243 if !conv.is_workspace_git && !no_workspace {
259 let mode = if workspace_init || workspace_update {
260 "always" } else {
262 config.settings.workspace_auto.as_str()
263 };
264
265 if mode != "never" {
266 let org_dir = root.join(&user).join(&conv.server).join(&conv.org);
267 let ws_repo_name = &config.settings.workspace_repo;
268 let ws_url =
269 format!("https://{}/{}/{}.git", conv.server, conv.org, ws_repo_name,);
270
271 let has_workspace_git = org_dir.join(".git").is_dir();
272 let org_dir_exists = org_dir.exists();
273
274 if has_workspace_git {
275 if workspace_update || mode == "always" {
277 eprintln!("Updating org workspace at {}...", org_dir.display());
279 match sesame_workspace::git::pull_ff_only(&org_dir) {
280 Ok(()) => eprintln!(" Workspace updated."),
281 Err(e) => eprintln!(" Warning: workspace pull failed: {e}"),
282 }
283 } else if mode == "auto" {
284 let local = sesame_workspace::git::head_commit_short(&org_dir)
286 .ok()
287 .flatten()
288 .unwrap_or_else(|| "(unborn)".into());
289 let branch = sesame_workspace::git::current_branch(&org_dir)
290 .unwrap_or_else(|_| "main".into());
291 let tracking = sesame_workspace::git::remote_tracking_commit_short(
292 &org_dir, &branch,
293 )
294 .ok()
295 .flatten();
296
297 if let Some(ref remote_commit) = tracking
298 && *remote_commit != local
299 {
300 eprintln!(
301 "Note: org workspace at {} is at {local}, origin/{branch} is at {remote_commit}",
302 org_dir.display(),
303 );
304 eprintln!(
305 " Update with: sesame workspace clone {} --workspace-update",
306 url,
307 );
308 }
309 }
310 } else if !org_dir_exists {
311 if sesame_workspace::git::probe_remote(&ws_url) {
313 eprintln!(
314 "Detected {}/{}/{}.git — setting up org workspace...",
315 conv.server, conv.org, ws_repo_name,
316 );
317 let ws_conv = sesame_workspace::convention::WorkspaceConvention {
318 server: conv.server.clone(),
319 org: conv.org.clone(),
320 repo: None,
321 is_workspace_git: true,
322 };
323 let ws_target = sesame_workspace::convention::canonical_path(
324 &root, &user, &ws_conv,
325 );
326 match sesame_workspace::git::clone_repo(
327 &ws_url, &ws_target, None, force,
328 ) {
329 Ok(p) => eprintln!(" Workspace initialized: {}", p.display()),
330 Err(e) => eprintln!(" Warning: workspace.git setup failed: {e}"),
331 }
332 }
333 } else if (workspace_init || mode == "always") && org_dir_exists {
334 if !force {
338 if sesame_workspace::git::probe_remote(&ws_url) {
339 eprintln!(
340 "Warning: --workspace-init would overwrite files in {}",
341 org_dir.display(),
342 );
343 eprintln!(
344 " Add --force to proceed: sesame workspace clone {} --workspace-init --force",
345 url,
346 );
347 }
348 } else if sesame_workspace::git::probe_remote(&ws_url) {
349 eprintln!(
350 "Initializing workspace.git around existing {}...",
351 org_dir.display(),
352 );
353 let ws_conv = sesame_workspace::convention::WorkspaceConvention {
354 server: conv.server.clone(),
355 org: conv.org.clone(),
356 repo: None,
357 is_workspace_git: true,
358 };
359 let ws_target = sesame_workspace::convention::canonical_path(
360 &root, &user, &ws_conv,
361 );
362 match sesame_workspace::git::clone_repo(
363 &ws_url, &ws_target, None, force,
364 ) {
365 Ok(p) => eprintln!(" Workspace initialized: {}", p.display()),
366 Err(e) => eprintln!(" Warning: workspace.git setup failed: {e}"),
367 }
368 }
369 } else if mode == "auto" && org_dir_exists {
370 if sesame_workspace::git::probe_remote(&ws_url) {
372 eprintln!(
373 "Tip: {}/{}/{}.git is available for this org.",
374 conv.server, conv.org, ws_repo_name,
375 );
376 eprintln!(
377 " Initialize with: sesame workspace clone {} --workspace-init",
378 url,
379 );
380 eprintln!(" Or directly: sesame workspace clone {ws_url}",);
381 }
382 }
383 }
384 }
385
386 let target_path = match &target {
388 sesame_workspace::CloneTarget::Regular(p) => p.clone(),
389 sesame_workspace::CloneTarget::WorkspaceGit(p) => p.clone(),
390 };
391
392 let adopted = if target_path.exists()
393 && sesame_workspace::git::is_git_repo(&target_path)
394 && adopt
395 {
396 let existing_remote = sesame_workspace::git::remote_url(&target_path)
398 .map_err(|e| anyhow::anyhow!("{e}"))?;
399 match existing_remote {
400 Some(ref remote) if urls_match(remote, &url) => true,
401 Some(ref remote) => {
402 anyhow::bail!(
403 "directory exists with different remote:\n existing: {remote}\n requested: {url}\nRemove the directory or fix the remote manually."
404 );
405 }
406 None => {
407 anyhow::bail!(
408 "directory exists as a git repo but has no 'origin' remote: {}",
409 target_path.display()
410 );
411 }
412 }
413 } else {
414 false
415 };
416
417 let result_path = if adopted {
418 println!(
419 "\x1b[32mAdopted\x1b[0m existing repository: {}",
420 target_path.display()
421 );
422 target_path
423 } else {
424 let rp = sesame_workspace::git::clone_repo(&url, &target, depth, force)
425 .map_err(|e| anyhow::anyhow!("{e}"))?;
426
427 match &target {
429 sesame_workspace::CloneTarget::WorkspaceGit(_) => {
430 println!("Cloned workspace.git to org directory: {}", rp.display());
431 println!(" Peer repos will be cloned as siblings inside this directory.");
432 }
433 sesame_workspace::CloneTarget::Regular(_) => {
434 println!("Cloned to: {}", rp.display());
435 }
436 }
437 rp
438 };
439
440 if let Some(ref profile_name) = profile {
442 let _validated = TrustProfileName::try_from(profile_name.as_str())
443 .map_err(|e| anyhow::anyhow!("invalid profile name: {e}"))?;
444 let mut ws_config = core_config::load_workspace_config().unwrap_or_default();
445 sesame_workspace::config::add_link(
446 &mut ws_config,
447 &result_path.display().to_string(),
448 profile_name,
449 );
450 core_config::save_workspace_config(&ws_config)
451 .map_err(|e| anyhow::anyhow!("{e}"))?;
452 println!("Linked -> profile \"{}\"", profile_name);
453 }
454
455 Ok(())
456 }
457
458 WorkspaceCmd::List {
459 server,
460 org,
461 profile,
462 format,
463 } => {
464 let config = core_config::load_workspace_config().unwrap_or_default();
465 let mut workspaces = sesame_workspace::discover::discover_workspaces(&config)
466 .map_err(|e| anyhow::anyhow!("{e}"))?;
467
468 if let Some(ref s) = server {
469 workspaces.retain(|w| w.convention.server == *s);
470 }
471 if let Some(ref o) = org {
472 workspaces.retain(|w| w.convention.org == *o);
473 }
474 if let Some(ref p) = profile {
475 workspaces.retain(|w| w.linked_profile.as_deref() == Some(p.as_str()));
476 }
477
478 match format {
479 WorkspaceListFormat::Table => {
480 if workspaces.is_empty() {
481 println!("No workspaces found.");
482 return Ok(());
483 }
484
485 let mut groups: std::collections::BTreeMap<
487 (String, String),
488 Vec<&sesame_workspace::DiscoveredWorkspace>,
489 > = std::collections::BTreeMap::new();
490 for ws in &workspaces {
491 let key = (ws.convention.server.clone(), ws.convention.org.clone());
492 groups.entry(key).or_default().push(ws);
493 }
494
495 let mut total_repos = 0usize;
496 for ((srv, org_name), entries) in &groups {
497 let has_ws = entries.iter().any(|e| e.is_workspace_git);
499 let ws_tag = if has_ws {
500 format!(" {}", "(workspace)".dimmed())
501 } else {
502 String::new()
503 };
504 println!("{}{ws_tag}", format!("{srv}/{org_name}").bold(),);
505
506 for ws in entries {
508 if ws.is_workspace_git {
509 continue; }
511 total_repos += 1;
512 let repo_name = ws.convention.repo.as_deref().unwrap_or("?");
513
514 let branch = sesame_workspace::git::current_branch(&ws.path)
515 .unwrap_or_else(|_| "?".into());
516 let commit = sesame_workspace::git::head_commit_short(&ws.path)
517 .ok()
518 .flatten()
519 .unwrap_or_else(|| "?".into());
520 let clean = sesame_workspace::git::is_clean(&ws.path).unwrap_or(true);
521 let status_str = if clean {
522 "clean".green().to_string()
523 } else {
524 "dirty".yellow().to_string()
525 };
526
527 let profile_str = match &ws.linked_profile {
528 Some(p) => format!(" profile: {}", p.green()),
529 None => String::new(),
530 };
531
532 println!(
533 " {:<20} {:<14} {} {}{profile_str}",
534 repo_name,
535 branch,
536 commit.dimmed(),
537 status_str,
538 );
539 }
540 }
541
542 let org_count = groups.len();
543 let server_count = groups
544 .keys()
545 .map(|(s, _)| s.as_str())
546 .collect::<std::collections::BTreeSet<_>>()
547 .len();
548 println!(
549 "\n{}",
550 format!(
551 "{server_count} server{}, {org_count} org{}, {total_repos} repo{}",
552 if server_count != 1 { "s" } else { "" },
553 if org_count != 1 { "s" } else { "" },
554 if total_repos != 1 { "s" } else { "" },
555 )
556 .dimmed(),
557 );
558 }
559 WorkspaceListFormat::Json => {
560 let json: Vec<serde_json::Value> = workspaces
561 .iter()
562 .map(|ws| {
563 serde_json::json!({
564 "server": ws.convention.server,
565 "org": ws.convention.org,
566 "repo": ws.convention.repo,
567 "profile": ws.linked_profile,
568 "path": ws.path.display().to_string(),
569 "is_workspace_git": ws.is_workspace_git,
570 })
571 })
572 .collect();
573 println!("{}", serde_json::to_string_pretty(&json)?);
574 }
575 }
576 Ok(())
577 }
578
579 WorkspaceCmd::Status { path, verbose } => {
580 let path = resolve_workspace_path(path)?;
581 let config = core_config::load_workspace_config().unwrap_or_default();
582 let root = sesame_workspace::config::resolve_root(&config);
583
584 let conv = sesame_workspace::convention::parse_path(&root, &path)
585 .map_err(|e| anyhow::anyhow!("{e}"))?;
586 let remote = sesame_workspace::git::remote_url(&path)
587 .ok()
588 .flatten()
589 .unwrap_or_else(|| "unknown".into());
590 let branch =
591 sesame_workspace::git::current_branch(&path).unwrap_or_else(|_| "unknown".into());
592 let clean = sesame_workspace::git::is_clean(&path).unwrap_or(false);
593
594 let effective =
596 sesame_workspace::config::resolve_effective_config(&config, &path, &root)
597 .map_err(|e| anyhow::anyhow!("{e}"))?;
598 let in_ws_git = sesame_workspace::convention::is_inside_workspace_git(&path);
599
600 println!("Workspace: {}", path.display());
601 println!("Remote: {remote}");
602 println!("Branch: {branch}");
603
604 let head_short = sesame_workspace::git::head_commit_short(&path)
606 .ok()
607 .flatten()
608 .unwrap_or_else(|| "(unborn)".into());
609 let head_summary = sesame_workspace::git::head_commit_summary(&path)
610 .ok()
611 .flatten()
612 .unwrap_or_default();
613 let tracking_short =
614 sesame_workspace::git::remote_tracking_commit_short(&path, &branch)
615 .ok()
616 .flatten();
617 print!("Commit: {head_short}");
618 if !head_summary.is_empty() {
619 print!(" {head_summary}");
620 }
621 println!();
622 if let Some(ref tracking) = tracking_short {
623 if *tracking != head_short {
624 println!(
625 "Tracking: {} (origin/{branch} — {})",
626 tracking,
627 "behind".yellow(),
628 );
629 } else {
630 println!("Tracking: {tracking} (origin/{branch} — up to date)");
631 }
632 }
633
634 let status_str = if clean {
636 "clean".green().to_string()
637 } else {
638 "dirty".yellow().to_string()
639 };
640 println!("Status: {status_str}");
641 println!(
642 "Profile: {}",
643 effective.profile.as_deref().unwrap_or("(none)")
644 );
645 println!(
646 "Namespace: {} ({})",
647 conv.org,
648 if in_ws_git {
649 "workspace.git"
650 } else {
651 "no workspace.git"
652 }
653 );
654
655 if verbose {
656 println!(
657 "Convention: {} / {} / {} / {} / {}",
658 root.display(),
659 config.settings.user,
660 conv.server,
661 conv.org,
662 conv.repo.as_deref().unwrap_or("(workspace.git)")
663 );
664
665 if let Ok(output) = std::process::Command::new("du")
667 .arg("-sh")
668 .arg("--")
669 .arg(&path)
670 .output()
671 && let Ok(s) = String::from_utf8(output.stdout)
672 && let Some(size) = s.split_whitespace().next()
673 {
674 println!("Disk: {size}");
675 }
676 }
677 Ok(())
678 }
679
680 WorkspaceCmd::Link { profile, path } => {
681 let _validated = TrustProfileName::try_from(profile.as_str())
682 .map_err(|e| anyhow::anyhow!("invalid profile name: {e}"))?;
683
684 let path = resolve_workspace_path(path)?;
685
686 let mut config = core_config::load_workspace_config().unwrap_or_default();
687 sesame_workspace::config::add_link(&mut config, &path.display().to_string(), &profile);
688 core_config::save_workspace_config(&config).map_err(|e| anyhow::anyhow!("{e}"))?;
689 println!("Linked {} -> profile \"{}\"", path.display(), profile);
690 Ok(())
691 }
692
693 WorkspaceCmd::Unlink { path } => {
694 let path = resolve_workspace_path(path)?;
695 let mut config = core_config::load_workspace_config().unwrap_or_default();
696 let path_str = path.display().to_string();
697 if sesame_workspace::config::remove_link(&mut config, &path_str) {
698 core_config::save_workspace_config(&config).map_err(|e| anyhow::anyhow!("{e}"))?;
699 println!("Unlinked {}", path.display());
700 } else {
701 println!("No link found for {}", path.display());
702 }
703 Ok(())
704 }
705
706 WorkspaceCmd::Shell {
707 profile,
708 path,
709 shell,
710 prefix,
711 command,
712 } => {
713 let path = resolve_workspace_path(path)?;
714 let config = core_config::load_workspace_config().unwrap_or_default();
715 let root = sesame_workspace::config::resolve_root(&config);
716
717 let effective =
719 sesame_workspace::config::resolve_effective_config(&config, &path, &root)
720 .map_err(|e| anyhow::anyhow!("{e}"))?;
721
722 let profile_csv = profile
724 .or(effective.profile)
725 .or_else(|| std::env::var("SESAME_PROFILES").ok())
726 .unwrap_or_else(|| core_types::DEFAULT_PROFILE_NAME.into());
727
728 let specs = parse_profile_specs(&profile_csv);
729 let secret_prefix = prefix.or(effective.secret_prefix);
730
731 let client = connect().await?;
733 let env_vars =
734 fetch_multi_profile_secrets(&client, &specs, secret_prefix.as_deref()).await?;
735
736 let (bin, args, is_interactive) = if !command.is_empty() {
738 (command[0].clone(), command[1..].to_vec(), false)
739 } else {
740 let shell_bin = shell
741 .or_else(|| std::env::var("SHELL").ok())
742 .unwrap_or_else(|| "/bin/sh".into());
743 (shell_bin, Vec::new(), true)
744 };
745
746 let mut cmd = std::process::Command::new(&bin);
747 cmd.args(&args);
748 cmd.current_dir(&path);
749 cmd.env("SESAME_PROFILES", &profile_csv);
750 cmd.env("SESAME_WORKSPACE", path.display().to_string());
751
752 for (k, v) in &effective.env {
754 cmd.env(k, v);
755 }
756
757 for (k, v) in &env_vars {
759 let val_str = String::from_utf8_lossy(v);
760 cmd.env(k, val_str.as_ref());
761 }
762
763 if is_interactive {
764 println!(
765 "Entering workspace shell (profiles: {profile_csv}, {} secrets injected)",
766 env_vars.len()
767 );
768 }
769 let status = cmd.status().context("failed to spawn command")?;
770
771 for (_, mut v) in env_vars {
773 v.zeroize();
774 }
775
776 std::process::exit(status.code().unwrap_or(1));
777 }
778
779 WorkspaceCmd::Config(sub) => match sub {
780 WorkspaceConfigCmd::Show { path } => {
781 let path = resolve_workspace_path(path)?;
782 let config = core_config::load_workspace_config().unwrap_or_default();
783 let root = sesame_workspace::config::resolve_root(&config);
784
785 let effective =
786 sesame_workspace::config::resolve_effective_config(&config, &path, &root)
787 .map_err(|e| anyhow::anyhow!("{e}"))?;
788
789 println!("Workspace: {}", path.display());
790 println!(
791 "Profile: {} (source: {})",
792 effective.profile.as_deref().unwrap_or("(none)"),
793 if effective.provenance.profile_source.is_empty() {
794 "default"
795 } else {
796 effective.provenance.profile_source
797 }
798 );
799 if let Some(ref prefix) = effective.secret_prefix {
800 println!(
801 "Secret prefix: {prefix} (source: {})",
802 effective.provenance.secret_prefix_source
803 );
804 }
805 if !effective.env.is_empty() {
806 println!("Environment:");
807 for (k, v) in &effective.env {
808 println!(" {k}={v}");
809 }
810 }
811 if !effective.tags.is_empty() {
812 println!("Tags: {}", effective.tags.join(", "));
813 }
814 Ok(())
815 }
816 },
817 }
818}