Skip to main content

systemprompt_cli/commands/core/content/files/
unlink.rs

1//! `core content files unlink` command.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}