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