Skip to main content

systemprompt_cli/commands/core/content/
show.rs

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