Skip to main content

systemprompt_cli/commands/web/assets/
list.rs

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