Repository: aicers/linguist
Pull request #25: Add support for local UI and Frontary paths
Author: login: jadenow
Created: 2025-05-15T00:47:26Z | Merged: 2025-05-29T08:17:48Z

Closes #23
- Introduce `--ui-path` and `--frontary-path` flags to optionally use local repositories
- Update README and CHANGELOG to reflect new features

Linked issue #23: Support collecting strings from local repositories
- linguist currently clones the main branch of both the UI and frontary repositories to collect strings.
- However, If local modifications have been made to either repository, linguist should provide a way to verify key values locally before submitting a PR.
- Therefore, the following enhancements are required:
    - When running linguist, the local paths to the ui and frontary projects should be accepted as optional execution arguments.
    - Based on the provided paths, the behavior should be as follows:
       - No paths provided: Clone the main branches of both the ui and frontary repositories. Then, read the frontary commit hash or tag from the Cargo.toml in the ui repository and perform a checkout in the frontary repository accordingly.
       - Only the ui path provided: Clone the main branch of the frontary repository. Then, read the frontary commit hash or tag from the Cargo.toml in the local ui project and perform a checkout in the frontary repository accordingly.
       - Only the frontary path provided: Clone the main branch of the ui repository. Use the cloned ui repository and the local frontray project as is.
       - Both ui and frontary paths provided: Use the local projects from the specified paths as is.
  


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc8b126..1803cfe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,3 +22,5 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - Excluded fixed non-key strings and single-character alphabetic strings.
 - Excluded strings in Frontary Model components and chart-related ID strings.
 - Compared `all_strings` against the keys in ko-KR.json and en-US.json.
+- Added optional CLI flags `--ui-path <PATH>` and `--frontary-path <PATH>` to
+  allow using local copies of the UI and Frontary repositories.
diff --git a/Cargo.toml b/Cargo.toml
index 094b692..317a7a9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,8 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
+clap = { version = "4", features = ["derive"] }
+toml = "0.8"
 git2 = "0.20"
 regex = "1"
 serde_json = "1"
diff --git a/README.md b/README.md
index 8d95e83..eab1500 100644
--- a/README.md
+++ b/README.md
@@ -13,17 +13,7 @@ compare and validate key usage across multiple language versions.
 By identifying inconsistencies between these translations, `linguist` ensures
 that they remain accurate and complete.
 
-## Usage
-
-### Cloning Repositories with SSH Authentication
-
-To run `linguist`, you must specify the **path to your SSH private key** as an argument.
-
-```sh
-cargo run ~/.ssh/my_custom_rsa_key
-```
-
-### Prerequisites
+## Prerequisites
 
 Before running `linguist`, make sure:
 
@@ -49,9 +39,40 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                   | Required |
+|-----------------------------------|---------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`        | Path to your SSH private key file used for GitHub operations  | Yes      |
+| `--ui-path <UI_PATH>`             | Local path of the `aice-web` repo instead of cloning remotely | No       |
+| `--frontary-path <FRONTARY_PATH>` | Local path of the `frontary` repo instead of cloning remotely | No       |
+<!-- markdownlint-enable -->
+
+#### Notes on Arguments
+
+- The `--ssh-key <SSH_KEY_PATH>` argument:
+  - Required for GitHub authentication.
+  - Must point to your SSH private key (e.g., `~/.ssh/id_rsa`).
+  - Ensure the key is loaded into your SSH agent before running.
+
+- The `--ui-path <UI_PATH>` argument:
+  - Optional; if provided, uses this local directory as the aice-web repository.
+  - If omitted, linguist will clone aice-web from the default remote URL.
+
+- The `--frontary-path <FRONTARY_PATH>` argument:
+  - Optional; if provided, uses this local directory as the frontary repository.
+  - If omitted, linguist will clone frontary from the default remote URL.
+
 ## License
 
-Copyright 2022-2025 ClumL Inc.
+Copyright 2025 ClumL Inc.
 
 Licensed under [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
 (the "License"); you may not use this crate except in compliance with the License.
diff --git a/src/main.rs b/src/main.rs
index d65c1b7..d7e5546 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,14 +1,27 @@
 mod repo;
 use std::collections::HashSet;
-use std::env;
 use std::fs;
 use std::io::{self, Error, ErrorKind};
 use std::path::{Path, PathBuf};
-use std::process;
 
+use clap::Parser;
 use regex::Regex;
 use repo::{setup_ssh_agent, RepoManager};
 use serde_json::Value;
+use toml::Value as TomlValue;
+
+#[derive(Parser)]
+#[command(author, version, about)]
+struct Args {
+    #[arg(long)]
+    ui_path: Option<PathBuf>,
+
+    #[arg(long)]
+    frontary_path: Option<PathBuf>,
+
+    #[arg(long, value_name = "SSH_KEY")]
+    ssh_key: PathBuf,
+}
 
 const FIXED_EXCLUDED_STRINGS: &[&str] = &[
     "&nbsp;",
@@ -124,94 +137,178 @@ const FIXED_UI_KEY: &[&str] = &[
     "Whitelist",
 ];
 
-fn main() -> Result<(), io::Error> {
-    let args: Vec<String> = env::args().collect();
-
-    if args.len() < 2 {
-        eprintln!("Usage: {} <path-to-ssh-key>", args[0]);
-        process::exit(1);
-    }
-
-    let ssh_key_path = PathBuf::from(&args[1]);
-
-    if !ssh_key_path.exists() {
-        eprintln!("Error: SSH key not found at {:?}", ssh_key_path.display());
-        process::exit(1);
-    }
-
-    let repo_manager = RepoManager::new().map_err(|e| {
-        eprintln!("Error creating temp directory: {e}");
-        e
-    })?;
+const AICE_WEB_URL: &str = "git@github.com:aicers/aice-web.git";
+const FRONTARY_URL: &str = "https://github.com/aicers/frontary.git";
+const UI_REPO_NAME: &str = "aice-web";
+const FRONTARY_REPO_NAME: &str = "frontary";
 
-    let frontary_url = "https://github.com/aicers/frontary.git";
-    let ui_url = "git@github.com:aicers/aice-web.git";
+fn main() -> Result<(), io::Error> {
+    let args = Args::parse();
+    let ui_local = args.ui_path.clone();
+    let fr_local = args.frontary_path.clone();
+    let ssh_key = args.ssh_key;
+
+    initialize_ssh(&ssh_key)?;
+    log_repo_strategy(ui_local.as_ref(), fr_local.as_ref());
+
+    let repo_manager =
+        RepoManager::new().map_err(|e| io::Error::other(format!("Temp dir error: {e}")))?;
+
+    let ui_repo = prepare_repo(AICE_WEB_URL, ui_local, UI_REPO_NAME, &repo_manager)?;
+    let fr_repo = prepare_repo(FRONTARY_URL, fr_local, FRONTARY_REPO_NAME, &repo_manager)?;
+
+    checkout_frontary(
+        args.frontary_path.as_ref(),
+        ui_repo.as_path(),
+        fr_repo.as_path(),
+    )?;
+    process_keys(&ui_repo, &fr_repo)?;
+    Ok(())
+}
 
-    if let Err(e) = setup_ssh_agent(&ssh_key_path) {
+fn initialize_ssh(ssh_key: &Path) -> Result<(), io::Error> {
+    if let Err(e) = setup_ssh_agent(ssh_key) {
         eprintln!("❌ Failed to set up SSH agent: {e}");
         return Err(io::Error::other("SSH setup failed"));
     }
+    println!("✅ SSH authentication succeeded");
+    Ok(())
+}
 
-    println!("🛠️ Cloning repository: {frontary_url}...");
-    if let Err(e) = repo_manager.clone_repo(frontary_url, "frontary") {
-        eprintln!("Failed to clone frontary: {e}");
-        return Err(io::Error::other(format!(
-            "❌ Failed to clone frontary: {e}"
-        )));
+fn log_repo_strategy(ui_path: Option<&PathBuf>, fr_path: Option<&PathBuf>) {
+    match (ui_path, fr_path) {
+        (None, None) => println!(
+            "🔄 No local paths: will clone both '{UI_REPO_NAME}' and '{FRONTARY_REPO_NAME}'."
+        ),
+        (Some(path), None) => println!(
+            "🔄 Using local {UI_REPO_NAME} at {}; will clone {FRONTARY_REPO_NAME}.",
+            path.display()
+        ),
+        (None, Some(path)) => println!(
+            "🔄 Will clone {UI_REPO_NAME}; using local {FRONTARY_REPO_NAME} at {}.",
+            path.display()
+        ),
+        (Some(ui), Some(fr)) => println!(
+            "🔄 Using local {UI_REPO_NAME} at {} and {FRONTARY_REPO_NAME} at {}.",
+            ui.display(),
+            fr.display()
+        ),
     }
-    println!("🛠️ Cloning repository: {ui_url}...");
-    if let Err(e) = repo_manager.clone_repo(ui_url, "aice-web") {
-        eprintln!("Failed to clone aice-web: {e}");
-        return Err(io::Error::other(format!(
-            "❌ Failed to clone aice-web: {e}"
-        )));
+}
+
+fn checkout_frontary(
+    fr_local: Option<&PathBuf>,
+    ui_repo: &Path,
+    fr_repo: &Path,
+) -> Result<(), io::Error> {
+    if fr_local.is_none() {
+        let tag = read_frontary_req(ui_repo)?;
+        println!("🔀 Checking out frontary at commit: {tag}");
+        RepoManager::checkout(fr_repo, &tag)
+            .map_err(|e| io::Error::other(format!("Checkout failed: {e}")))?;
     }
+    Ok(())
+}
 
-    let temp_path = repo_manager.temp_dir.path();
-    // Update file paths dynamically using `temp_path`
-    let en_path = temp_path.join("aice-web/langs/en-US.json");
-    let ko_path = temp_path.join("aice-web/langs/ko-KR.json");
-    let ui_files = get_files_with_extension(temp_path.join("aice-web/src"), "rs")?;
-    let css_files = get_files_with_extension(temp_path.join("aice-web/static"), "css")?;
-    let frontary_file_paths = get_files_with_extension(temp_path.join("frontary"), "rs")?;
-    let css_classes_and_ids = extract_css_classes_and_ids(&css_files)?;
+fn prepare_repo(
+    repo_url: &str,
+    override_path: Option<PathBuf>,
+    name: &str,
+    manager: &RepoManager,
+) -> Result<PathBuf, io::Error> {
+    if let Some(path) = override_path {
+        if path.exists() {
+            return Ok(path);
+        }
+        return Err(io::Error::new(
+            io::ErrorKind::NotFound,
+            format!("Local {name} repo not found at {}", path.display()),
+        ));
+    }
 
-    let en_key_list = extract_keys_from_json(&en_path)?;
-    let ko_key_list = extract_keys_from_json(&ko_path)?;
+    manager
+        .clone_repo(repo_url, name)
+        .map_err(|e| io::Error::other(format!("Failed to clone {name}: {e}")))?;
+    Ok(manager.temp_dir.path().join(name))
+}
 
+fn process_keys(ui_repo: &Path, fr_repo: &Path) -> Result<(), io::Error> {
+    // collect paths & files
+    let en_path = ui_repo.join("langs/en-US.json");
+    let ko_path = ui_repo.join("langs/ko-KR.json");
+    let ui_files = get_files_with_extension(ui_repo.join("src"), "rs")?;
+    let css_files = get_files_with_extension(ui_repo.join("static"), "css")?;
+    let frontary_files = get_files_with_extension(fr_repo.join("src"), "rs")?;
+    let css_ids = extract_css_classes_and_ids(&css_files)?;
+    // JSON keys
+    let en_keys = extract_keys_from_json(&en_path)?;
+    let ko_keys = extract_keys_from_json(&ko_path)?;
+    // regex for string literals
     let re = Regex::new(r#""([^"\\]*(\\.[^"\\]*)*)""#)
-        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+        .map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
 
-    let mut ui_strings: HashSet<String> = ui_files
+    let mut ui_strings = ui_files
         .into_iter()
-        .map(|path| collect_strings_from_file(&path, &re))
-        .flat_map(|result| result.into_iter().flatten())
+        .map(|p| collect_strings_from_file(&p, &re))
+        .flat_map(Result::into_iter)
+        .flatten()
         .collect::<HashSet<_>>();
-
     ui_strings.retain(|s| {
-        !FIXED_EXCLUDED_STRINGS.contains(&s.as_str())
-            && !css_classes_and_ids
-                .iter()
-                .any(|class_or_id| class_or_id == s)
+        !FIXED_EXCLUDED_STRINGS.iter().any(|&e| e == s) && !css_ids.iter().any(|id| id == s)
     });
-
     ui_strings.extend(FIXED_UI_KEY.iter().map(ToString::to_string));
 
-    let mut frontary_strings: HashSet<String> = frontary_file_paths
+    let mut frontary_strings = frontary_files
         .into_iter()
-        .map(|path| extract_frontary_keys_from_file(&path, &re))
-        .flat_map(|result| result.into_iter().flatten())
-        .collect();
-
+        .map(|p| extract_frontary_keys_from_file(&p, &re))
+        .flat_map(Result::into_iter)
+        .flatten()
+        .collect::<HashSet<_>>();
     frontary_strings.extend(FIXED_FRONTARY_KEY.iter().map(ToString::to_string));
 
-    let all_strings: HashSet<String> = ui_strings.union(&frontary_strings).cloned().collect();
+    let all_strings = ui_strings.union(&frontary_strings).cloned().collect();
+    compare_keys("all_strings", &all_strings, "ko-KR.json", &ko_keys);
+    compare_keys("all_strings", &all_strings, "en-US.json", &en_keys);
+    compare_keys("ko-KR.json", &ko_keys, "en-US.json", &en_keys);
+    Ok(())
+}
 
-    compare_keys("all_strings", &all_strings, "ko-KR.json", &ko_key_list);
-    compare_keys("all_strings", &all_strings, "en-US.json", &en_key_list);
-    compare_keys("ko-KR.json", &ko_key_list, "en-US.json", &en_key_list);
+fn read_frontary_req(ui_root: &Path) -> Result<String, io::Error> {
+    let cargo_toml = ui_root.join("Cargo.toml");
+    let toml_str = fs::read_to_string(&cargo_toml).map_err(|e| {
+        io::Error::new(
+            e.kind(),
+            format!("Failed to read {}: {e}", cargo_toml.display()),
+        )
+    })?;
 
-    Ok(())
+    let cargo: TomlValue = toml::from_str(&toml_str)
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("Invalid TOML: {e}")))?;
+
+    if let Some(frontary) = cargo
+        .get("dependencies")
+        .and_then(|deps| deps.get("frontary"))
+    {
+        if let Some(version) = frontary.as_str() {
+            return Ok(version.to_string());
+        }
+        if let Some(table) = frontary.as_table() {
+            if let Some(tag) = table.get("tag").and_then(TomlValue::as_str) {
+                return Ok(tag.to_string());
+            }
+            if let Some(rev) = table.get("rev").and_then(TomlValue::as_str) {
+                return Ok(rev.to_string());
+            }
+            if let Some(ver) = table.get("version").and_then(TomlValue::as_str) {
+                return Ok(ver.to_string());
+            }
+        }
+    }
+
+    Err(io::Error::new(
+        io::ErrorKind::NotFound,
+        "`frontary` dependency not found in Cargo.toml",
+    ))
 }
 
 fn extract_keys_from_json<P: AsRef<Path>>(path: P) -> Result<HashSet<String>, io::Error> {
diff --git a/src/repo.rs b/src/repo.rs
index f10e979..717f0cd 100644
--- a/src/repo.rs
+++ b/src/repo.rs
@@ -3,7 +3,7 @@ use std::io::{self, Error};
 use std::path::{Path, PathBuf};
 use std::process::{Command, Stdio};
 
-use git2::{build::RepoBuilder, Cred, FetchOptions, RemoteCallbacks};
+use git2::{build::RepoBuilder, BranchType, Cred, FetchOptions, RemoteCallbacks, Repository};
 use tempfile::TempDir;
 
 pub(crate) struct RepoManager {
@@ -55,6 +55,26 @@ impl RepoManager {
             Err(_) => Err(git2::Error::from_str("❌ Failed to clone repository.")),
         }
     }
+
+    pub(crate) fn checkout(repo_path: &Path, reference: &str) -> Result<(), git2::Error> {
+        let repo = Repository::open(repo_path)?;
+        let obj = repo.revparse_single(reference)?;
+
+        repo.checkout_tree(&obj, None)?;
+
+        if repo.find_branch(reference, BranchType::Local).is_ok() {
+            repo.set_head(&format!("refs/heads/{reference}"))?;
+        } else if repo
+            .find_reference(&format!("refs/tags/{reference}"))
+            .is_ok()
+        {
+            repo.set_head(&format!("refs/tags/{reference}"))?;
+        } else {
+            repo.set_head_detached(obj.id())?;
+        }
+
+        Ok(())
+    }
 }
 
 pub(crate) fn setup_ssh_agent(ssh_key_path: &Path) -> Result<(), git2::Error> {


Reviews:
user: login: kimhanbeom
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T02:16:51Z
user: login: kimhanbeom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T04:15:31Z
user: login: kimhanbeom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T04:16:08Z
user: login: kimhanbeom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T04:19:00Z
user: login: kimhanbeom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T06:52:31Z
user: login: kimhanbeom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:03:37Z
user: login: kimhanbeom
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T04:03:14Z
user: login: kimhanbeom
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T07:08:41Z
user: login: kimhanbeom
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T08:33:32Z
user: login: kimhanbeom
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T08:45:40Z
user: login: kimhanbeom
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T06:49:50Z
user: login: kimhanbeom
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T07:45:36Z
user: login: kimhanbeom
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T08:16:59Z

Review comments:
diff_hunk: @@ -50,6 +50,41 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+### Options
path: README.md
user: login: kimhanbeom
body: Could you please write it in a similar format to the one we have in our other repositories?
An example is shown below

## Usage

You can run liguist by invoking the following command:

```sh
liguist [--ui-path <UI_PATH>] ......
```

### Arguments

The following arguments configure the behavior and runtime of Liguist:


| Argument        | Description                                    | Required |
| --------------- | ---------------------------------------------- | -------- |
| `<UI_PATH>` |  ....     | ....      |
| `<FRONTARY_PATH>` |  ....     | ....      |

created_at: 2025-05-20T01:31:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +50,41 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+### Options
+
+- `--ui-path <PATH>`
+  Use a local clone of the `aice-web` repo instead of cloning remote.
+- `--frontary-path <PATH>`
+  Use a local clone of the `frontary` repo instead of cloning remote.
+
+### Scenarios
+
+1. **No paths provided** (default cloning):
+
+   ```sh
+   cargo run -- ~/.ssh/my_key
path: README.md
user: login: kimhanbeom
body: Please modify so that ssh key is also set via command-line option (ex) --ssh-key)
created_at: 2025-05-20T01:53:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +46,42 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                          | Required |
+|-----------------------------------|----------------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`                  | Path to your SSH private key file used for GitHub operations         | Yes      |
+| `--ui-path <UI_PATH>`             | Local path of the `aice-web` repo instead of cloning remotely  | No       |
+| `--frontary-path <FRONTARY_PATH>` | Local path of the `frontary` repo instead of cloning remotely  | No       |
+
path: README.md
user: login: kimhanbeom
body: Could you please remove the blank lines to minimize the markdownlint-disable scope.

created_at: 2025-05-23T04:15:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +46,42 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                          | Required |
+|-----------------------------------|----------------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`                  | Path to your SSH private key file used for GitHub operations         | Yes      |
path: README.md
user: login: kimhanbeom
body: Could you please adjust whitespace so that the vertical `|` lines in Markdown table line up neatly, ensuring consistent column widths?
created_at: 2025-05-23T04:16:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +46,42 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                          | Required |
+|-----------------------------------|----------------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`                  | Path to your SSH private key file used for GitHub operations         | Yes      |
+| `--ui-path <UI_PATH>`             | Local path of the `aice-web` repo instead of cloning remotely  | No       |
+| `--frontary-path <FRONTARY_PATH>` | Local path of the `frontary` repo instead of cloning remotely  | No       |
+
+<!-- markdownlint-enable -->
+
+#### Notes on Arguments
+
+- The `--ssh-key <SSH_KEY_PATH>` argument:
+  - Required for GitHub authentication.
+  - Must point to your SSH private key (e.g., `~/.ssh/id_rsa`).
+  - Ensure the key is loaded into your SSH agent before running.
+
+- The `--ui-path <UI_PATH>` argument:
+  - Optional; if provided, uses this local directory as the
+    aice-web repository.
+  - If omitted, linguist will clone aice-web from the default
+    remote URL.
+
+- The `--frontary-path <FRONTARY_PATH>` argument:
+  - Optional; if provided, uses this local directory as the
+    frontary repository.
+  - If omitted, linguist will clone frontary from the default
+    remote URL.
+
 ## License
 
 Copyright 2022-2025 ClumL Inc.
path: README.md
user: login: kimhanbeom
body: Could you please change the copyright from `2022-2025` to `2025`?
created_at: 2025-05-23T04:19:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,93 +137,217 @@ const FIXED_UI_KEY: &[&str] = &[
 ];
 
 fn main() -> Result<(), io::Error> {
-    let args: Vec<String> = env::args().collect();
-
-    if args.len() < 2 {
-        eprintln!("Usage: {} <path-to-ssh-key>", args[0]);
-        process::exit(1);
-    }
-
-    let ssh_key_path = PathBuf::from(&args[1]);
-
-    if !ssh_key_path.exists() {
-        eprintln!("Error: SSH key not found at {:?}", ssh_key_path.display());
-        process::exit(1);
-    }
-
-    let repo_manager = RepoManager::new().map_err(|e| {
-        eprintln!("Error creating temp directory: {e}");
-        e
-    })?;
-
-    let frontary_url = "https://github.com/aicers/frontary.git";
-    let ui_url = "git@github.com:aicers/aice-web.git";
+    let args = Args::parse();
+    let ui_local = args.ui_path.clone();
+    let fr_local = args.frontary_path.clone();
+    let ssh_key = args.ssh_key;
+
+    initialize_ssh(&ssh_key)?;
+    log_repo_strategy(ui_local.as_ref(), fr_local.as_ref());
+
+    let repo_manager =
+        RepoManager::new().map_err(|e| io::Error::other(format!("Temp dir error: {e}")))?;
+
+    let (ui_repo, fr_repo) = prepare_repositories(ui_local, fr_local, &repo_manager)?;
+    checkout_frontary(
+        args.frontary_path.as_ref(),
+        ui_repo.as_path(),
+        fr_repo.as_path(),
+    )?;
+    process_keys(&ui_repo, &fr_repo)?;
+    Ok(())
+}
 
-    if let Err(e) = setup_ssh_agent(&ssh_key_path) {
+fn initialize_ssh(ssh_key: &Path) -> Result<(), io::Error> {
+    if let Err(e) = setup_ssh_agent(ssh_key) {
         eprintln!("❌ Failed to set up SSH agent: {e}");
         return Err(io::Error::other("SSH setup failed"));
     }
+    println!("✅ SSH authentication succeeded");
+    Ok(())
+}
 
-    println!("🛠️ Cloning repository: {frontary_url}...");
-    if let Err(e) = repo_manager.clone_repo(frontary_url, "frontary") {
-        eprintln!("Failed to clone frontary: {e}");
-        return Err(io::Error::other(format!(
-            "❌ Failed to clone frontary: {e}"
-        )));
-    }
-    println!("🛠️ Cloning repository: {ui_url}...");
-    if let Err(e) = repo_manager.clone_repo(ui_url, "aice-web") {
-        eprintln!("Failed to clone aice-web: {e}");
-        return Err(io::Error::other(format!(
-            "❌ Failed to clone aice-web: {e}"
-        )));
+fn log_repo_strategy(ui_path: Option<&PathBuf>, fr_path: Option<&PathBuf>) {
+    match (ui_path.as_ref(), fr_path.as_ref()) {
+        (None, None) => {
+            println!("🔄 No local paths: will clone both 'aice-web' and 'frontary'.");
+        }
+        (Some(path), None) => {
+            println!(
+                "🔄 Using local aice-web at {}; will clone frontary.",
+                path.display()
+            );
+        }
+        (None, Some(path)) => {
+            println!(
+                "🔄 Will clone aice-web; using local frontary at {}.",
+                path.display()
+            );
+        }
+        (Some(ui_path), Some(fr_path)) => {
+            println!(
+                "🔄 Using local aice-web at {} and frontary at {}.",
+                ui_path.display(),
+                fr_path.display()
+            );
+        }
     }
+}
 
-    let temp_path = repo_manager.temp_dir.path();
-    // Update file paths dynamically using `temp_path`
-    let en_path = temp_path.join("aice-web/langs/en-US.json");
-    let ko_path = temp_path.join("aice-web/langs/ko-KR.json");
-    let ui_files = get_files_with_extension(temp_path.join("aice-web/src"), "rs")?;
-    let css_files = get_files_with_extension(temp_path.join("aice-web/static"), "css")?;
-    let frontary_file_paths = get_files_with_extension(temp_path.join("frontary"), "rs")?;
-    let css_classes_and_ids = extract_css_classes_and_ids(&css_files)?;
+fn prepare_repositories(
+    ui_local: Option<PathBuf>,
+    fr_local: Option<PathBuf>,
+    mgr: &RepoManager,
+) -> Result<(PathBuf, PathBuf), io::Error> {
+    // aice-web
+    let ui_repo = if let Some(path) = ui_local {
+        println!("📂 Using local aice-web at {}", path.display());
path: src/main.rs
user: login: kimhanbeom
body: How about simplifying this part by removing the extra logs, since `log_repo_strategy` already provides sufficient details?

```rust
let ui_repo = prepare_repo(
    "git@github.com:aicers/aice-web.git",
    ui_local,
    "aice-web",
    mgr,
)?;
``` 
created_at: 2025-05-23T06:52:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,93 +137,217 @@ const FIXED_UI_KEY: &[&str] = &[
 ];
 
 fn main() -> Result<(), io::Error> {
-    let args: Vec<String> = env::args().collect();
-
-    if args.len() < 2 {
-        eprintln!("Usage: {} <path-to-ssh-key>", args[0]);
-        process::exit(1);
-    }
-
-    let ssh_key_path = PathBuf::from(&args[1]);
-
-    if !ssh_key_path.exists() {
-        eprintln!("Error: SSH key not found at {:?}", ssh_key_path.display());
-        process::exit(1);
-    }
-
-    let repo_manager = RepoManager::new().map_err(|e| {
-        eprintln!("Error creating temp directory: {e}");
-        e
-    })?;
-
-    let frontary_url = "https://github.com/aicers/frontary.git";
-    let ui_url = "git@github.com:aicers/aice-web.git";
+    let args = Args::parse();
+    let ui_local = args.ui_path.clone();
+    let fr_local = args.frontary_path.clone();
+    let ssh_key = args.ssh_key;
+
+    initialize_ssh(&ssh_key)?;
+    log_repo_strategy(ui_local.as_ref(), fr_local.as_ref());
+
+    let repo_manager =
+        RepoManager::new().map_err(|e| io::Error::other(format!("Temp dir error: {e}")))?;
+
+    let (ui_repo, fr_repo) = prepare_repositories(ui_local, fr_local, &repo_manager)?;
+    checkout_frontary(
+        args.frontary_path.as_ref(),
+        ui_repo.as_path(),
+        fr_repo.as_path(),
+    )?;
+    process_keys(&ui_repo, &fr_repo)?;
+    Ok(())
+}
 
-    if let Err(e) = setup_ssh_agent(&ssh_key_path) {
+fn initialize_ssh(ssh_key: &Path) -> Result<(), io::Error> {
+    if let Err(e) = setup_ssh_agent(ssh_key) {
         eprintln!("❌ Failed to set up SSH agent: {e}");
         return Err(io::Error::other("SSH setup failed"));
     }
+    println!("✅ SSH authentication succeeded");
+    Ok(())
+}
 
-    println!("🛠️ Cloning repository: {frontary_url}...");
-    if let Err(e) = repo_manager.clone_repo(frontary_url, "frontary") {
-        eprintln!("Failed to clone frontary: {e}");
-        return Err(io::Error::other(format!(
-            "❌ Failed to clone frontary: {e}"
-        )));
-    }
-    println!("🛠️ Cloning repository: {ui_url}...");
-    if let Err(e) = repo_manager.clone_repo(ui_url, "aice-web") {
-        eprintln!("Failed to clone aice-web: {e}");
-        return Err(io::Error::other(format!(
-            "❌ Failed to clone aice-web: {e}"
-        )));
+fn log_repo_strategy(ui_path: Option<&PathBuf>, fr_path: Option<&PathBuf>) {
+    match (ui_path.as_ref(), fr_path.as_ref()) {
+        (None, None) => {
+            println!("🔄 No local paths: will clone both 'aice-web' and 'frontary'.");
+        }
+        (Some(path), None) => {
+            println!(
+                "🔄 Using local aice-web at {}; will clone frontary.",
+                path.display()
+            );
+        }
+        (None, Some(path)) => {
+            println!(
+                "🔄 Will clone aice-web; using local frontary at {}.",
+                path.display()
+            );
+        }
+        (Some(ui_path), Some(fr_path)) => {
+            println!(
+                "🔄 Using local aice-web at {} and frontary at {}.",
+                ui_path.display(),
+                fr_path.display()
+            );
+        }
     }
+}
 
-    let temp_path = repo_manager.temp_dir.path();
-    // Update file paths dynamically using `temp_path`
-    let en_path = temp_path.join("aice-web/langs/en-US.json");
-    let ko_path = temp_path.join("aice-web/langs/ko-KR.json");
-    let ui_files = get_files_with_extension(temp_path.join("aice-web/src"), "rs")?;
-    let css_files = get_files_with_extension(temp_path.join("aice-web/static"), "css")?;
-    let frontary_file_paths = get_files_with_extension(temp_path.join("frontary"), "rs")?;
-    let css_classes_and_ids = extract_css_classes_and_ids(&css_files)?;
+fn prepare_repositories(
+    ui_local: Option<PathBuf>,
+    fr_local: Option<PathBuf>,
+    mgr: &RepoManager,
+) -> Result<(PathBuf, PathBuf), io::Error> {
+    // aice-web
+    let ui_repo = if let Some(path) = ui_local {
+        println!("📂 Using local aice-web at {}", path.display());
path: src/main.rs
user: login: kimhanbeom
body: Since the code clearly indicates which repository is being handled, the comment stating the repository name seems unnecessary. Would you mind removing it?
created_at: 2025-05-23T07:03:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,9 +13,7 @@ compare and validate key usage across multiple language versions.
 By identifying inconsistencies between these translations, `linguist` ensures
 that they remain accurate and complete.
 
-## Usage
-
-### Cloning Repositories with SSH Authentication
+## Prerequisites
 
 To run `linguist`, you must specify the **path to your SSH private key** as an
path: README.md
user: login: kimhanbeom
body: The part about the SSH key execution option is already sufficiently explained in the Usage section, so I suggest removing it to avoid redundancy.
created_at: 2025-05-26T04:02:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,15 +15,6 @@ that they remain accurate and complete.
 
 ## Usage
 
-### Cloning Repositories with SSH Authentication
-
-To run `linguist`, you must specify the **path to your SSH private key** as an
-argument.
-
-```sh
-cargo run ~/.ssh/my_custom_rsa_key
-```
-
 ### Prerequisites
path: README.md
user: login: kimhanbeom
body: Could you please modify the Prerequisites section and Usage section as below. 

```
## Prerequisites

Before running `linguist`, make sure:

- You have GitHub SSH authentication set up.
- Your SSH key is added to GitHub.
.....

## Usage

To start linguist, run the following command:

linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]

....

### Arguments


#### Notes on Arguments

```
created_at: 2025-05-27T07:08:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,14 +1,26 @@
 mod repo;
 use std::collections::HashSet;
-use std::env;
 use std::fs;
 use std::io::{self, Error, ErrorKind};
 use std::path::{Path, PathBuf};
-use std::process;
 
+use clap::Parser;
 use regex::Regex;
 use repo::{setup_ssh_agent, RepoManager};
 use serde_json::Value;
+use toml::Value as TomlValue;
path: src/main.rs
user: login: kimhanbeom
body: You should add a blank line between the use statement and the derive attribute for better readability. Please add a blank line.
created_at: 2025-05-28T08:36:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,6 +55,25 @@ impl RepoManager {
             Err(_) => Err(git2::Error::from_str("❌ Failed to clone repository.")),
         }
     }
+    pub(crate) fn checkout(repo_path: &Path, reference: &str) -> Result<(), git2::Error> {
path: src/repo.rs
user: login: kimhanbeom
body: Please add a blank line.
created_at: 2025-05-28T08:45:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,9 +39,44 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                   | Required |
+|-----------------------------------|---------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`        | Path to your SSH private key file used for GitHub operations  | Yes      |
+| `--ui-path <UI_PATH>`             | Local path of the `aice-web` repo instead of cloning remotely | No       |
+| `--frontary-path <FRONTARY_PATH>` | Local path of the `frontary` repo instead of cloning remotely | No       |
+<!-- markdownlint-enable -->
+
+#### Notes on Arguments
+
+- The `--ssh-key <SSH_KEY_PATH>` argument:
+  - Required for GitHub authentication.
+  - Must point to your SSH private key (e.g., `~/.ssh/id_rsa`).
+  - Ensure the key is loaded into your SSH agent before running.
+
+- The `--ui-path <UI_PATH>` argument:
+  - Optional; if provided, uses this local directory as the
+    aice-web repository.
path: README.md
user: login: kimhanbeom
body: Could you please merge the following line into the previous one.
created_at: 2025-05-29T06:46:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,9 +39,44 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                   | Required |
+|-----------------------------------|---------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`        | Path to your SSH private key file used for GitHub operations  | Yes      |
+| `--ui-path <UI_PATH>`             | Local path of the `aice-web` repo instead of cloning remotely | No       |
+| `--frontary-path <FRONTARY_PATH>` | Local path of the `frontary` repo instead of cloning remotely | No       |
+<!-- markdownlint-enable -->
+
+#### Notes on Arguments
+
+- The `--ssh-key <SSH_KEY_PATH>` argument:
+  - Required for GitHub authentication.
+  - Must point to your SSH private key (e.g., `~/.ssh/id_rsa`).
+  - Ensure the key is loaded into your SSH agent before running.
+
+- The `--ui-path <UI_PATH>` argument:
+  - Optional; if provided, uses this local directory as the
+    aice-web repository.
path: README.md
user: login: kimhanbeom
body: Please merge the following phrases—`remote URL.` and `frontary repository.`—into a single line as well.
created_at: 2025-05-29T06:49:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,9 +39,41 @@ Before running `linguist`, make sure:
 
   If authentication issues persist, rerun this command again.
 
+## Usage
+
+```sh
+linguist --ssh-key <SSH_KEY_PATH> [--ui-path <UI_PATH>] [--frontary-path \ <FRONTARY_PATH>]
+```
+
+### Arguments
+
+<!-- markdownlint-disable -->
+| Argument                          | Description                                                   | Required |
+|-----------------------------------|---------------------------------------------------------------|----------|
+| `--ssh-key <SSH_KEY_PATH>`        | Path to your SSH private key file used for GitHub operations  | Yes      |
+| `--ui-path <UI_PATH>`             | Local path of the `aice-web` repo instead of cloning remotely | No       |
+| `--frontary-path <FRONTARY_PATH>` | Local path of the `frontary` repo instead of cloning remotely | No       |
+<!-- markdownlint-enable -->
+
+#### Notes on Arguments
+
+- The `--ssh-key <SSH_KEY_PATH>` argument:
+  - Required for GitHub authentication.
+  - Must point to your SSH private key (e.g., `~/.ssh/id_rsa`).
+  - Ensure the key is loaded into your SSH agent before running.
+
+- The `--ui-path <UI_PATH>` argument:
+  - Optional; if provided, uses this local directory as the aice-web repository.
+  - If omitted, linguist will clone aice-web from the default remote URL.
+
+- The `--frontary-path <FRONTARY_PATH>` argument:
+  - Optional; if provided, uses this local directory as the frontary repository.
+  - If omitted, linguist will clone frontary from the default
+    remote URL.
path: README.md
user: login: kimhanbeom
body: Could you also merge this part into the previous line?
created_at: 2025-05-29T07:45:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: kimhanbeom
created_at: 2025-05-20T02:18:38Z
body: When running linguist, an error occurs as above due to a path value problem. Please fix it to run normally.
```
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
```
author_association: CONTRIBUTOR
user: login: kimhanbeom
created_at: 2025-05-23T05:24:17Z
body: Define url and repository name as `const` instead of using them directly as `&str`.
author_association: CONTRIBUTOR
user: login: kimhanbeom
created_at: 2025-05-27T06:11:51Z
body: @jadenow 
I apologize for the lack of clarity in my explanation. I didn’t mean to suggest removing the entire Prerequisites section—just the part mentioned below.

```
  To run `linguist`, you must specify the **path to your SSH private key** as an
  argument.
       
   cargo run ~/.ssh/my_custom_rsa_key
```
author_association: CONTRIBUTOR
user: login: kimhanbeom
created_at: 2025-05-29T01:24:53Z
body: Please update the README and CHANGELOG in this PR if there are any modifications needed based on the [revised Markdown formatting rules](https://github.com/aicers/linguist/issues/28)
author_association: CONTRIBUTOR
user: login: kimhanbeom
created_at: 2025-05-29T07:26:18Z
body: I apologize for the lack of clarity in my explanation. My intention was to merge the lines into a single line since the total character count does not exceed 80.

```
  - Optional; if provided, uses this local directory as the aice-web repository.
  - If omitted, linguist will clone aice-web from the default remote URL.
```
author_association: CONTRIBUTOR