Skip to main content

aurora_modules/
dev.rs

1use aurora_core::{AuroraResult, Pipeline, Value, AuroraError};
2use std::process::Command;
3use std::io::{self, Write, BufRead};
4
5fn check_git_installed() -> AuroraResult<()> {
6    let status = Command::new("git").arg("--version").output()
7        .map_err(|_| AuroraError::CommandNotFound(
8            "Git не установлен. Установи: sudo pacman -S git (Arch) / sudo apt install git (Debian) / brew install git (macOS)".into()
9        ))?;
10    if !status.status.success() {
11        return Err(AuroraError::CommandNotFound(
12            "Git не установлен. Установи: sudo pacman -S git (Arch) / sudo apt install git (Debian) / brew install git (macOS)".into()
13        ));
14    }
15    Ok(())
16}
17
18fn check_git_repo() -> AuroraResult<bool> {
19    Ok(Command::new("git").args(["rev-parse", "--git-dir"]).output()
20        .ok()
21        .is_some_and(|o| o.status.success()))
22}
23
24fn require_git_repo(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
25    if !check_git_repo()? {
26        return Err(AuroraError::NotFound(
27            localizer.t(
28                "Не git-репозиторий. Запусти `aurora dev init` или `git init`.",
29                "Not a git repository. Run `aurora dev init` or `git init` first."
30            ).to_string()
31        ));
32    }
33    Ok(())
34}
35
36fn check_git_config() -> AuroraResult<Option<(String, String)>> {
37    let name = Command::new("git").args(["config", "user.name"]).output().ok()
38        .and_then(|o| if o.status.success() {
39            String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
40        } else { None });
41
42    let email = Command::new("git").args(["config", "user.email"]).output().ok()
43        .and_then(|o| if o.status.success() {
44            String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
45        } else { None });
46
47    match (name, email) {
48        (Some(n), Some(e)) => Ok(Some((n, e))),
49        _ => Ok(None),
50    }
51}
52
53fn prompt(prompt_text: &str) -> AuroraResult<String> {
54    print!("{prompt_text}");
55    io::stdout().flush().ok();
56    let mut line = String::new();
57    io::stdin().lock().read_line(&mut line)
58        .map_err(|_| AuroraError::ModuleError("Не удалось прочитать ввод".into()))?;
59    Ok(line.trim().to_string())
60}
61
62fn prompt_yes_no(prompt_text: &str) -> AuroraResult<bool> {
63    let answer = prompt(&format!("{prompt_text} [y/N]: "))?;
64    Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes") || answer.eq_ignore_ascii_case("да"))
65}
66
67fn run_cmd(args: &[&str]) -> AuroraResult<(bool, String, String)> {
68    let output = Command::new("git").args(args).output()
69        .map_err(|e| AuroraError::ModuleError(format!("git failed: {e}")))?;
70    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
71    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
72    Ok((output.status.success(), stdout, stderr))
73}
74
75const GITIGNORE_TEMPLATE: &str = r#".gitignore           # Локальный — не засоряет репозиторий
76
77# ===== Файлы сборки / зависимостей =====
78target/          # Rust: результат компиляции (cargo build)
79build/           # C/C++/Make: папка сборки
80dist/            # Собранные файлы для публикации
81node_modules/    # JavaScript: зависимости npm (их НЕ хранят в git)
82vendor/          # PHP / Go: сторонние библиотеки
83__pycache__/     # Python: кеш байткода
84*.pyc            # Python: скомпилированные файлы
85.env             # Локальные переменные (пароли, ключи API)
86
87# ===== IDE и редакторы =====
88.idea/           # JetBrains (IntelliJ IDEA, PyCharm, GoLand)
89.vscode/         # VS Code — только если там не общие настройки
90*.swp            # Vim: временные файлы
91*.swo            # Vim: временные файлы
92.DS_Store        # macOS: служебные файлы Finder
93Thumbs.db        # Windows: кеш миниатюр
94
95# ===== Логи и временные файлы =====
96*.log            # Лог-файлы (их не коммитят)
97*.tmp            # Временные файлы
98*.bak            # Бэкапы
99"#;
100
101fn ensure_git_repo(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
102    if check_git_repo()? {
103        return Ok(());
104    }
105
106    println!();
107    println!("{}", localizer.t(
108        "⚠ Git-репозиторий не найден! Хочешь создать его?",
109        "⚠ Git repository not found! Would you like to create one?"
110    ));
111    println!("  {}", localizer.t(
112        "Это создаст папку .git и начнёт отслеживать изменения в проекте.",
113        "This creates a .git folder and starts tracking changes in your project."
114    ));
115
116    if !prompt_yes_no(localizer.t("Создать репозиторий", "Create repository"))? {
117        return Err(AuroraError::ModuleError(
118            localizer.t(
119                "Коммит отменён — нет репозитория. Создай вручную: git init",
120                "Commit cancelled — no repository. Create manually: git init"
121            ).to_string()
122        ));
123    }
124
125    let (ok, _, err) = run_cmd(&["init"])?;
126    if !ok {
127        return Err(AuroraError::ModuleError(
128            format!("git init failed: {err}")
129        ));
130    }
131
132    println!("{}", localizer.t("✓ Репозиторий создан!", "✓ Repository created!"));
133    println!();
134
135    // .gitignore
136    if prompt_yes_no(localizer.t(
137        "Создать файл .gitignore? Он скрывает мусор (log, tmp, __pycache__...) из репозитория.",
138        "Create .gitignore? It hides junk files (logs, tmp, __pycache__...) from the repo."
139    ))? {
140        let gitignore_path = std::path::Path::new(".gitignore");
141        if gitignore_path.exists() {
142            println!("  .gitignore уже существует — пропускаем.");
143        } else {
144            std::fs::write(gitignore_path, GITIGNORE_TEMPLATE)
145                .map_err(|e| AuroraError::ModuleError(format!("Не удалось создать .gitignore: {e}")))?;
146            println!("{}", localizer.t(
147                "✓ .gitignore создан! Отредактируй его под свой проект.",
148                "✓ .gitignore created! Edit it for your project."
149            ));
150        }
151    }
152
153    Ok(())
154}
155
156fn ensure_git_config(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
157    if let Some((name, email)) = check_git_config()? {
158        println!("  {}: {name} <{email}>", localizer.t("Git настроен как", "Git configured as"));
159        return Ok(());
160    }
161
162    println!();
163    println!("{}", localizer.t(
164        "⚠ Git не знает твоё имя и email — они нужны для подписи коммитов.",
165        "⚠ Git doesn't know your name and email — needed to sign commits."
166    ));
167
168    let name = prompt(&localizer.t("Введи имя (как в подписи к коммиту): ", "Enter your name (as commit signature): "))?;
169    if name.is_empty() {
170        return Err(AuroraError::ModuleError(
171            localizer.t("Имя не может быть пустым. Коммит отменён.", "Name cannot be empty. Commit cancelled.").to_string()
172        ));
173    }
174
175    let email = prompt(&localizer.t("Введи email: ", "Enter your email: "))?;
176    if email.is_empty() {
177        return Err(AuroraError::ModuleError(
178            localizer.t("Email не может быть пустым. Коммит отменён.", "Email cannot be empty. Commit cancelled.").to_string()
179        ));
180    }
181
182    let (ok1, _, _) = run_cmd(&["config", "user.name", &name])?;
183    let (ok2, _, _) = run_cmd(&["config", "user.email", &email])?;
184
185    if ok1 && ok2 {
186        println!("{}", localizer.t("✓ Git настроен!", "✓ Git configured!"));
187    }
188
189    Ok(())
190}
191
192fn ensure_staged(localizer: &aurora_locale::Localizer) -> AuroraResult<bool> {
193    let (_, stdout, _) = run_cmd(&["status", "--short"])?;
194
195    if stdout.trim().is_empty() {
196        println!("{}", localizer.t(
197            "Нет изменений для коммита. Нечего коммитить.",
198            "No changes to commit. Nothing to commit."
199        ));
200        return Ok(false);
201    }
202
203    let lines: Vec<&str> = stdout.lines()
204        .filter(|l| !l.trim().is_empty())
205        .collect();
206
207    // Первый символ — статус в индексе (staging area)
208    // ' ' = не в индексе, '?' = untracked, M/A/D/R = staged
209    let staged: Vec<&&str> = lines.iter().filter(|l| {
210        let c = l.as_bytes().first().copied().unwrap_or(b' ');
211        c != b' ' && c != b'?'
212    }).collect();
213
214    let unstaged: Vec<&&str> = lines.iter().filter(|l| {
215        let c = l.as_bytes().first().copied().unwrap_or(b' ');
216        c == b' '
217    }).collect();
218
219    let untracked: Vec<&&str> = lines.iter().filter(|l| {
220        l.starts_with("??")
221    }).collect();
222
223    if !staged.is_empty() {
224        return Ok(true);
225    }
226
227    if !untracked.is_empty() || !unstaged.is_empty() {
228        let total = untracked.len() + unstaged.len();
229        println!("  {total} {}", localizer.t(
230            "файлов не добавлены в коммит. Нужно git add",
231            "files not staged. Need git add"
232        ));
233
234        if prompt_yes_no(localizer.t(
235            "Добавить ВСЕ (git add -A)?",
236            "Add ALL (git add -A)?"
237        ))? {
238            run_cmd(&["add", "-A"])?;
239            println!("{}", localizer.t(
240                "✓ Все файлы добавлены!",
241                "✓ All files staged!"
242            ));
243            return Ok(true);
244        }
245
246        println!("  {}", localizer.t(
247            "Коммит отменён. Добавь файлы: git add <file>",
248            "Commit cancelled. Stage files: git add <file>"
249        ));
250        return Ok(false);
251    }
252
253    Ok(false)
254}
255
256pub fn dev_commit(msg: &str, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
257    check_git_installed()?;
258
259    // Шаг 1: репозиторий
260    ensure_git_repo(localizer)?;
261
262    // Шаг 2: конфиг git
263    ensure_git_config(localizer)?;
264
265    // Шаг 3: staged файлы
266    if !ensure_staged(localizer)? {
267        return Ok(Pipeline::single(Value::String(
268            localizer.t("Коммит отменён.", "Commit cancelled.").to_string()
269        )));
270    }
271
272    // Шаг 4: если сообщение не указано — спросить
273    let commit_msg = if msg.is_empty() || msg == "commit" {
274        println!();
275        println!("{}", localizer.t(
276            "Опиши, что изменилось (описание коммита):",
277            "Describe what changed (commit message):"
278        ));
279        prompt("> ")?
280    } else {
281        msg.to_string()
282    };
283
284    if commit_msg.is_empty() {
285        return Err(AuroraError::ModuleError(
286            localizer.t("Сообщение коммита не может быть пустым.", "Commit message cannot be empty.").to_string()
287        ));
288    }
289
290    // Шаг 5: коммит
291    let (ok, _stdout, stderr) = run_cmd(&["commit", "-m", &commit_msg])?;
292
293    if !ok {
294        return Err(AuroraError::ModuleError(
295            format!("{}: {stderr}",
296                localizer.t("Git commit не удался", "Git commit failed")
297            )
298        ));
299    }
300
301    println!();
302    println!("{}", localizer.t("✓ Закоммичено!", "✓ Committed!"));
303    println!("  {commit_msg}");
304
305    Ok(Pipeline::table(
306        vec!["status".into(), "message".into()],
307        vec![vec![
308            Value::String("committed".into()),
309            Value::String(commit_msg),
310        ]],
311    ))
312}
313
314pub fn dev_log(oneline: bool, graph: bool, n: Option<usize>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
315    check_git_installed()?;
316    require_git_repo(localizer)?;
317    let mut cmd = Command::new("git");
318    cmd.arg("log");
319    if graph { cmd.arg("--graph"); }
320    if let Some(count) = n { cmd.arg(format!("-{count}")); }
321    if oneline {
322        cmd.arg("--oneline");
323    } else {
324        cmd.arg("--format=COMMIT%x1e%H%x1f%an%x1f%ai%x1f%s");
325    }
326
327    let output = cmd.output()
328        .map_err(|e| AuroraError::ModuleError(format!("git log failed: {e}")))?;
329    if !output.status.success() {
330        let stderr = String::from_utf8_lossy(&output.stderr);
331        return Err(AuroraError::ModuleError(format!("git log: {stderr}")));
332    }
333
334    let stdout = String::from_utf8_lossy(&output.stdout);
335    let mut rows: Vec<Vec<Value>> = Vec::new();
336    for line in stdout.lines() {
337        if line.is_empty() { continue; }
338        if oneline {
339            let clean = if graph { line.split('\x1e').last().unwrap_or(line) } else { line };
340            if let Some(pos) = clean.find(' ') {
341                let hash = &clean[..pos];
342                let msg = clean[pos + 1..].trim();
343                rows.push(vec![
344                    Value::String(hash.into()),
345                    Value::String(String::new()),
346                    Value::String(String::new()),
347                    Value::String(msg.into()),
348                ]);
349            }
350        } else {
351            let clean = if graph { line.split('\x1e').last().unwrap_or(line) } else { line };
352            let parts: Vec<&str> = clean.splitn(4, '\x1f').collect();
353            if parts.len() >= 4 {
354                rows.push(vec![
355                    Value::String(parts[0].into()),
356                    Value::String(parts[1].into()),
357                    Value::String(parts[2].into()),
358                    Value::String(parts[3].into()),
359                ]);
360            }
361        }
362    }
363    Ok(Pipeline::table(
364        vec!["commit".into(), "author".into(), "date".into(), "message".into()],
365        rows,
366    ))
367}
368
369pub fn dev_status() -> AuroraResult<Pipeline> {
370    check_git_installed()?;
371    if !check_git_repo()? {
372        return Ok(Pipeline::single(Value::String(
373            "Not a git repository. Use 'aurora dev commit' to create one.".to_string()
374        )));
375    }
376    let output = Command::new("git").args(["status", "--short"]).output()
377        .map_err(|e| AuroraError::ModuleError(format!("git status failed: {e}")))?;
378    let stdout = String::from_utf8_lossy(&output.stdout);
379    let mut rows: Vec<Vec<Value>> = Vec::new();
380    for line in stdout.lines() {
381        let line = line.trim();
382        if line.is_empty() || line.len() <= 3 { continue; }
383        let status = &line[..2];
384        let file = &line[3..];
385        rows.push(vec![Value::String(status.trim().into()), Value::String(file.into())]);
386    }
387    Ok(Pipeline::table(vec!["status".into(), "file".into()], rows))
388}
389
390pub fn dev_diff(stat: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
391    check_git_installed()?;
392    require_git_repo(localizer)?;
393    let mut cmd = Command::new("git");
394    cmd.arg("diff");
395    if stat { cmd.arg("--stat"); }
396    let output = cmd.output()
397        .map_err(|e| AuroraError::ModuleError(format!("git diff failed: {e}")))?;
398    let stdout = String::from_utf8_lossy(&output.stdout);
399    let mut rows: Vec<Vec<Value>> = Vec::new();
400    if stat {
401        for line in stdout.lines() {
402            if line.is_empty() { continue; }
403            rows.push(vec![Value::String(line.into())]);
404        }
405    } else {
406        rows.push(vec![Value::String(stdout.into())]);
407    }
408    Ok(Pipeline::table(vec!["diff".into()], rows))
409}
410
411pub fn dev_push(remote: Option<&str>, branch: Option<&str>, upstream: bool, force: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
412    check_git_installed()?;
413    require_git_repo(localizer)?;
414
415    let remote = remote.unwrap_or("origin");
416    let current_branch = if branch.is_some() {
417        branch.unwrap().to_string()
418    } else {
419        let (ok, stdout, _) = run_cmd(&["rev-parse", "--abbrev-ref", "HEAD"])?;
420        if !ok {
421            return Err(AuroraError::ModuleError(
422                "Не удалось определить текущую ветку".into()
423            ));
424        }
425        stdout.trim().to_string()
426    };
427
428    let remote_str = remote.to_string();
429    let branch_str = current_branch.clone();
430
431    if force {
432        if !prompt_yes_no(&format!(
433            "{} {}/{}? {}",
434            localizer.t("⚠ FORCE PUSH на", "⚠ FORCE PUSH to"),
435            remote_str, branch_str,
436            localizer.t("История будет перезаписана! Продолжить?", "History will be rewritten! Continue?")
437        ))? {
438            return Ok(Pipeline::single(Value::String(localizer.t("Отменено.", "Cancelled.").to_string())));
439        }
440    }
441
442    println!("{} {}/{}", localizer.t("→ Пушим в", "→ Pushing to"), remote, current_branch);
443
444    let mut args = vec!["push"];
445    if upstream { args.push("-u"); }
446    if force { args.push("--force"); }
447    args.push(remote);
448    args.push(&current_branch);
449
450    let (ok, stdout, stderr) = run_cmd(&args)?;
451
452    if !ok {
453        let msg = if stderr.contains("fatal:") && stderr.contains("repository") {
454            localizer.t(
455                "Репозиторий не найден. Проверь remote: aurora dev remote list",
456                "Repository not found. Check remote: aurora dev remote list"
457            )
458        } else if stderr.contains("fatal:") && stderr.contains("refspec") {
459            localizer.t(
460                "Ветка не найдена на удалённом репозитории. Попробуй --upstream или git push -u origin <ветка>",
461                "Branch not found on remote. Try --upstream or git push -u origin <branch>"
462            )
463        } else {
464            localizer.t(
465                "Git push не удался. Проверь remote и права доступа.",
466                "Git push failed. Check remote and access rights."
467            )
468        };
469        return Err(AuroraError::ModuleError(format!("{msg}\n{stderr}")));
470    }
471
472    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
473    let mut rows: Vec<Vec<Value>> = Vec::new();
474    for line in output.lines() {
475        let line = line.trim();
476        if line.is_empty() { continue; }
477        rows.push(vec![Value::String(line.into())]);
478    }
479
480    println!("{} {}/{}", localizer.t("✓ Запушено в", "✓ Pushed to"), remote, current_branch);
481    Ok(Pipeline::table(vec!["output".into()], rows))
482}
483
484pub fn dev_fetch(remote: Option<&str>, branch: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
485    check_git_installed()?;
486    require_git_repo(localizer)?;
487
488    let remote = remote.unwrap_or("origin");
489    let mut args = vec!["fetch", remote];
490    if let Some(b) = branch { args.push(b); }
491
492    println!("{} {}", localizer.t("→ Получаю из", "→ Fetching from"), remote);
493    let (ok, stdout, stderr) = run_cmd(&args)?;
494
495    if !ok {
496        return Err(AuroraError::ModuleError(
497            format!("git fetch: {stderr}")
498        ));
499    }
500
501    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
502    let mut rows: Vec<Vec<Value>> = Vec::new();
503    for line in output.lines() {
504        let line = line.trim();
505        if line.is_empty() { continue; }
506        rows.push(vec![Value::String(line.into())]);
507    }
508
509    println!("{} {}{}",
510        localizer.t("✓ Получено из", "✓ Fetched from"),
511        remote,
512        branch.map(|b| format!(" ({b})")).unwrap_or_default()
513    );
514    Ok(Pipeline::table(vec!["output".into()], rows))
515}
516
517pub fn dev_pull(remote: Option<&str>, branch: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
518    check_git_installed()?;
519    require_git_repo(localizer)?;
520
521    let remote = remote.unwrap_or("origin");
522    let current_branch = if branch.is_some() {
523        branch.unwrap().to_string()
524    } else {
525        let (ok, stdout, _) = run_cmd(&["rev-parse", "--abbrev-ref", "HEAD"])?;
526        if !ok {
527            return Err(AuroraError::ModuleError(
528                "Не удалось определить текущую ветку".into()
529            ));
530        }
531        stdout.trim().to_string()
532    };
533
534    println!("{} {}/{}", localizer.t("→ Тяну из", "→ Pulling from"), remote, current_branch);
535
536    let (ok, stdout, stderr) = run_cmd(&["pull", remote, &current_branch])?;
537    if !ok {
538        return Err(AuroraError::ModuleError(format!("git pull: {stderr}")));
539    }
540
541    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
542    let mut rows: Vec<Vec<Value>> = Vec::new();
543    for line in output.lines() {
544        let line = line.trim();
545        if line.is_empty() { continue; }
546        rows.push(vec![Value::String(line.into())]);
547    }
548
549    Ok(Pipeline::table(vec!["output".into()], rows))
550}
551
552pub fn dev_clone(url: &str, target: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
553    check_git_installed()?;
554
555    let dir = target.unwrap_or("");
556    println!("{} {} {}",
557        localizer.t("→ Клонирую", "→ Cloning"),
558        url,
559        if dir.is_empty() { "".into() } else { format!("→ {dir}") }
560    );
561
562    let mut args = vec!["clone", url];
563    if !dir.is_empty() { args.push(dir); }
564
565    let (ok, _stdout, stderr) = run_cmd(&args)?;
566    if !ok {
567        return Err(AuroraError::ModuleError(format!("git clone: {stderr}")));
568    }
569
570    println!("{}", localizer.t("✓ Репозиторий склонирован!", "✓ Repository cloned!"));
571    Ok(Pipeline::table(
572        vec!["url".into(), "target".into()],
573        vec![vec![Value::String(url.into()), Value::String(if dir.is_empty() { "." } else { dir }.into())]],
574    ))
575}
576
577pub fn dev_init(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
578    check_git_installed()?;
579
580    if check_git_repo()? {
581        return Err(AuroraError::ModuleError(
582            localizer.t("Репозиторий уже инициализирован.", "Repository already initialized.").to_string()
583        ));
584    }
585
586    let (ok, _, stderr) = run_cmd(&["init"])?;
587    if !ok {
588        return Err(AuroraError::ModuleError(format!("git init: {stderr}")));
589    }
590
591    println!("{}", localizer.t("✓ Репозиторий создан!", "✓ Repository created!"));
592
593    // .gitignore
594    if prompt_yes_no(localizer.t(
595        "Создать .gitignore? Он скрывает мусор из репозитория.",
596        "Create .gitignore? It hides junk from the repo."
597    ))? {
598        let gitignore_path = std::path::Path::new(".gitignore");
599        if gitignore_path.exists() {
600            println!("  .gitignore уже существует — пропускаем.");
601        } else {
602            std::fs::write(gitignore_path, GITIGNORE_TEMPLATE)
603                .map_err(|e| AuroraError::ModuleError(format!("Не удалось создать .gitignore: {e}")))?;
604            println!("{}", localizer.t("✓ .gitignore создан!", "✓ .gitignore created!"));
605        }
606    }
607
608    // user config
609    if let Some((name, email)) = check_git_config()? {
610        println!("  {}: {name} <{email}>", localizer.t("Git настроен как", "Git configured as"));
611    } else if prompt_yes_no(localizer.t(
612        "Настроить имя и email для git?",
613        "Set up your git name and email?"
614    ))? {
615        let name = prompt(&localizer.t("Имя: ", "Name: "))?;
616        let email = prompt(&localizer.t("Email: ", "Email: "))?;
617        if !name.is_empty() && !email.is_empty() {
618            run_cmd(&["config", "user.name", &name])?;
619            run_cmd(&["config", "user.email", &email])?;
620            println!("{}", localizer.t("✓ Git настроен!", "✓ Git configured!"));
621        }
622    }
623
624    Ok(Pipeline::table(
625        vec!["status".into()],
626        vec![vec![Value::String("initialized".into())]],
627    ))
628}
629
630pub fn dev_add(files: &[String], localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
631    check_git_installed()?;
632    require_git_repo(localizer)?;
633
634    let (ok, stdout, stderr) = if files.is_empty() {
635        run_cmd(&["add", "-A"])?
636    } else {
637        let mut args = vec!["add"];
638        for f in files { args.push(f); }
639        run_cmd(&args)?
640    };
641
642    if !ok {
643        return Err(AuroraError::ModuleError(format!("git add: {stderr}")));
644    }
645
646    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
647    let mut rows: Vec<Vec<Value>> = Vec::new();
648    for line in output.lines() {
649        let line = line.trim();
650        if line.is_empty() { continue; }
651        rows.push(vec![Value::String(line.into())]);
652    }
653
654    println!("{}", localizer.t("✓ Файлы добавлены!", "✓ Files staged!"));
655    Ok(Pipeline::table(vec!["output".into()], rows))
656}
657
658pub fn dev_checkout(target: Option<&str>, create: bool, orphan: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
659    check_git_installed()?;
660    require_git_repo(localizer)?;
661
662    let mut args = vec!["checkout"];
663    if orphan {
664        args.push("--orphan");
665        let branch = target.unwrap_or("");
666        if branch.is_empty() {
667            return Err(AuroraError::InvalidInput(
668                localizer.t("Укажи имя ветки: aurora dev checkout --orphan <имя>", "Specify branch name: aurora dev checkout --orphan <name>").to_string()
669            ));
670        }
671        args.push(branch);
672        let (ok, stdout, stderr) = run_cmd(&args)?;
673        if !ok { return Err(AuroraError::ModuleError(format!("git checkout --orphan: {stderr}"))); }
674        println!("{} {}", localizer.t("✓ Создана orphan-ветка", "✓ Created orphan branch"), branch);
675        let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
676        return Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(output.trim().into())]]));
677    }
678
679    let target = target.unwrap_or("");
680    if create && !target.is_empty() { args.push("-b"); }
681    if !target.is_empty() { args.push(target); }
682
683    let (ok, stdout, stderr) = run_cmd(&args)?;
684    if !ok {
685        let hint = if stderr.contains("already exists") {
686            localizer.t("Ветка уже существует. Используй dev checkout <ветка> без --create.", "Branch exists. Use dev checkout <branch> without --create.")
687        } else if stderr.contains("did not match any") {
688            localizer.t("Ветка не найдена. Используй --create чтобы создать.", "Branch not found. Use --create to create it.")
689        } else { "" };
690        return Err(AuroraError::ModuleError(format!("git checkout: {stderr}{}", if hint.is_empty() { "".into() } else { format!("\n{hint}") })));
691    }
692
693    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
694    let mut rows: Vec<Vec<Value>> = Vec::new();
695    for line in output.lines() {
696        let line = line.trim();
697        if line.is_empty() { continue; }
698        rows.push(vec![Value::String(line.into())]);
699    }
700
701    println!("{} {} → {}", localizer.t("✓ Переключено на", "✓ Switched to"), target,
702        if create { localizer.t("(новая ветка)", "(new branch)") } else { "" });
703    Ok(Pipeline::table(vec!["output".into()], rows))
704}
705
706pub fn dev_stash_push(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
707    check_git_installed()?;
708    require_git_repo(localizer)?;
709    let (ok, _stdout, stderr) = run_cmd(&["stash"])?;
710    if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
711    println!("{}", localizer.t("✓ Изменения спрятаны!", "✓ Changes stashed!"));
712    Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(stderr.trim().into())]]))
713}
714
715pub fn dev_stash_list(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
716    check_git_installed()?;
717    require_git_repo(localizer)?;
718    let (ok, stdout, stderr) = run_cmd(&["stash", "list"])?;
719    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
720    if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
721    let mut rows: Vec<Vec<Value>> = Vec::new();
722    for line in output.lines() { let l = line.trim(); if l.is_empty() { continue; } rows.push(vec![Value::String(l.into())]); }
723    Ok(Pipeline::table(vec!["stash".into()], rows))
724}
725
726pub fn dev_stash_pop(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
727    check_git_installed()?;
728    require_git_repo(localizer)?;
729    let (ok, stdout, stderr) = run_cmd(&["stash", "pop"])?;
730    if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
731    println!("{}", localizer.t("✓ Изменения достаны!", "✓ Changes popped!"));
732    let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
733    Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(output.trim().into())]]))
734}
735
736pub fn dev_reset(files: &[String], hard: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
737    check_git_installed()?;
738    require_git_repo(localizer)?;
739
740    if hard {
741        if !prompt_yes_no(&localizer.t(
742            "⚠ HARD RESET — все несохранённые изменения будут потеряны! Продолжить?",
743            "⚠ HARD RESET — all unsaved changes will be lost! Continue?"
744        ))? {
745            return Ok(Pipeline::single(Value::String(localizer.t("Отменено.", "Cancelled.").to_string())));
746        }
747        let (ok, _, stderr) = run_cmd(&["reset", "--hard"])?;
748        if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
749        println!("{}", localizer.t("✓ Жёсткий сброс выполнен!", "✓ Hard reset done!"));
750    } else if files.is_empty() {
751        let (ok, _, stderr) = run_cmd(&["reset"])?;
752        if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
753        println!("{}", localizer.t("✓ Все файлы сняты с индекса!", "✓ All files unstaged!"));
754    } else {
755        let mut args = vec!["reset"];
756        for f in files { args.push(f); }
757        let (ok, _, stderr) = run_cmd(&args)?;
758        if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
759    }
760
761    Ok(Pipeline::table(
762        vec!["status".into()],
763        vec![vec![Value::String(if hard { "hard reset" } else { "reset" }.into())]],
764    ))
765}
766
767pub fn dev_config(key: Option<&str>, value: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
768    check_git_installed()?;
769
770    match (key, value) {
771        (Some(k), Some(v)) => {
772            let (ok, _, stderr) = run_cmd(&["config", k, v])?;
773            if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
774            println!("{} {k} = {v}", localizer.t("✓ Установлено:", "✓ Set:"));
775            Ok(Pipeline::table(
776                vec!["key".into(), "value".into()],
777                vec![vec![Value::String(k.into()), Value::String(v.into())]],
778            ))
779        }
780        (Some(k), None) => {
781            let (ok, stdout, stderr) = run_cmd(&["config", k])?;
782            if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
783            Ok(Pipeline::table(
784                vec!["key".into(), "value".into()],
785                vec![vec![Value::String(k.into()), Value::String(stdout.trim().into())]],
786            ))
787        }
788        (None, None) => {
789            let (ok, stdout, stderr) = run_cmd(&["config", "--list"])?;
790            if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
791            let mut rows: Vec<Vec<Value>> = Vec::new();
792            for line in stdout.lines() {
793                let line = line.trim();
794                if line.is_empty() { continue; }
795                if let Some(eq) = line.find('=') {
796                    rows.push(vec![Value::String(line[..eq].into()), Value::String(line[eq+1..].into())]);
797                }
798            }
799            Ok(Pipeline::table(vec!["key".into(), "value".into()], rows))
800        }
801        (None, Some(_)) => Err(AuroraError::InvalidInput(
802            "нельзя указать значение без ключа. Используй: aurora dev config <key> <value>".into()
803        )),
804    }
805}
806
807pub fn dev_remote_add(name: Option<&str>, url: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
808    check_git_installed()?;
809    require_git_repo(localizer)?;
810
811    let remote_name = name.unwrap_or("origin");
812
813    let repo_url = match url {
814        Some(u) => u.to_string(),
815        None => {
816            println!("{}", localizer.t(
817                "Введи URL репозитория (например https://github.com/user/repo.git):",
818                "Enter repository URL (e.g. https://github.com/user/repo.git):"
819            ));
820            prompt("> ")?
821        }
822    };
823
824    if repo_url.is_empty() {
825        return Err(AuroraError::ModuleError(
826            localizer.t("URL не может быть пустым.", "URL cannot be empty.").to_string()
827        ));
828    }
829
830    // Проверяем, существует ли уже такой remote
831    let (exists_ok, remotes, _) = run_cmd(&["remote", "get-url", remote_name])?;
832    if exists_ok {
833        if !prompt_yes_no(&format!(
834            "{} {}: {}. {}?",
835            localizer.t("Remote", "Remote"),
836            remote_name,
837            remotes.trim(),
838            localizer.t("Перезаписать", "Overwrite")
839        ))? {
840            return Ok(Pipeline::single(Value::String(
841                localizer.t("Отменено.", "Cancelled.").to_string()
842            )));
843        }
844        run_cmd(&["remote", "set-url", remote_name, &repo_url])?;
845        println!("{} {} = {repo_url}", localizer.t("✓ Remote обновлён:", "✓ Remote updated:"), remote_name);
846    } else {
847        run_cmd(&["remote", "add", remote_name, &repo_url])?;
848        println!("{} {} = {repo_url}", localizer.t("✓ Remote добавлен:", "✓ Remote added:"), remote_name);
849    }
850
851    Ok(Pipeline::table(
852        vec!["remote".into(), "url".into()],
853        vec![vec![Value::String(remote_name.into()), Value::String(repo_url)]],
854    ))
855}
856
857pub fn dev_remote_list(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
858    check_git_installed()?;
859    require_git_repo(localizer)?;
860
861    let (ok, stdout, stderr) = run_cmd(&["remote", "-v"])?;
862    if !ok {
863        return Err(AuroraError::ModuleError(
864            format!("git remote: {stderr}")
865        ));
866    }
867
868    let mut rows: Vec<Vec<Value>> = Vec::new();
869    for line in stdout.lines() {
870        let line = line.trim();
871        if line.is_empty() { continue; }
872        let parts: Vec<&str> = line.splitn(2, '\t').collect();
873        if parts.len() == 2 {
874            rows.push(vec![
875                Value::String(parts[0].into()),
876                Value::String(parts[1].into()),
877            ]);
878        }
879    }
880
881    if rows.is_empty() {
882        println!("{}", localizer.t(
883            "Нет удалённых репозиториев. Добавь: aurora dev remote add <url>",
884            "No remotes configured. Add one: aurora dev remote add <url>"
885        ));
886    }
887
888    Ok(Pipeline::table(vec!["remote".into(), "url".into()], rows))
889}
890
891pub fn dev_remote_remove(name: &str, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
892    check_git_installed()?;
893    require_git_repo(localizer)?;
894
895    let (ok, _, stderr) = run_cmd(&["remote", "remove", name])?;
896    if !ok {
897        let hint = if stderr.contains("No such remote") {
898            format!(". {}",
899                localizer.t("Проверь список: aurora dev remote list", "Check the list: aurora dev remote list")
900            )
901        } else { String::new() };
902        return Err(AuroraError::ModuleError(format!("git remote remove: {stderr}{hint}")));
903    }
904
905    println!("{} {}", localizer.t("✓ Remote удалён:", "✓ Remote removed:"), name);
906    Ok(Pipeline::table(
907        vec!["remote".into(), "action".into()],
908        vec![vec![Value::String(name.into()), Value::String("removed".into())]],
909    ))
910}
911
912pub fn dev_branch(delete: bool, rename: bool, name: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
913    check_git_installed()?;
914    require_git_repo(localizer)?;
915
916    if delete {
917        let branch = name.unwrap_or("");
918        if branch.is_empty() {
919            return Err(AuroraError::InvalidInput(
920                localizer.t("Укажи ветку для удаления: aurora dev branch -D <имя>", "Specify branch to delete: aurora dev branch -D <name>").to_string()
921            ));
922        }
923        let (ok, _, stderr) = run_cmd(&["branch", "-D", branch])?;
924        if !ok { return Err(AuroraError::ModuleError(format!("git branch -D: {stderr}"))); }
925        println!("{} {}", localizer.t("✓ Ветка удалена:", "✓ Branch deleted:"), branch);
926        return Ok(Pipeline::table(
927            vec!["branch".into(), "action".into()],
928            vec![vec![Value::String(branch.into()), Value::String("deleted".into())]],
929        ));
930    }
931
932    if rename {
933        let new_name = name.unwrap_or("");
934        if new_name.is_empty() {
935            return Err(AuroraError::InvalidInput(
936                localizer.t("Укажи новое имя: aurora dev branch -m <новое-имя>", "Specify new name: aurora dev branch -m <new-name>").to_string()
937            ));
938        }
939        let (ok, _, stderr) = run_cmd(&["branch", "-m", new_name])?;
940        if !ok { return Err(AuroraError::ModuleError(format!("git branch -m: {stderr}"))); }
941        println!("{} {}", localizer.t("✓ Ветка переименована в:", "✓ Branch renamed to:"), new_name);
942        return Ok(Pipeline::table(
943            vec!["branch".into(), "action".into()],
944            vec![vec![Value::String(new_name.into()), Value::String("renamed".into())]],
945        ));
946    }
947
948    let output = Command::new("git").arg("branch").output()
949        .map_err(|e| AuroraError::ModuleError(format!("git branch failed: {e}")))?;
950    let stdout = String::from_utf8_lossy(&output.stdout);
951    let mut rows: Vec<Vec<Value>> = Vec::new();
952    for line in stdout.lines() {
953        let line = line.trim();
954        if line.is_empty() { continue; }
955        let current = line.starts_with('*');
956        let name = if current { &line[2..] } else { line };
957        rows.push(vec![Value::String(name.trim().into()), Value::Bool(current)]);
958    }
959    Ok(Pipeline::table(vec!["branch".into(), "current".into()], rows))
960}