Skip to main content

systemprompt_cli/commands/core/content/
show.rs

1use super::types::ContentDetailOutput;
2use crate::cli_settings::CliConfig;
3use crate::shared::CommandOutput;
4use anyhow::{Result, anyhow};
5use clap::Args;
6use systemprompt_content::ContentRepository;
7use systemprompt_database::DbPool;
8use systemprompt_identifiers::{ContentId, LocaleCode, SourceId};
9use systemprompt_runtime::AppContext;
10
11#[derive(Debug, Args)]
12pub struct ShowArgs {
13    #[arg(help = "Content ID or slug")]
14    pub identifier: String,
15
16    #[arg(
17        long,
18        help = "Source ID — only required when the slug exists in more than one source"
19    )]
20    pub source: Option<String>,
21}
22
23pub async fn execute(args: ShowArgs, config: &CliConfig) -> Result<CommandOutput> {
24    let ctx = AppContext::new().await?;
25    execute_with_pool(args, ctx.db_pool(), config).await
26}
27
28pub async fn execute_with_pool(
29    args: ShowArgs,
30    pool: &DbPool,
31    _config: &CliConfig,
32) -> Result<CommandOutput> {
33    let repo = ContentRepository::new(pool)?;
34    let locale = LocaleCode::new("en");
35
36    let content = if args.identifier.starts_with("content_")
37        || args.identifier.contains('-') && args.identifier.len() > 30
38    {
39        let id = ContentId::new(args.identifier.clone());
40        repo.get_by_id(&id)
41            .await?
42            .ok_or_else(|| anyhow!("Content not found: {}", args.identifier))?
43    } else if let Some(source_id) = args.source.as_ref() {
44        let source = SourceId::new(source_id.clone());
45        repo.get_by_source_and_slug(&source, &args.identifier, &locale)
46            .await?
47            .ok_or_else(|| {
48                anyhow!(
49                    "Content not found: slug '{}' in source '{}'",
50                    args.identifier,
51                    source_id
52                )
53            })?
54    } else {
55        let sources = repo.find_sources_by_slug(&args.identifier, &locale).await?;
56        match sources.as_slice() {
57            [] => return Err(anyhow!("No content with slug '{}' found", args.identifier)),
58            [only] => repo
59                .get_by_source_and_slug(only, &args.identifier, &locale)
60                .await?
61                .ok_or_else(|| anyhow!("Content not found: {}", args.identifier))?,
62            many => {
63                let list = many
64                    .iter()
65                    .map(SourceId::as_str)
66                    .collect::<Vec<_>>()
67                    .join(", ");
68                return Err(anyhow!(
69                    "Slug '{}' exists in multiple sources: [{}]. Re-run with --source <SOURCE> to \
70                     disambiguate.",
71                    args.identifier,
72                    list
73                ));
74            },
75        }
76    };
77
78    let keywords: Vec<String> = content
79        .keywords
80        .split(',')
81        .map(|s| s.trim().to_owned())
82        .filter(|s| !s.is_empty())
83        .collect();
84
85    let output = ContentDetailOutput {
86        id: content.id,
87        slug: content.slug,
88        title: content.title,
89        description: if content.description.is_empty() {
90            None
91        } else {
92            Some(content.description)
93        },
94        body: content.body,
95        author: if content.author.is_empty() {
96            None
97        } else {
98            Some(content.author)
99        },
100        published_at: Some(content.published_at),
101        keywords,
102        kind: content.kind,
103        image: content.image,
104        category_id: content.category_id,
105        source_id: content.source_id,
106        version_hash: content.version_hash,
107        is_public: content.public,
108        updated_at: content.updated_at,
109    };
110
111    Ok(CommandOutput::card_value("Content Details", &output))
112}