systemprompt_cli/commands/web/assets/
list.rs1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc};
3use clap::{Args, ValueEnum};
4use walkdir::WalkDir;
5
6use crate::CliConfig;
7use crate::shared::CommandOutput;
8
9use super::super::paths::WebPaths;
10use super::super::types::{AssetSummary, AssetType};
11use super::asset_type::determine_asset_type;
12
13#[derive(Debug, Clone, Copy, ValueEnum, Default)]
14pub enum AssetTypeFilter {
15 #[default]
16 All,
17 Css,
18 Logo,
19 Favicon,
20 Font,
21 Image,
22}
23
24#[derive(Debug, Clone, Copy, Args)]
25pub struct ListArgs {
26 #[arg(long, value_enum, default_value = "all", help = "Filter by asset type")]
27 pub asset_type: AssetTypeFilter,
28}
29
30pub(super) fn execute(args: ListArgs, config: &CliConfig) -> Result<CommandOutput> {
31 execute_in_dir(args, config, &WebPaths::resolve()?.assets)
32}
33
34pub fn execute_in_dir(
35 args: ListArgs,
36 _config: &CliConfig,
37 assets_dir: &std::path::Path,
38) -> Result<CommandOutput> {
39 if !assets_dir.exists() {
40 let empty: Vec<AssetSummary> = vec![];
41 return Ok(CommandOutput::table_of(
42 vec!["path", "asset_type", "size_bytes", "modified"],
43 &empty,
44 )
45 .with_title("Assets"));
46 }
47
48 let mut assets: Vec<AssetSummary> = Vec::new();
49
50 for entry in WalkDir::new(assets_dir)
51 .follow_links(true)
52 .into_iter()
53 .filter_map(Result::ok)
54 {
55 let path = entry.path();
56
57 if !path.is_file() {
58 continue;
59 }
60
61 let relative_path = path
62 .strip_prefix(assets_dir)
63 .unwrap_or(path)
64 .to_string_lossy()
65 .to_string();
66
67 let asset_type = determine_asset_type(path, &relative_path);
68
69 if !matches_filter(asset_type, args.asset_type) {
70 continue;
71 }
72
73 let metadata = path.metadata().context("Failed to get file metadata")?;
74 let size_bytes = metadata.len();
75 let modified = metadata.modified().ok().map_or_else(
76 || "unknown".to_owned(),
77 |t| {
78 let datetime: DateTime<Utc> = t.into();
79 datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string()
80 },
81 );
82
83 assets.push(AssetSummary {
84 path: relative_path,
85 asset_type,
86 size_bytes,
87 modified,
88 });
89 }
90
91 assets.sort_by(|a, b| a.path.cmp(&b.path));
92
93 Ok(CommandOutput::table_of(
94 vec!["path", "asset_type", "size_bytes", "modified"],
95 &assets,
96 )
97 .with_title("Assets"))
98}
99
100fn matches_filter(asset_type: AssetType, filter: AssetTypeFilter) -> bool {
101 match filter {
102 AssetTypeFilter::All => true,
103 AssetTypeFilter::Css => asset_type == AssetType::Css,
104 AssetTypeFilter::Logo => asset_type == AssetType::Logo,
105 AssetTypeFilter::Favicon => asset_type == AssetType::Favicon,
106 AssetTypeFilter::Font => asset_type == AssetType::Font,
107 AssetTypeFilter::Image => {
108 asset_type == AssetType::Image
109 || asset_type == AssetType::Logo
110 || asset_type == AssetType::Favicon
111 },
112 }
113}