auditkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui.rs ADDED
@@ -0,0 +1,84 @@
1
+ use std::time::Duration;
2
+
3
+ use anyhow::Result;
4
+ use colored::Colorize;
5
+ use indicatif::{ProgressBar, ProgressStyle};
6
+
7
+ pub fn section(title: &str) {
8
+ println!("\n{}", format!("== {title} ==").bold());
9
+ }
10
+
11
+ pub fn bullet(value: &str) {
12
+ println!("{} {value}", "-".cyan());
13
+ }
14
+
15
+ pub fn saved(path: impl std::fmt::Display) {
16
+ println!("{} {}", "[saved]".green(), path);
17
+ }
18
+
19
+ pub fn error(message: impl std::fmt::Display) {
20
+ eprintln!("{} {}", "[error]".red(), message);
21
+ }
22
+
23
+ pub fn score_status(score: i32) -> &'static str {
24
+ if score >= 85 {
25
+ "strong"
26
+ } else if score >= 65 {
27
+ "okay"
28
+ } else {
29
+ "needs work"
30
+ }
31
+ }
32
+
33
+ pub fn with_task<T>(label: &str, task: impl FnOnce() -> Result<T>) -> Result<T> {
34
+ let spinner = ProgressBar::new_spinner();
35
+ spinner.set_style(
36
+ ProgressStyle::with_template("{spinner:.cyan} {msg}")
37
+ .unwrap_or_else(|_| ProgressStyle::default_spinner()),
38
+ );
39
+ spinner.set_message(label.to_string());
40
+ spinner.enable_steady_tick(Duration::from_millis(80));
41
+
42
+ match task() {
43
+ Ok(value) => {
44
+ spinner.finish_with_message(format!("Done: {label}"));
45
+ Ok(value)
46
+ }
47
+ Err(error) => {
48
+ spinner.finish_with_message(format!("Failed: {label}"));
49
+ Err(error)
50
+ }
51
+ }
52
+ }
53
+
54
+ pub fn help() {
55
+ println!(
56
+ "{}",
57
+ "+--------------------------------------------------------+\n\
58
+ | Audit Kit |\n\
59
+ +--------------------------------------------------------+\n\
60
+ | ak new create audit workspace |\n\
61
+ | ak check latest run + save quick website feedback |\n\
62
+ | ak security latest run + save security header check |\n\
63
+ | ak lighthouse latest run + save Lighthouse audit |\n\
64
+ | ak inspect latest run check + security + Lighthouse |\n\
65
+ | ak report latest create final report + client email |\n\
66
+ | ak list show audit folders |\n\
67
+ | ak check <url> quick one-off website check |\n\
68
+ | ak security <url> quick one-off security check |\n\
69
+ | ak lighthouse <url> quick one-off Lighthouse check |\n\
70
+ +--------------------------------------------------------+"
71
+ );
72
+ }
73
+
74
+ #[cfg(test)]
75
+ mod tests {
76
+ use super::*;
77
+
78
+ #[test]
79
+ fn score_status_matches_cli_labels() {
80
+ assert_eq!(score_status(95), "strong");
81
+ assert_eq!(score_status(74), "okay");
82
+ assert_eq!(score_status(40), "needs work");
83
+ }
84
+ }
@@ -0,0 +1,111 @@
1
+ use std::collections::BTreeMap;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use anyhow::{Context, Result};
6
+
7
+ use crate::audit::AuditInput;
8
+ use crate::templates;
9
+
10
+ #[derive(Debug, Clone)]
11
+ pub struct Workspace {
12
+ pub root: PathBuf,
13
+ pub audits_dir: PathBuf,
14
+ }
15
+
16
+ impl Workspace {
17
+ pub fn discover() -> Result<Self> {
18
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
19
+ Ok(Self {
20
+ audits_dir: manifest_dir.join("audits"),
21
+ root: manifest_dir,
22
+ })
23
+ }
24
+
25
+ pub fn audit_folder(&self, folder_name: &str) -> PathBuf {
26
+ self.audits_dir.join(folder_name)
27
+ }
28
+
29
+ pub fn create_audit(&self, audit: &AuditInput) -> Result<PathBuf> {
30
+ let folder = self.audit_folder(&format!("{}-{}", audit.created_at, audit.slug));
31
+ fs::create_dir_all(&folder)?;
32
+
33
+ for (relative, content) in templates::create_audit_files(audit) {
34
+ let path = folder.join(relative);
35
+ if let Some(parent) = path.parent() {
36
+ fs::create_dir_all(parent)?;
37
+ }
38
+ fs::write(path, content)?;
39
+ }
40
+
41
+ Ok(folder)
42
+ }
43
+
44
+ pub fn list_audits(&self) -> Result<Vec<String>> {
45
+ fs::create_dir_all(&self.audits_dir)?;
46
+ let mut folders = Vec::new();
47
+
48
+ for entry in fs::read_dir(&self.audits_dir)? {
49
+ let entry = entry?;
50
+ if entry.file_type()?.is_dir() {
51
+ folders.push(entry.file_name().to_string_lossy().to_string());
52
+ }
53
+ }
54
+
55
+ folders.sort();
56
+ Ok(folders)
57
+ }
58
+
59
+ pub fn resolve_target(&self, target: Option<&str>) -> Result<String> {
60
+ let folders = self.list_audits()?;
61
+ if folders.is_empty() {
62
+ anyhow::bail!("No audits found.");
63
+ }
64
+
65
+ match target {
66
+ None | Some("latest") => Ok(folders.last().expect("non-empty").to_string()),
67
+ Some(value) if folders.iter().any(|folder| folder == value) => Ok(value.to_string()),
68
+ Some(value) => anyhow::bail!("Audit folder not found: {value}"),
69
+ }
70
+ }
71
+
72
+ pub fn read_audit_files(&self, folder_name: &str) -> Result<BTreeMap<String, String>> {
73
+ let folder = self.audit_folder(folder_name);
74
+ let mut files = BTreeMap::new();
75
+ read_markdown_files(&folder, &folder, &mut files)?;
76
+ Ok(files)
77
+ }
78
+
79
+ pub fn write_audit_file(
80
+ &self,
81
+ folder_name: &str,
82
+ filename: &str,
83
+ content: &str,
84
+ ) -> Result<PathBuf> {
85
+ let path = self.audit_folder(folder_name).join(filename);
86
+ if let Some(parent) = path.parent() {
87
+ fs::create_dir_all(parent)?;
88
+ }
89
+ fs::write(&path, content)?;
90
+ Ok(path)
91
+ }
92
+ }
93
+
94
+ fn read_markdown_files(
95
+ root: &Path,
96
+ current: &Path,
97
+ files: &mut BTreeMap<String, String>,
98
+ ) -> Result<()> {
99
+ for entry in fs::read_dir(current).with_context(|| format!("Reading {}", current.display()))? {
100
+ let entry = entry?;
101
+ let path = entry.path();
102
+ if entry.file_type()?.is_dir() {
103
+ read_markdown_files(root, &path, files)?;
104
+ } else if path.extension().is_some_and(|extension| extension == "md") {
105
+ let relative = path.strip_prefix(root)?.to_string_lossy().to_string();
106
+ files.insert(relative, fs::read_to_string(path)?);
107
+ }
108
+ }
109
+
110
+ Ok(())
111
+ }