1pub mod accounts;
2pub mod format;
3pub mod migrations;
4pub mod secure_storage;
5
6use anyhow::{Context, Result};
7use colored::Colorize;
8use dirs::home_dir;
9use serde::{Deserialize, Serialize};
10use std::env;
11use std::path::PathBuf;
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Config {
15 pub api_key: Option<String>,
17 pub api_url: Option<String>,
18 pub ai_provider: Option<String>,
19 pub model: Option<String>,
20
21 pub tokens_max_input: Option<usize>,
23 pub tokens_max_output: Option<u32>,
24
25 pub commit_type: Option<String>,
27 pub emoji: Option<bool>,
28 pub description: Option<bool>,
29 pub description_capitalize: Option<bool>,
30 pub description_add_period: Option<bool>,
31 pub description_max_length: Option<usize>,
32
33 pub language: Option<String>,
35 pub message_template_placeholder: Option<String>,
36 pub prompt_module: Option<String>,
37
38 pub gitpush: Option<bool>,
40 pub one_line_commit: Option<bool>,
41 pub why: Option<bool>,
42 pub omit_scope: Option<bool>,
43 pub generate_count: Option<u8>,
44 pub clipboard_on_timeout: Option<bool>,
45
46 pub action_enabled: Option<bool>,
48
49 pub test_mock_type: Option<String>,
51
52 pub hook_auto_uncomment: Option<bool>,
54 pub pre_gen_hook: Option<Vec<String>>,
55 pub pre_commit_hook: Option<Vec<String>>,
56 pub post_commit_hook: Option<Vec<String>>,
57 pub hook_strict: Option<bool>,
58 pub hook_timeout_ms: Option<u64>,
59
60 pub commitlint_config: Option<String>,
62 pub custom_prompt: Option<String>,
63
64 pub learn_from_history: Option<bool>,
66 pub history_commits_count: Option<usize>,
67 pub style_profile: Option<String>,
68}
69
70impl Default for Config {
71 fn default() -> Self {
72 Self {
73 api_key: None,
74 api_url: None,
75 ai_provider: Some("openai".to_string()),
76 model: Some("gpt-3.5-turbo".to_string()),
77 tokens_max_input: Some(4096),
78 tokens_max_output: Some(500),
79 commit_type: Some("conventional".to_string()),
80 emoji: Some(false),
81 description: Some(false),
82 description_capitalize: Some(true),
83 description_add_period: Some(false),
84 description_max_length: Some(100),
85 language: Some("en".to_string()),
86 message_template_placeholder: Some("$msg".to_string()),
87 prompt_module: Some("conventional-commit".to_string()),
88 gitpush: Some(false),
89 one_line_commit: Some(false),
90 why: Some(false),
91 omit_scope: Some(false),
92 generate_count: Some(1),
93 clipboard_on_timeout: Some(true),
94 action_enabled: Some(false),
95 test_mock_type: None,
96 hook_auto_uncomment: Some(false),
97 pre_gen_hook: None,
98 pre_commit_hook: None,
99 post_commit_hook: None,
100 hook_strict: Some(true),
101 hook_timeout_ms: Some(30000),
102 commitlint_config: None,
103 custom_prompt: None,
104 learn_from_history: Some(false),
105 history_commits_count: Some(10),
106 style_profile: None,
107 }
108 }
109}
110
111impl Config {
112 #[allow(dead_code)]
114 pub fn global_config_path() -> Result<PathBuf> {
115 if let Ok(config_home) = env::var("RCO_CONFIG_HOME") {
116 Ok(PathBuf::from(config_home).join("config.toml"))
117 } else {
118 let home = home_dir().context("Could not find home directory")?;
119 Ok(home.join(".config").join("rustycommit").join("config.toml"))
120 }
121 }
122
123 pub fn load() -> Result<Self> {
125 format::ConfigLocations::load_merged()
127 }
128
129 pub fn save(&self) -> Result<()> {
130 self.save_to(format::ConfigLocation::Global)
132 }
133
134 pub fn save_to(&self, location: format::ConfigLocation) -> Result<()> {
136 let mut save_config = self.clone();
138
139 if let Some(ref api_key) = self.api_key {
141 if secure_storage::is_available() {
142 match secure_storage::store_secret("RCO_API_KEY", api_key) {
143 Ok(_) => {
144 save_config.api_key = None;
146 }
147 Err(e) => {
148 eprintln!("Warning: Secure storage unavailable, falling back to file: {e}");
150 }
151 }
152 }
153 }
154
155 format::ConfigLocations::save(&save_config, location)
156 }
157
158 fn get_env_var(base_name: &str) -> Option<String> {
160 let rco_key = format!("RCO_{}", base_name);
161
162 env::var(&rco_key).ok()
164 }
165
166 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
167 if value == "undefined" || value == "null" {
169 return Ok(());
170 }
171
172 match key {
173 "RCO_API_KEY" => {
175 self.api_key = Some(value.to_string());
176 if secure_storage::is_available() {
178 let _ = secure_storage::store_secret("RCO_API_KEY", value);
179 }
180 }
181 "RCO_API_URL" => self.api_url = Some(value.to_string()),
182 "RCO_AI_PROVIDER" => self.ai_provider = Some(value.to_string()),
183 "RCO_MODEL" => self.model = Some(value.to_string()),
184 "RCO_TOKENS_MAX_INPUT" => {
185 self.tokens_max_input = Some(
186 value
187 .parse()
188 .context("Invalid number for TOKENS_MAX_INPUT")?,
189 );
190 }
191 "RCO_TOKENS_MAX_OUTPUT" => {
192 self.tokens_max_output = Some(
193 value
194 .parse()
195 .context("Invalid number for TOKENS_MAX_OUTPUT")?,
196 );
197 }
198 "RCO_COMMIT_TYPE" => {
199 self.commit_type = Some(value.to_string());
200 }
201 "RCO_PROMPT_MODULE" => {
202 let commit_type = match value {
204 "conventional-commit" => "conventional",
205 _ => value,
206 };
207 self.commit_type = Some(commit_type.to_string());
208 self.prompt_module = Some(value.to_string());
209 }
210 "RCO_EMOJI" => {
211 self.emoji = Some(value.parse().context("Invalid boolean for EMOJI")?);
212 }
213 "RCO_DESCRIPTION_CAPITALIZE" => {
214 self.description_capitalize = Some(
215 value
216 .parse()
217 .context("Invalid boolean for DESCRIPTION_CAPITALIZE")?,
218 );
219 }
220 "RCO_DESCRIPTION_ADD_PERIOD" => {
221 self.description_add_period = Some(
222 value
223 .parse()
224 .context("Invalid boolean for DESCRIPTION_ADD_PERIOD")?,
225 );
226 }
227 "RCO_DESCRIPTION_MAX_LENGTH" => {
228 self.description_max_length = Some(
229 value
230 .parse()
231 .context("Invalid number for DESCRIPTION_MAX_LENGTH")?,
232 );
233 }
234 "RCO_LANGUAGE" => self.language = Some(value.to_string()),
235 "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => {
236 self.message_template_placeholder = Some(value.to_string());
237 }
238 "RCO_GITPUSH" => {
239 self.gitpush = Some(value.parse().context("Invalid boolean for GITPUSH")?);
240 }
241 "RCO_ONE_LINE_COMMIT" => {
242 self.one_line_commit = Some(
243 value
244 .parse()
245 .context("Invalid boolean for ONE_LINE_COMMIT")?,
246 );
247 }
248 "RCO_ACTION_ENABLED" => {
249 self.action_enabled = Some(
250 value
251 .parse()
252 .context("Invalid boolean for ACTION_ENABLED")?,
253 );
254 }
255 "RCO_DESCRIPTION" => {
256 self.description = Some(value.parse().context("Invalid boolean for DESCRIPTION")?);
257 }
258 "RCO_WHY" => {
259 self.why = Some(value.parse().context("Invalid boolean for WHY")?);
260 }
261 "RCO_OMIT_SCOPE" => {
262 self.omit_scope = Some(value.parse().context("Invalid boolean for OMIT_SCOPE")?);
263 }
264 "RCO_TEST_MOCK_TYPE" => {
265 self.test_mock_type = Some(value.to_string());
266 }
267 "RCO_HOOK_AUTO_UNCOMMENT" => {
268 self.hook_auto_uncomment = Some(
269 value
270 .parse()
271 .context("Invalid boolean for HOOK_AUTO_UNCOMMENT")?,
272 );
273 }
274 "RCO_PRE_GEN_HOOK" => {
275 let items = value
276 .split(';')
277 .map(|s| s.trim().to_string())
278 .filter(|s| !s.is_empty())
279 .collect();
280 self.pre_gen_hook = Some(items);
281 }
282 "RCO_PRE_COMMIT_HOOK" => {
283 let items = value
284 .split(';')
285 .map(|s| s.trim().to_string())
286 .filter(|s| !s.is_empty())
287 .collect();
288 self.pre_commit_hook = Some(items);
289 }
290 "RCO_POST_COMMIT_HOOK" => {
291 let items = value
292 .split(';')
293 .map(|s| s.trim().to_string())
294 .filter(|s| !s.is_empty())
295 .collect();
296 self.post_commit_hook = Some(items);
297 }
298 "RCO_HOOK_STRICT" => {
299 self.hook_strict = Some(value.parse().context("Invalid boolean for HOOK_STRICT")?);
300 }
301 "RCO_HOOK_TIMEOUT_MS" => {
302 self.hook_timeout_ms = Some(
303 value
304 .parse()
305 .context("Invalid number for HOOK_TIMEOUT_MS")?,
306 );
307 }
308 "RCO_COMMITLINT_CONFIG" => {
309 self.commitlint_config = Some(value.to_string());
310 }
311 "RCO_CUSTOM_PROMPT" => {
312 self.custom_prompt = Some(value.to_string());
313 }
314 "RCO_GENERATE_COUNT" => {
315 self.generate_count = Some(
316 value
317 .parse()
318 .context("Invalid number for GENERATE_COUNT (1-5)")?,
319 );
320 }
321 "RCO_CLIPBOARD_ON_TIMEOUT" => {
322 self.clipboard_on_timeout = Some(
323 value
324 .parse()
325 .context("Invalid boolean for CLIPBOARD_ON_TIMEOUT")?,
326 );
327 }
328 "RCO_LEARN_FROM_HISTORY" => {
329 self.learn_from_history = Some(
330 value
331 .parse()
332 .context("Invalid boolean for LEARN_FROM_HISTORY")?,
333 );
334 }
335 "RCO_HISTORY_COMMITS_COUNT" => {
336 self.history_commits_count = Some(
337 value
338 .parse()
339 .context("Invalid number for HISTORY_COMMITS_COUNT")?,
340 );
341 }
342 "RCO_STYLE_PROFILE" => {
343 self.style_profile = Some(value.to_string());
344 }
345 "RCO_API_CUSTOM_HEADERS" => {
347 return Ok(());
349 }
350 _ => anyhow::bail!("Unknown configuration key: {}", key),
351 }
352
353 self.save()?;
354 Ok(())
355 }
356
357 pub fn get(&self, key: &str) -> Result<String> {
358 let value = match key {
359 "RCO_API_KEY" => {
360 self.api_key
362 .as_ref()
363 .map(|s| s.to_string())
364 .or_else(|| secure_storage::get_secret("RCO_API_KEY").ok().flatten())
365 }
366 "RCO_API_URL" => self.api_url.as_ref().map(|s| s.to_string()),
367 "RCO_AI_PROVIDER" => self.ai_provider.as_ref().map(|s| s.to_string()),
368 "RCO_MODEL" => self.model.as_ref().map(|s| s.to_string()),
369 "RCO_TOKENS_MAX_INPUT" => self.tokens_max_input.map(|v| v.to_string()),
370 "RCO_TOKENS_MAX_OUTPUT" => self.tokens_max_output.map(|v| v.to_string()),
371 "RCO_COMMIT_TYPE" => self.commit_type.as_ref().map(|s| s.to_string()),
372 "RCO_EMOJI" => self.emoji.map(|v| v.to_string()),
373 "RCO_DESCRIPTION_CAPITALIZE" => self.description_capitalize.map(|v| v.to_string()),
374 "RCO_DESCRIPTION_ADD_PERIOD" => self.description_add_period.map(|v| v.to_string()),
375 "RCO_DESCRIPTION_MAX_LENGTH" => self.description_max_length.map(|v| v.to_string()),
376 "RCO_LANGUAGE" => self.language.as_ref().map(|s| s.to_string()),
377 "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => self
378 .message_template_placeholder
379 .as_ref()
380 .map(|s| s.to_string()),
381 "RCO_GITPUSH" => self.gitpush.map(|v| v.to_string()),
382 "RCO_ONE_LINE_COMMIT" => self.one_line_commit.map(|v| v.to_string()),
383 "RCO_ACTION_ENABLED" => self.action_enabled.map(|v| v.to_string()),
384 "RCO_COMMITLINT_CONFIG" => self.commitlint_config.as_ref().map(|s| s.to_string()),
385 "RCO_CUSTOM_PROMPT" => self.custom_prompt.as_ref().map(|s| s.to_string()),
386 "RCO_GENERATE_COUNT" => self.generate_count.map(|v| v.to_string()),
387 "RCO_CLIPBOARD_ON_TIMEOUT" => self.clipboard_on_timeout.map(|v| v.to_string()),
388 _ => None,
389 };
390
391 value.ok_or_else(|| anyhow::anyhow!("Configuration key '{}' not found or not set", key))
392 }
393
394 pub fn reset(&mut self, keys: Option<&[String]>) -> Result<()> {
395 if let Some(key_list) = keys {
396 let default = Self::default();
397 for key in key_list {
398 match key.as_str() {
399 "RCO_API_KEY" => {
400 self.api_key = default.api_key.clone();
401 let _ = secure_storage::delete_secret("RCO_API_KEY");
403 }
404 "RCO_API_URL" => self.api_url = default.api_url.clone(),
405 "RCO_AI_PROVIDER" => self.ai_provider = default.ai_provider.clone(),
406 "RCO_MODEL" => self.model = default.model.clone(),
407 "RCO_TOKENS_MAX_INPUT" => self.tokens_max_input = default.tokens_max_input,
408 "RCO_TOKENS_MAX_OUTPUT" => self.tokens_max_output = default.tokens_max_output,
409 "RCO_COMMIT_TYPE" => self.commit_type = default.commit_type.clone(),
410 "RCO_EMOJI" => self.emoji = default.emoji,
411 "RCO_DESCRIPTION_CAPITALIZE" => {
412 self.description_capitalize = default.description_capitalize
413 }
414 "RCO_DESCRIPTION_ADD_PERIOD" => {
415 self.description_add_period = default.description_add_period
416 }
417 "RCO_DESCRIPTION_MAX_LENGTH" => {
418 self.description_max_length = default.description_max_length
419 }
420 "RCO_LANGUAGE" => self.language = default.language.clone(),
421 "RCO_MESSAGE_TEMPLATE_PLACEHOLDER" => {
422 self.message_template_placeholder =
423 default.message_template_placeholder.clone()
424 }
425 "RCO_GITPUSH" => self.gitpush = default.gitpush,
426 "RCO_ONE_LINE_COMMIT" => self.one_line_commit = default.one_line_commit,
427 "RCO_ACTION_ENABLED" => self.action_enabled = default.action_enabled,
428 "RCO_PRE_GEN_HOOK" => self.pre_gen_hook = default.pre_gen_hook.clone(),
429 "RCO_PRE_COMMIT_HOOK" => self.pre_commit_hook = default.pre_commit_hook.clone(),
430 "RCO_POST_COMMIT_HOOK" => {
431 self.post_commit_hook = default.post_commit_hook.clone()
432 }
433 "RCO_HOOK_STRICT" => self.hook_strict = default.hook_strict,
434 "RCO_HOOK_TIMEOUT_MS" => self.hook_timeout_ms = default.hook_timeout_ms,
435 "RCO_GENERATE_COUNT" => self.generate_count = default.generate_count,
436 "RCO_CLIPBOARD_ON_TIMEOUT" => {
437 self.clipboard_on_timeout = default.clipboard_on_timeout
438 }
439 _ => anyhow::bail!("Unknown configuration key: {}", key),
440 }
441 }
442 } else {
443 *self = Self::default();
444 }
445
446 self.save()?;
447 Ok(())
448 }
449
450 pub fn load_with_commitlint(&mut self) -> Result<()> {
452 if let Ok(commitlint_path) = env::var("COMMITLINT_CONFIG") {
454 self.commitlint_config = Some(commitlint_path);
455 }
456
457 if self.commitlint_config.is_none() {
459 let home = home_dir().context("Could not find home directory")?;
460
461 let possible_paths = [
463 home.join(".commitlintrc.js"),
464 home.join(".commitlintrc.json"),
465 home.join(".commitlintrc.yml"),
466 home.join(".commitlintrc.yaml"),
467 home.join("commitlint.config.js"),
468 ];
469
470 for path in &possible_paths {
471 if path.exists() {
472 self.commitlint_config = Some(path.to_string_lossy().to_string());
473 break;
474 }
475 }
476 }
477
478 Ok(())
479 }
480
481 pub fn apply_commitlint_rules(&mut self) -> Result<()> {
483 if let Some(ref config_path) = self.commitlint_config.clone() {
484 let path = PathBuf::from(config_path);
485 if path.exists() {
486 if self.commit_type.is_none() {
489 self.commit_type = Some("conventional".to_string());
490 }
491
492 println!("📋 Found commitlint config at: {}", config_path);
495 println!("🔧 Using conventional commit format for consistency");
496 }
497 }
498 Ok(())
499 }
500
501 pub fn get_effective_prompt(
503 &self,
504 diff: &str,
505 context: Option<&str>,
506 full_gitmoji: bool,
507 ) -> String {
508 if let Some(ref custom_prompt) = self.custom_prompt {
509 tracing::warn!(
511 "Using custom prompt template - diff content will be included in the prompt. \
512 Ensure your custom prompt does not exfiltrate or log sensitive code."
513 );
514 eprintln!(
515 "{}",
516 "Warning: Using custom prompt template. Your diff content will be sent to the AI provider."
517 .yellow()
518 );
519
520 let mut prompt = custom_prompt.clone();
522 prompt = prompt.replace("$diff", diff);
523 if let Some(ctx) = context {
524 prompt = prompt.replace("$context", ctx);
525 }
526 prompt
527 } else {
528 super::providers::build_prompt(diff, context, self, full_gitmoji)
530 }
531 }
532
533 pub fn merge(&mut self, other: Config) {
535 macro_rules! merge_field {
536 ($field:ident) => {
537 if other.$field.is_some() {
538 self.$field = other.$field;
539 }
540 };
541 }
542
543 merge_field!(api_key);
544 merge_field!(api_url);
545 merge_field!(ai_provider);
546 merge_field!(model);
547 merge_field!(tokens_max_input);
548 merge_field!(tokens_max_output);
549 merge_field!(commit_type);
550 merge_field!(emoji);
551 merge_field!(description);
552 merge_field!(description_capitalize);
553 merge_field!(description_add_period);
554 merge_field!(description_max_length);
555 merge_field!(language);
556 merge_field!(message_template_placeholder);
557 merge_field!(prompt_module);
558 merge_field!(gitpush);
559 merge_field!(one_line_commit);
560 merge_field!(why);
561 merge_field!(omit_scope);
562 merge_field!(action_enabled);
563 merge_field!(test_mock_type);
564 merge_field!(hook_auto_uncomment);
565 merge_field!(pre_gen_hook);
566 merge_field!(pre_commit_hook);
567 merge_field!(post_commit_hook);
568 merge_field!(hook_strict);
569 merge_field!(hook_timeout_ms);
570 merge_field!(commitlint_config);
571 merge_field!(custom_prompt);
572 merge_field!(generate_count);
573 merge_field!(clipboard_on_timeout);
574 merge_field!(learn_from_history);
575 merge_field!(history_commits_count);
576 merge_field!(style_profile);
577 }
578
579 pub fn load_from_environment(&mut self) {
582 macro_rules! load_env_var {
584 ($field:ident, $base_name:expr) => {
585 if let Some(value) = Self::get_env_var($base_name) {
586 self.$field = Some(value);
587 }
588 };
589 }
590
591 macro_rules! load_env_var_parse {
592 ($field:ident, $base_name:expr, $type:ty) => {
593 if let Some(value) = Self::get_env_var($base_name) {
594 if let Ok(parsed) = value.parse::<$type>() {
595 self.$field = Some(parsed);
596 }
597 }
598 };
599 }
600
601 load_env_var!(api_key, "API_KEY");
602 load_env_var!(api_url, "API_URL");
603 load_env_var!(ai_provider, "AI_PROVIDER");
604 load_env_var!(model, "MODEL");
605 load_env_var_parse!(tokens_max_input, "TOKENS_MAX_INPUT", usize);
606 load_env_var_parse!(tokens_max_output, "TOKENS_MAX_OUTPUT", u32);
607 load_env_var!(commit_type, "COMMIT_TYPE");
608 load_env_var_parse!(emoji, "EMOJI", bool);
609 load_env_var_parse!(description, "DESCRIPTION", bool);
610 load_env_var_parse!(description_capitalize, "DESCRIPTION_CAPITALIZE", bool);
611 load_env_var_parse!(description_add_period, "DESCRIPTION_ADD_PERIOD", bool);
612 load_env_var_parse!(description_max_length, "DESCRIPTION_MAX_LENGTH", usize);
613 load_env_var!(language, "LANGUAGE");
614 load_env_var!(message_template_placeholder, "MESSAGE_TEMPLATE_PLACEHOLDER");
615 load_env_var!(prompt_module, "PROMPT_MODULE");
616 load_env_var_parse!(gitpush, "GITPUSH", bool);
617 load_env_var_parse!(one_line_commit, "ONE_LINE_COMMIT", bool);
618 load_env_var_parse!(why, "WHY", bool);
619 load_env_var_parse!(omit_scope, "OMIT_SCOPE", bool);
620 load_env_var_parse!(action_enabled, "ACTION_ENABLED", bool);
621 load_env_var!(test_mock_type, "TEST_MOCK_TYPE");
622 load_env_var_parse!(hook_auto_uncomment, "HOOK_AUTO_UNCOMMENT", bool);
623 load_env_var!(commitlint_config, "COMMITLINT_CONFIG");
624 load_env_var!(custom_prompt, "CUSTOM_PROMPT");
625 load_env_var_parse!(generate_count, "GENERATE_COUNT", u8);
626 load_env_var_parse!(clipboard_on_timeout, "CLIPBOARD_ON_TIMEOUT", bool);
627 load_env_var_parse!(learn_from_history, "LEARN_FROM_HISTORY", bool);
628 load_env_var_parse!(history_commits_count, "HISTORY_COMMITS_COUNT", usize);
629 load_env_var!(style_profile, "STYLE_PROFILE");
630 }
631}
632
633#[allow(dead_code)]
638impl Config {
639 pub fn get_active_account(&self) -> Result<Option<accounts::AccountConfig>> {
641 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
642 if let Some(account) = accounts_config.get_active_account() {
643 return Ok(Some(account.clone()));
644 }
645 }
646 Ok(None)
647 }
648
649 pub fn has_accounts(&self) -> bool {
651 accounts::AccountsConfig::load()
652 .map(|c| c.map(|ac| !ac.accounts.is_empty()).unwrap_or(false))
653 .unwrap_or(false)
654 }
655
656 pub fn get_account(&self, alias: &str) -> Result<Option<accounts::AccountConfig>> {
658 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
659 if let Some(account) = accounts_config.get_account(alias) {
660 return Ok(Some(account.clone()));
661 }
662 }
663 Ok(None)
664 }
665
666 pub fn list_accounts(&self) -> Result<Vec<accounts::AccountConfig>> {
668 if let Some(accounts_config) = accounts::AccountsConfig::load()? {
669 Ok(accounts_config
670 .list_accounts()
671 .into_iter()
672 .cloned()
673 .collect())
674 } else {
675 Ok(Vec::new())
676 }
677 }
678
679 pub fn set_default_account(&mut self, alias: &str) -> Result<()> {
681 let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
682 accounts_config.set_active_account(alias)?;
683 accounts_config.save()?;
684 Ok(())
685 }
686
687 pub fn remove_account(&mut self, alias: &str) -> Result<()> {
689 let mut accounts_config = accounts::AccountsConfig::load()?.unwrap_or_default();
690 if accounts_config.remove_account(alias) {
691 accounts_config.save()?;
692 }
693 Ok(())
694 }
695}