Skip to content

Commit 1651255

Browse files
committed
Migration logic for the auto-complete repo
1 parent 16bcca2 commit 1651255

File tree

10 files changed

+215
-42
lines changed

10 files changed

+215
-42
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ anyhow = "1.0.98"
3333
appkit-nsworkspace-bindings = { path = "crates/macos-utils/appkit-nsworkspace-bindings" }
3434
async-trait = "0.1.87"
3535
aws-smithy-runtime-api = "1.6.1"
36+
rustix = { version = "1.1.2", features = ["fs"] }
3637
aws-smithy-types = "1.2.10"
3738
aws-types = "1.3.0"
3839
base64 = "0.22.1"

crates/fig_desktop/src/install.rs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,6 @@ const PREVIOUS_VERSION_KEY: &str = "desktop.versionAtPreviousLaunch";
2424
#[cfg(target_os = "macos")]
2525
const MIGRATED_KEY: &str = "desktop.migratedFromFig";
2626

27-
#[cfg(target_os = "macos")]
28-
pub async fn migrate_data_dir() {
29-
// Migrate the user data dir
30-
if let (Ok(old), Ok(new)) = (fig_util::directories::old_fig_data_dir(), fig_data_dir()) {
31-
if !old.is_symlink() && old.is_dir() && !new.is_dir() {
32-
match tokio::fs::rename(&old, &new).await {
33-
Ok(()) => {
34-
if let Err(err) = symlink(&new, &old).await {
35-
error!(%err, "Failed to symlink old user data dir");
36-
}
37-
},
38-
Err(err) => {
39-
error!(%err, "Failed to migrate user data dir");
40-
},
41-
}
42-
}
43-
}
44-
}
45-
4627
#[cfg(target_os = "macos")]
4728
fn run_input_method_migration() {
4829
use fig_integrations::input_method::InputMethod;

crates/fig_desktop/src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ async fn main() -> ExitCode {
101101

102102
fig_telemetry::init_global_telemetry_emitter();
103103

104-
#[cfg(target_os = "macos")]
105-
install::migrate_data_dir().await;
104+
if let Err(err) = fig_install::migrate::migrate_if_needed().await {
105+
error!(%err, "Failed to migrate data directory");
106+
}
106107

107108
if let Err(err) = fig_settings::settings::init_global() {
108109
error!(%err, "failed to init global settings");

crates/fig_install/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ bytes.workspace = true
1919
camino.workspace = true
2020
cfg-if.workspace = true
2121
cookie = "0.18.0"
22+
eyre.workspace = true
2223
fig_integrations.workspace = true
2324
fig_os_shim.workspace = true
2425
fig_request.workspace = true
@@ -29,6 +30,7 @@ hex.workspace = true
2930
regex.workspace = true
3031
reqwest.workspace = true
3132
ring.workspace = true
33+
rustix.workspace = true
3234
semver = { version = "1.0.26", features = ["serde"] }
3335
serde.workspace = true
3436
serde_json.workspace = true

crates/fig_install/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod index;
66
mod linux;
77
#[cfg(target_os = "macos")]
88
pub mod macos;
9+
pub mod migrate;
910
#[cfg(windows)]
1011
mod windows;
1112

crates/fig_install/src/migrate.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use std::path::PathBuf;
2+
3+
use eyre::Result;
4+
use rustix::fs::{
5+
FlockOperation,
6+
flock,
7+
};
8+
use tokio::fs;
9+
use tracing::debug;
10+
11+
const KIRO_MIGRATION_KEY: &str = "migration.kiro.completed";
12+
13+
pub async fn migrate_if_needed() -> Result<bool> {
14+
let status = detect_migration().await?;
15+
16+
match status {
17+
MigrationStatus::Completed => {
18+
debug!("Migration already completed");
19+
return Ok(false);
20+
},
21+
MigrationStatus::NotNeeded => {
22+
debug!("No migration needed");
23+
mark_migration_completed()?;
24+
return Ok(false);
25+
},
26+
MigrationStatus::Needed => {
27+
debug!("Migrating database and settings");
28+
},
29+
}
30+
31+
let _lock = match acquire_migration_lock()? {
32+
Some(lock) => lock,
33+
None => {
34+
debug!("Migration already in progress");
35+
return Ok(false);
36+
},
37+
};
38+
39+
let old_dir = fig_util::directories::old_fig_data_dir()?;
40+
let new_dir = fig_util::directories::fig_data_dir()?;
41+
42+
debug!("Old directory: {}", old_dir.display());
43+
debug!("New directory: {}", new_dir.display());
44+
45+
// Copy essential files from old directory to new directory
46+
if !new_dir.exists() {
47+
fs::create_dir_all(&new_dir).await?;
48+
}
49+
debug!("Copying essential files from old to new directory");
50+
copy_essential_files(&old_dir, &new_dir).await?;
51+
52+
// Mark migration as completed in database
53+
debug!("Marking migration as completed");
54+
mark_migration_completed()?;
55+
56+
debug!("Migration completed successfully");
57+
Ok(true)
58+
}
59+
60+
#[derive(Debug)]
61+
enum MigrationStatus {
62+
NotNeeded,
63+
Needed,
64+
Completed,
65+
}
66+
67+
async fn detect_migration() -> Result<MigrationStatus> {
68+
let old_dir = fig_util::directories::old_fig_data_dir()?;
69+
let new_dir = fig_util::directories::fig_data_dir()?;
70+
71+
// If new directory doesn't exist yet, check if old directory exists
72+
if !new_dir.exists() {
73+
if old_dir.exists() && old_dir.is_dir() {
74+
return Ok(MigrationStatus::Needed);
75+
} else {
76+
return Ok(MigrationStatus::NotNeeded);
77+
}
78+
}
79+
80+
// New directory exists, check database flag (safe now since new_dir exists)
81+
let migration_completed = is_migration_completed()?;
82+
83+
if migration_completed {
84+
Ok(MigrationStatus::Completed)
85+
} else if old_dir.exists() && old_dir.is_dir() {
86+
Ok(MigrationStatus::Needed)
87+
} else {
88+
Ok(MigrationStatus::NotNeeded)
89+
}
90+
}
91+
92+
async fn copy_essential_files(src: &std::path::Path, dst: &std::path::Path) -> Result<()> {
93+
// Copy data-local-dir files (database and history)
94+
let data_files = ["data.sqlite3", "history"];
95+
for file_name in data_files {
96+
let src_path = src.join(file_name);
97+
let dst_path = dst.join(file_name);
98+
99+
if src_path.exists() {
100+
debug!("Copying {} to {}", src_path.display(), dst_path.display());
101+
fs::copy(&src_path, &dst_path).await?;
102+
}
103+
}
104+
105+
// Copy settings file to its specific location
106+
let old_settings = src.join("settings.json");
107+
if old_settings.exists() {
108+
let new_settings = fig_util::directories::settings_path()?;
109+
if !new_settings.exists() {
110+
if let Some(parent) = new_settings.parent() {
111+
fs::create_dir_all(parent).await?;
112+
}
113+
debug!("Copying settings to {}", new_settings.display());
114+
fs::copy(&old_settings, &new_settings).await?;
115+
}
116+
}
117+
118+
Ok(())
119+
}
120+
121+
fn mark_migration_completed() -> Result<()> {
122+
let db = fig_settings::sqlite::database()?;
123+
db.set_state_value(KIRO_MIGRATION_KEY, true)?;
124+
Ok(())
125+
}
126+
127+
struct MigrationLock {
128+
_file: std::fs::File,
129+
path: PathBuf,
130+
}
131+
132+
impl Drop for MigrationLock {
133+
fn drop(&mut self) {
134+
let _ = std::fs::remove_file(&self.path);
135+
}
136+
}
137+
138+
fn acquire_migration_lock() -> Result<Option<MigrationLock>> {
139+
let lock_path = migration_lock_path()?;
140+
141+
if let Some(parent) = lock_path.parent() {
142+
std::fs::create_dir_all(parent)?;
143+
}
144+
145+
// Check if lock file exists and is stale
146+
if lock_path.exists() {
147+
match std::fs::metadata(&lock_path)
148+
.and_then(|m| m.modified())
149+
.and_then(|t| t.elapsed().map_err(std::io::Error::other))
150+
{
151+
Ok(elapsed) if elapsed.as_secs() > 10 => {
152+
let _ = std::fs::remove_file(&lock_path);
153+
},
154+
_ => {
155+
// Continue with lock attempt even if metadata operations fail
156+
debug!("Failed to get lock file metadata, continuing with lock attempt");
157+
},
158+
}
159+
}
160+
161+
// Try to acquire the lock
162+
let file = std::fs::OpenOptions::new()
163+
.create(true)
164+
.write(true)
165+
.truncate(false)
166+
.open(&lock_path)?;
167+
168+
match flock(&file, FlockOperation::NonBlockingLockExclusive) {
169+
Ok(()) => Ok(Some(MigrationLock {
170+
_file: file,
171+
path: lock_path,
172+
})),
173+
Err(_) => Ok(None),
174+
}
175+
}
176+
177+
fn migration_lock_path() -> Result<PathBuf> {
178+
Ok(fig_util::directories::fig_data_dir()?.join("migration.lock"))
179+
}
180+
181+
fn is_migration_completed() -> Result<bool> {
182+
Ok(fig_settings::state::get_bool_or(KIRO_MIGRATION_KEY, false))
183+
}

crates/fig_util/src/consts.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ pub const PRODUCT_NAME: &str = "Kiro CLI";
2525

2626
pub const RUNTIME_DIR_NAME: &str = "cwrun";
2727

28+
/// Data directory name used in paths like ~/.local/share/{OLD_DATA_DIR_NAME}
29+
#[cfg(unix)]
30+
pub const OLD_DATA_DIR_NAME: &str = "amazon-q";
31+
#[cfg(windows)]
32+
pub const OLD_DATA_DIR_NAME: &str = "AmazonQ";
33+
2834
/// Data directory name used in paths like ~/.local/share/{DATA_DIR_NAME}
2935
#[cfg(unix)]
3036
pub const DATA_DIR_NAME: &str = "kiro-cli";

crates/fig_util/src/directories.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::system_info::{
2828
use crate::{
2929
BACKUP_DIR_NAME,
3030
DATA_DIR_NAME,
31+
OLD_DATA_DIR_NAME,
3132
TAURI_PRODUCT_NAME,
3233
};
3334

@@ -133,7 +134,9 @@ pub fn config_dir() -> Result<PathBuf> {
133134
/// This should be removed at some point in the future, once all our users have migrated
134135
/// - MacOS: `$HOME/Library/Application Support/codewhisperer`
135136
pub fn old_fig_data_dir() -> Result<PathBuf> {
136-
Ok(dirs::data_local_dir().ok_or(DirectoryError::NoHomeDirectory)?.join("q"))
137+
Ok(dirs::data_local_dir()
138+
.ok_or(DirectoryError::NoHomeDirectory)?
139+
.join(OLD_DATA_DIR_NAME))
137140
}
138141

139142
/// The q data directory
@@ -446,11 +449,11 @@ pub fn bundle_metadata_path<Ctx: EnvProvider + PlatformProvider>(ctx: &Ctx) -> R
446449

447450
/// The path to the fig settings file
448451
///
449-
/// - Linux: `$HOME/.aws/kiro-cli/settings.json`
450-
/// - MacOS: `$HOME/.aws/kiro-cli/settings.json`
451-
/// - Windows: `$HOME/.aws/kiro-cli/settings.json`
452+
/// - MacOS: `$HOME/.kiro/settings/cli.json`
453+
/// - Linux: `$HOME/.kiro/settings/cli.json`
454+
/// - Windows: `$HOME/.kiro/settings/cli.json`
452455
pub fn settings_path() -> Result<PathBuf> {
453-
Ok(home_dir()?.join(".aws").join("kiro-cli").join("settings.json"))
456+
Ok(home_dir()?.join(".kiro").join("settings").join("cli.json"))
454457
}
455458

456459
/// The path to the lock file used to indicate that the app is updating

crates/q_cli/src/cli/mod.rs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ use crate::util::{
9191
};
9292

9393
const LEGACY_WARNING: &str = "Warning! Q CLI is now Kiro CLI and should be invoked as kiro-cli rather than q";
94-
const KIRO_MIGRATION_KEY: &str = "migration.kiro.completed";
9594

9695
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
9796
pub enum OutputFormat {
@@ -303,12 +302,11 @@ pub struct Cli {
303302

304303
impl Cli {
305304
pub async fn execute(self) -> Result<ExitCode> {
305+
let is_hidden_command = matches!(self.subcommand, Some(CliRootCommands::Internal(_)));
306+
306307
// Show legacy warning if flag is set, but not for hidden commands
307-
if self.show_legacy_warning {
308-
let is_hidden_command = matches!(self.subcommand, Some(CliRootCommands::Internal(_)));
309-
if !is_hidden_command {
310-
eprintln!("\x1b[33m{}\x1b[0m", LEGACY_WARNING);
311-
}
308+
if self.show_legacy_warning && !is_hidden_command {
309+
eprintln!("\x1b[33m{}\x1b[0m", LEGACY_WARNING);
312310
}
313311

314312
// Initialize our logger and keep around the guard so logging can perform as expected.
@@ -342,16 +340,11 @@ impl Cli {
342340
debug!(command =? std::env::args().collect::<Vec<_>>(), "Command ran");
343341

344342
// Run migration silently on startup (skips if already completed or locked)
345-
let migration_complete = database()
346-
.ok()
347-
.and_then(|db| db.get_state_value(KIRO_MIGRATION_KEY).ok())
348-
.and_then(|v| v.and_then(|val| val.as_bool()))
349-
.unwrap_or(false);
350-
351-
if !migration_complete {
352-
let _ = Self::execute_chat("migrate", Some(vec!["--yes".to_owned()]), false).await;
353-
} else {
354-
debug!("Migration already completed");
343+
// Only run for non-hidden commands
344+
if !is_hidden_command {
345+
if let Err(err) = fig_install::migrate::migrate_if_needed().await {
346+
debug!(%err, "Failed to migrate");
347+
}
355348
}
356349

357350
self.send_telemetry().await;

0 commit comments

Comments
 (0)