systemprompt_cli/commands/core/content/
show.rs1use 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}