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