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