1use dialoguer::{theme::ColorfulTheme, Confirm, Input, FuzzySelect, MultiSelect};
2
3#[derive(Debug, Clone)]
4pub struct InstanceConfig {
5 pub name: String,
6 pub domain: String,
7 pub description: String,
8 pub database_url: String,
9 pub redis_url: String,
10 pub theme: String,
11 pub feature_content: bool,
13 pub feature_social: bool,
14 pub feature_hubs: bool,
15 pub feature_docs: bool,
16 pub feature_video: bool,
17 pub feature_contests: bool,
18 pub feature_learning: bool,
19 pub feature_explainers: bool,
20 pub feature_federation: bool,
21 pub feature_admin: bool,
22 pub contest_creation: String,
24 pub content_types: Vec<String>,
26 pub auth_email_password: bool,
28 pub auth_magic_link: bool,
29 pub auth_passkeys: bool,
30 pub auth_github: bool,
31 pub auth_google: bool,
32 pub use_docker: bool,
34}
35
36impl InstanceConfig {
37 pub fn with_defaults(name: &str) -> Self {
38 Self {
39 name: sanitize_value(name),
40 domain: format!("{}.localhost", sanitize_value(name)),
41 description: format!("A CommonPub community: {}", sanitize_value(name)),
42 database_url: "postgresql://commonpub:commonpub_dev@localhost:5432/commonpub".to_string(),
43 redis_url: "redis://localhost:6379".to_string(),
44 theme: "base".to_string(),
45 feature_content: true,
46 feature_social: true,
47 feature_hubs: true,
48 feature_docs: true,
49 feature_video: true,
50 feature_contests: false,
51 feature_learning: true,
52 feature_explainers: true,
53 feature_federation: false,
54 feature_admin: false,
55 contest_creation: "admin".to_string(),
56 content_types: vec!["project".to_string(), "article".to_string(), "blog".to_string(), "explainer".to_string()],
57 auth_email_password: true,
58 auth_magic_link: false,
59 auth_passkeys: false,
60 auth_github: false,
61 auth_google: false,
62 use_docker: true,
63 }
64 }
65}
66
67pub fn sanitize_value(input: &str) -> String {
69 input
70 .chars()
71 .filter(|c| !c.is_control())
72 .collect::<String>()
73 .replace('\'', "")
74}
75
76pub fn prompt_config(name: &str) -> Result<InstanceConfig, Box<dyn std::error::Error>> {
77 let theme = ColorfulTheme::default();
78
79 println!("\n ┌─ CommonPub Setup ─────────────────────┐");
80 println!(" │ Let's configure your instance. │");
81 println!(" └────────────────────────────────────────┘\n");
82
83 let instance_name: String = Input::with_theme(&theme)
86 .with_prompt("Instance name")
87 .default(name.to_string())
88 .interact_text()?;
89 let instance_name = sanitize_value(&instance_name);
90
91 let domain: String = Input::with_theme(&theme)
92 .with_prompt("Domain")
93 .default(format!("{}.localhost", name))
94 .interact_text()?;
95 let domain = sanitize_value(&domain);
96
97 let description: String = Input::with_theme(&theme)
98 .with_prompt("Description")
99 .default(format!("A CommonPub community: {}", name))
100 .interact_text()?;
101 let description = sanitize_value(&description);
102
103 let themes = vec!["base", "deepwood", "hackbuild", "deveco"];
106 let theme_idx = FuzzySelect::with_theme(&theme)
107 .with_prompt("Theme")
108 .items(&themes)
109 .default(0)
110 .interact()?;
111
112 let use_docker = Confirm::with_theme(&theme)
115 .with_prompt("Include Docker Compose? (Postgres, Redis, Meilisearch)")
116 .default(true)
117 .interact()?;
118
119 let database_url: String = if use_docker {
120 "postgresql://commonpub:commonpub_dev@localhost:5432/commonpub".to_string()
121 } else {
122 Input::with_theme(&theme)
123 .with_prompt("Database URL")
124 .default("postgresql://commonpub:commonpub_dev@localhost:5432/commonpub".to_string())
125 .interact_text()?
126 };
127
128 let redis_url: String = if use_docker {
129 "redis://localhost:6379".to_string()
130 } else {
131 Input::with_theme(&theme)
132 .with_prompt("Redis URL")
133 .default("redis://localhost:6379".to_string())
134 .interact_text()?
135 };
136
137 println!("\n Features — select what to enable:");
140
141 let feature_items = vec![
142 ("Content system (CRUD, publishing, slugs)", true),
143 ("Social (likes, comments, bookmarks)", true),
144 ("Hubs (communities, feeds, moderation)", true),
145 ("Docs (CodeMirror editor, versioning)", true),
146 ("Video content type", true),
147 ("Contests", false),
148 ("Learning paths (enrollment, progress)", true),
149 ("Explainers (interactive modules)", true),
150 ("Federation (ActivityPub)", false),
151 ("Admin panel (user mgmt, reports)", false),
152 ];
153 let feature_labels: Vec<&str> = feature_items.iter().map(|(l, _)| *l).collect();
154 let feature_defaults: Vec<bool> = feature_items.iter().map(|(_, d)| *d).collect();
155
156 let selected = MultiSelect::with_theme(&theme)
157 .with_prompt("Features")
158 .items(&feature_labels)
159 .defaults(&feature_defaults)
160 .interact()?;
161
162 let feature_content = selected.contains(&0);
163 let feature_social = selected.contains(&1);
164 let feature_hubs = selected.contains(&2);
165 let feature_docs = selected.contains(&3);
166 let feature_video = selected.contains(&4);
167 let feature_contests = selected.contains(&5);
168 let feature_learning = selected.contains(&6);
169 let feature_explainers = selected.contains(&7);
170 let feature_federation = selected.contains(&8);
171 let feature_admin = selected.contains(&9);
172
173 let content_types = if feature_content {
176 println!("\n Content types — select what content can be created:");
177
178 let ct_items = vec![
179 ("Projects", true),
180 ("Articles", true),
181 ("Blogs", true),
182 ("Explainers", true),
183 ];
184 let ct_labels: Vec<&str> = ct_items.iter().map(|(l, _)| *l).collect();
185 let ct_defaults: Vec<bool> = ct_items.iter().map(|(_, d)| *d).collect();
186
187 let ct_selected = MultiSelect::with_theme(&theme)
188 .with_prompt("Content types")
189 .items(&ct_labels)
190 .defaults(&ct_defaults)
191 .interact()?;
192
193 let type_names = vec!["project", "article", "blog", "explainer"];
194 ct_selected.iter().map(|&i| type_names[i].to_string()).collect()
195 } else {
196 vec![]
197 };
198
199 let contest_creation = if feature_contests {
202 let options = vec!["admin — only admins", "staff — staff and admins", "open — any user"];
203 let idx = FuzzySelect::with_theme(&theme)
204 .with_prompt("Who can create contests?")
205 .items(&options)
206 .default(0)
207 .interact()?;
208 match idx {
209 0 => "admin",
210 1 => "staff",
211 _ => "open",
212 }.to_string()
213 } else {
214 "admin".to_string()
215 };
216
217 println!("\n Authentication — select sign-in methods:");
220
221 let auth_items = vec![
222 ("Email / password", true),
223 ("Magic link (passwordless email)", false),
224 ("Passkeys (WebAuthn)", false),
225 ("GitHub OAuth", false),
226 ("Google OAuth", false),
227 ];
228 let auth_labels: Vec<&str> = auth_items.iter().map(|(l, _)| *l).collect();
229 let auth_defaults: Vec<bool> = auth_items.iter().map(|(_, d)| *d).collect();
230
231 let auth_selected = MultiSelect::with_theme(&theme)
232 .with_prompt("Auth methods")
233 .items(&auth_labels)
234 .defaults(&auth_defaults)
235 .interact()?;
236
237 let auth_email_password = auth_selected.contains(&0);
238 let auth_magic_link = auth_selected.contains(&1);
239 let auth_passkeys = auth_selected.contains(&2);
240 let auth_github = auth_selected.contains(&3);
241 let auth_google = auth_selected.contains(&4);
242
243 Ok(InstanceConfig {
244 name: instance_name,
245 domain,
246 description,
247 database_url,
248 redis_url,
249 theme: themes[theme_idx].to_string(),
250 feature_content,
251 feature_social,
252 feature_hubs,
253 feature_docs,
254 feature_video,
255 feature_contests,
256 feature_learning,
257 feature_explainers,
258 feature_federation,
259 feature_admin,
260 contest_creation,
261 content_types,
262 auth_email_password,
263 auth_magic_link,
264 auth_passkeys,
265 auth_github,
266 auth_google,
267 use_docker,
268 })
269}