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