systemprompt_cli/commands/core/content/files/
unlink.rs1use anyhow::{Result, anyhow};
7use clap::Args;
8use systemprompt_database::DbPool;
9use systemprompt_files::FileRepository;
10use systemprompt_identifiers::{ContentId, FileId};
11use systemprompt_runtime::AppContext;
12
13use crate::CliConfig;
14use crate::commands::core::files::types::ContentUnlinkOutput;
15use crate::interactive::Prompter;
16use crate::shared::CommandOutput;
17
18#[derive(Debug, Clone, Args)]
19pub struct UnlinkArgs {
20 #[arg(value_name = "FILE_ID", help = "File ID (UUID format)")]
21 pub file: String,
22
23 #[arg(long, help = "Content ID")]
24 pub content: String,
25
26 #[arg(
27 short = 'y',
28 long,
29 help = "Skip confirmation (required in non-interactive mode)"
30 )]
31 pub yes: bool,
32
33 #[arg(long, help = "Preview unlink without executing")]
34 pub dry_run: bool,
35}
36
37pub(super) async fn execute(
38 args: UnlinkArgs,
39 prompter: &dyn Prompter,
40 config: &CliConfig,
41) -> Result<CommandOutput> {
42 let ctx = AppContext::new().await?;
43 execute_with_pool(args, prompter, ctx.db_pool(), config).await
44}
45
46pub async fn execute_with_pool(
47 args: UnlinkArgs,
48 prompter: &dyn Prompter,
49 pool: &DbPool,
50 config: &CliConfig,
51) -> Result<CommandOutput> {
52 let file_id = parse_file_id(&args.file)?;
53 let content_id = ContentId::new(args.content.clone());
54
55 if !args.yes {
56 if config.is_interactive() {
57 let confirmed = prompter.confirm(
58 &format!(
59 "Unlink file '{}' from content '{}'?",
60 args.file, args.content
61 ),
62 false,
63 )?;
64
65 if !confirmed {
66 return Err(anyhow!("Unlink cancelled by user"));
67 }
68 } else {
69 return Err(anyhow!(
70 "--yes is required to unlink files in non-interactive mode"
71 ));
72 }
73 }
74
75 if args.dry_run {
76 let output = ContentUnlinkOutput {
77 file_id,
78 content_id,
79 message: format!(
80 "[DRY-RUN] Would unlink file '{}' from content '{}'",
81 args.file, args.content
82 ),
83 };
84 return Ok(CommandOutput::card_value("File Unlink (Dry Run)", &output));
85 }
86
87 let service = FileRepository::new(pool)?;
88
89 service.unlink_from_content(&content_id, &file_id).await?;
90
91 let output = ContentUnlinkOutput {
92 file_id,
93 content_id,
94 message: "File unlinked from content successfully".to_owned(),
95 };
96
97 Ok(CommandOutput::card_value("File Unlinked", &output))
98}
99
100fn parse_file_id(id: &str) -> Result<FileId> {
101 uuid::Uuid::parse_str(id).map_err(|_e| {
102 anyhow!(
103 "Invalid file ID format. Expected UUID like 'b75940ac-c50f-4d46-9fdd-ebb4970b2a7d', \
104 got '{}'",
105 id
106 )
107 })?;
108 Ok(FileId::new(id.to_owned()))
109}