Skip to main content

systemprompt_cli/commands/core/content/
show.rs

1use super::types::ContentDetailOutput;
2use crate::cli_settings::CliConfig;
3use crate::shared::CommandResult;
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(
24    args: ShowArgs,
25    config: &CliConfig,
26) -> Result<CommandResult<ContentDetailOutput>> {
27    let ctx = AppContext::new().await?;
28    execute_with_pool(args, ctx.db_pool(), config).await
29}
30
31pub async fn execute_with_pool(
32    args: ShowArgs,
33    pool: &DbPool,
34    _config: &CliConfig,
35) -> Result<CommandResult<ContentDetailOutput>> {
36    let repo = ContentRepository::new(pool)?;
37    let locale = LocaleCode::new("en");
38
39    let content = if args.identifier.starts_with("content_")
40        || args.identifier.contains('-') && args.identifier.len() > 30
41    {
42        let id = ContentId::new(args.identifier.clone());
43        repo.get_by_id(&id)
44            .await?
45            .ok_or_else(|| anyhow!("Content not found: {}", args.identifier))?
46    } else if let Some(source_id) = args.source.as_ref() {
47        let source = SourceId::new(source_id.clone());
48        repo.get_by_source_and_slug(&source, &args.identifier, &locale)
49            .await?
50            .ok_or_else(|| {
51                anyhow!(
52                    "Content not found: slug '{}' in source '{}'",
53                    args.identifier,
54                    source_id
55                )
56            })?
57    } else {
58        let sources = repo.find_sources_by_slug(&args.identifier, &locale).await?;
59        match sources.as_slice() {
60            [] => return Err(anyhow!("No content with slug '{}' found", args.identifier)),
61            [only] => repo
62                .get_by_source_and_slug(only, &args.identifier, &locale)
63                .await?
64                .ok_or_else(|| anyhow!("Content not found: {}", args.identifier))?,
65            many => {
66                let list = many
67                    .iter()
68                    .map(SourceId::as_str)
69                    .collect::<Vec<_>>()
70                    .join(", ");
71                return Err(anyhow!(
72                    "Slug '{}' exists in multiple sources: [{}]. Re-run with --source <SOURCE> to \
73                     disambiguate.",
74                    args.identifier,
75                    list
76                ));
77            },
78        }
79    };
80
81    let keywords: Vec<String> = content
82        .keywords
83        .split(',')
84        .map(|s| s.trim().to_string())
85        .filter(|s| !s.is_empty())
86        .collect();
87
88    let output = ContentDetailOutput {
89        id: content.id,
90        slug: content.slug,
91        title: content.title,
92        description: if content.description.is_empty() {
93            None
94        } else {
95            Some(content.description)
96        },
97        body: content.body,
98        author: if content.author.is_empty() {
99            None
100        } else {
101            Some(content.author)
102        },
103        published_at: Some(content.published_at),
104        keywords,
105        kind: content.kind,
106        image: content.image,
107        category_id: content.category_id,
108        source_id: content.source_id,
109        version_hash: content.version_hash,
110        is_public: content.public,
111        updated_at: content.updated_at,
112    };
113
114    Ok(CommandResult::card(output).with_title("Content Details"))
115}