Skip to main content

cli/commands/
preset.rs

1use std::path::PathBuf;
2
3use clap::{Args, Subcommand, ValueEnum};
4
5#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
6pub enum PresetTemplateKind {
7    App,
8    Shell,
9    Sys,
10}
11
12#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
13pub enum PresetReportFormat {
14    Text,
15    Json,
16}
17
18#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
19pub enum PresetPlatform {
20    Macos,
21    Linux,
22    Windows,
23}
24
25#[derive(Args, Debug)]
26pub struct ExportCommand {
27    /// Directory to export presets into. Defaults to the configured presets_dir.
28    #[arg(value_name = "DIR")]
29    pub dir: Option<PathBuf>,
30    /// Overwrite existing files
31    #[arg(long, short = 'f')]
32    pub force: bool,
33}
34
35#[derive(Args, Debug)]
36pub struct CopyCommand {
37    /// Built-in preset to copy (app/name, shell/name, or sys/name)
38    #[arg(value_name = "KIND/NAME", value_parser = parse_copy_target)]
39    pub target: String,
40    /// Overwrite existing files
41    #[arg(long, short = 'f')]
42    pub force: bool,
43}
44
45pub(crate) fn parse_copy_target(value: &str) -> Result<String, String> {
46    if value.contains('\\') {
47        return Err(format!(
48            "invalid preset target '{value}': expected app/name, shell/name, or sys/name"
49        ));
50    }
51
52    let mut parts = value.split('/');
53    let kind = parts.next().unwrap_or_default();
54    let name = parts.next().unwrap_or_default();
55    if parts.next().is_some()
56        || !matches!(kind, "app" | "shell" | "sys")
57        || name.is_empty()
58        || matches!(name, "." | "..")
59    {
60        return Err(format!(
61            "invalid preset target '{value}': expected app/name, shell/name, or sys/name"
62        ));
63    }
64    Ok(value.to_string())
65}
66
67#[derive(Args, Debug)]
68pub struct LinkCommand {
69    /// Directory to use as the external presets source.
70    #[arg(value_name = "PATH")]
71    pub path: PathBuf,
72    /// Create the directory if it does not already exist.
73    #[arg(long)]
74    pub create: bool,
75    /// Run external shell source changes on their next invocation.
76    #[arg(long)]
77    pub live: bool,
78}
79
80#[derive(Args, Debug)]
81pub struct OverlayLinkCommand {
82    /// Directory to use as the presets overlay. Mutually exclusive with --git.
83    #[arg(value_name = "PATH", conflicts_with = "git")]
84    pub path: Option<PathBuf>,
85    /// Git URL for a shine-managed overlay. shine clones it (`--depth 1`) under
86    /// `~/.shine/overlay` and keeps it mirrored to the remote tip on `shine preset pull`.
87    #[arg(long, value_name = "URL")]
88    pub git: Option<String>,
89    /// Branch to track for --git. Defaults to the remote's default branch.
90    #[arg(long, value_name = "BRANCH", requires = "git")]
91    pub branch: Option<String>,
92    /// Create the directory if it does not already exist (path mode only).
93    #[arg(long)]
94    pub create: bool,
95}
96
97#[derive(Subcommand, Debug)]
98pub enum OverlayCommands {
99    /// Set the presets overlay in the active config (local PATH or --git URL).
100    Link(OverlayLinkCommand),
101    /// Remove the presets overlay from the active config.
102    Unlink,
103    /// Show information about the active presets overlay.
104    Info,
105}
106
107#[derive(Subcommand, Debug)]
108pub enum PresetCommands {
109    /// Create a shine.toml template for a new app, shell, or sys preset
110    New {
111        #[arg(value_enum)]
112        kind: PresetTemplateKind,
113        /// Overwrite shine.toml if it already exists
114        #[arg(long, short = 'f')]
115        force: bool,
116    },
117    /// Generate the versioned JSON Schema and command-help reference
118    Schema {
119        /// Output format
120        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
121        format: PresetReportFormat,
122    },
123    /// Statically validate preset metadata and referenced files
124    Validate {
125        /// Preset repository, category directory, or shine.toml (defaults to current directory)
126        #[arg(value_name = "PATH", default_value = ".")]
127        path: PathBuf,
128        /// Output format
129        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
130        format: PresetReportFormat,
131    },
132    /// Check Preset author quality, portability, and permission minimization
133    Lint {
134        /// Preset repository, category directory, or shine.toml (defaults to current directory)
135        #[arg(value_name = "PATH", default_value = ".")]
136        path: PathBuf,
137        /// Output format
138        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
139        format: PresetReportFormat,
140        /// Exit with status 1 when lint warnings are present
141        #[arg(long)]
142        deny_warnings: bool,
143    },
144    /// Preview a first install against deterministic synthetic host state
145    Plan {
146        /// One app, shell, or sys category directory, or its shine.toml
147        #[arg(value_name = "CATEGORY", default_value = ".")]
148        path: PathBuf,
149        /// Target platform for the hypothetical authoring report
150        #[arg(long, value_enum)]
151        platform: PresetPlatform,
152        /// Output format
153        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
154        format: PresetReportFormat,
155    },
156    /// Run declarative shine.test.toml authoring fixtures
157    Test {
158        /// One app, shell, or sys category directory, or its shine.toml
159        #[arg(value_name = "CATEGORY", default_value = ".")]
160        path: PathBuf,
161        /// Output format
162        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
163        format: PresetReportFormat,
164    },
165    /// Build a deterministic, policy-gated Preset bundle
166    Pack {
167        /// One app, shell, or sys category directory, or its shine.toml
168        #[arg(value_name = "CATEGORY", default_value = ".")]
169        path: PathBuf,
170        /// Destination tar.gz bundle path (must be outside the category)
171        #[arg(long, value_name = "FILE")]
172        output: PathBuf,
173        /// Replace an existing output file
174        #[arg(long, short = 'f')]
175        force: bool,
176        /// Report output format
177        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
178        format: PresetReportFormat,
179    },
180    /// Review and migrate legacy Preset metadata for Shine 2
181    Migrate {
182        /// Preset repository, category directory, or shine.toml; defaults to active sources
183        #[arg(value_name = "PATH")]
184        path: Option<PathBuf>,
185        /// Preview migration without creating backups or changing files
186        #[arg(long, conflicts_with = "yes")]
187        dry_run: bool,
188        /// Apply the displayed migration without prompting
189        #[arg(long)]
190        yes: bool,
191        /// Report output format; JSON apply requires --yes
192        #[arg(long, value_enum, default_value_t = PresetReportFormat::Text)]
193        format: PresetReportFormat,
194    },
195    /// Copy built-in presets to a directory for local customization
196    Export(ExportCommand),
197    /// Copy one built-in preset into the current directory
198    Copy(CopyCommand),
199    /// Set the external presets directory in the active config
200    Link(LinkCommand),
201    /// Remove the external presets directory from the active config
202    Unlink,
203    /// Manage the personal presets overlay directory
204    Overlay {
205        #[command(subcommand)]
206        command: OverlayCommands,
207    },
208    /// Pull Git-managed preset and overlay repositories
209    Pull,
210}