Skip to content

Commit 79c67d6

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

File tree

10 files changed

+234
-42
lines changed

10 files changed

+234
-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: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use std::path::PathBuf;
2+
3+
use eyre::Result;
4+
use rustix::fs::{
5+
FlockOperation,
6+
flock,
7+
};
8+
use serde_json::Value;
9+
use tokio::fs;
10+
use tracing::debug;
11+
12+
const KIRO_MIGRATION_KEY: &str = "migration.kiro.completed";
13+
14+
pub async fn migrate_if_needed() -> Result<bool> {
15+
let status = detect_migration().await?;
16+
17+
match status {
18+
MigrationStatus::Completed => {
19+
debug!("Migration already completed");
20+
return Ok(false);
21+
},
22+
MigrationStatus::NotNeeded => {
23+
debug!("No migration needed");
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+
// Migrate settings to new location
53+
debug!("Migrating settings");
54+
migrate_settings().await?;
55+
56+
// Mark migration as completed in database
57+
debug!("Marking migration as completed");
58+
mark_migration_completed()?;
59+
60+
debug!("Migration completed successfully");
61+
Ok(true)
62+
}
63+
64+
#[derive(Debug)]
65+
enum MigrationStatus {
66+
NotNeeded,
67+
Needed,
68+
Completed,
69+
}
70+
71+
async fn detect_migration() -> Result<MigrationStatus> {
72+
let old_dir = fig_util::directories::old_fig_data_dir()?;
73+
let new_dir = fig_util::directories::fig_data_dir()?;
74+
75+
// If new directory doesn't exist yet, check if old directory exists
76+
if !new_dir.exists() {
77+
if old_dir.exists() && old_dir.is_dir() {
78+
return Ok(MigrationStatus::Needed);
79+
} else {
80+
return Ok(MigrationStatus::NotNeeded);
81+
}
82+
}
83+
84+
// New directory exists, check database flag (safe now since new_dir exists)
85+
let migration_completed = is_migration_completed()?;
86+
87+
if migration_completed {
88+
Ok(MigrationStatus::Completed)
89+
} else if old_dir.exists() && old_dir.is_dir() {
90+
Ok(MigrationStatus::Needed)
91+
} else {
92+
Ok(MigrationStatus::NotNeeded)
93+
}
94+
}
95+
96+
async fn migrate_settings() -> Result<()> {
97+
let old_settings = fig_util::directories::fig_data_dir()?.join("settings.json");
98+
let new_settings = fig_util::directories::settings_path()?;
99+
100+
if !old_settings.exists() || new_settings.exists() {
101+
return Ok(());
102+
}
103+
104+
let content = fs::read_to_string(&old_settings).await?;
105+
let settings: serde_json::Map<String, Value> = serde_json::from_str(&content)?;
106+
107+
if let Some(parent) = new_settings.parent() {
108+
fs::create_dir_all(parent).await?;
109+
}
110+
111+
let json = serde_json::to_string_pretty(&settings)?;
112+
fs::write(new_settings, json).await?;
113+
114+
Ok(())
115+
}
116+
117+
async fn copy_essential_files(src: &std::path::Path, dst: &std::path::Path) -> Result<()> {
118+
// Only copy SQLite database and settings files
119+
let essential_files = ["data.sqlite3", "settings.json"];
120+
121+
for file_name in essential_files {
122+
let src_path = src.join(file_name);
123+
let dst_path = dst.join(file_name);
124+
125+
if src_path.exists() {
126+
debug!("Copying {} to {}", src_path.display(), dst_path.display());
127+
fs::copy(&src_path, &dst_path).await?;
128+
}
129+
}
130+
131+
Ok(())
132+
}
133+
134+
fn mark_migration_completed() -> Result<()> {
135+
let db = fig_settings::sqlite::database()?;
136+
db.set_state_value(KIRO_MIGRATION_KEY, true)?;
137+
Ok(())
138+
}
139+
140+
struct MigrationLock {
141+
_file: std::fs::File,
142+
path: PathBuf,
143+
}
144+
145+
impl Drop for MigrationLock {
146+
fn drop(&mut self) {
147+
let _ = std::fs::remove_file(&self.path);
148+
}
149+
}
150+
151+
fn acquire_migration_lock() -> Result<Option<MigrationLock>> {
152+
let lock_path = migration_lock_path()?;
153+
154+
if let Some(parent) = lock_path.parent() {
155+
std::fs::create_dir_all(parent)?;
156+
}
157+
158+
let file = std::fs::OpenOptions::new()
159+
.create(true)
160+
.write(true)
161+
.truncate(false)
162+
.open(&lock_path)?;
163+
164+
match flock(&file, FlockOperation::NonBlockingLockExclusive) {
165+
Ok(()) => Ok(Some(MigrationLock {
166+
_file: file,
167+
path: lock_path,
168+
})),
169+
Err(_) => {
170+
if let Ok(metadata) = std::fs::metadata(&lock_path) {
171+
if let Ok(modified) = metadata.modified() {
172+
if let Ok(elapsed) = modified.elapsed() {
173+
if elapsed.as_secs() > 10 {
174+
std::fs::remove_file(&lock_path)?;
175+
let file = std::fs::OpenOptions::new()
176+
.create(true)
177+
.write(true)
178+
.truncate(false)
179+
.open(&lock_path)?;
180+
return match flock(&file, FlockOperation::NonBlockingLockExclusive) {
181+
Ok(()) => Ok(Some(MigrationLock {
182+
_file: file,
183+
path: lock_path,
184+
})),
185+
Err(_) => Ok(None),
186+
};
187+
}
188+
}
189+
}
190+
}
191+
Ok(None)
192+
},
193+
}
194+
}
195+
196+
fn migration_lock_path() -> Result<PathBuf> {
197+
Ok(fig_util::directories::fig_data_dir()?.join("migration.lock"))
198+
}
199+
200+
fn is_migration_completed() -> Result<bool> {
201+
Ok(fig_settings::state::get_bool_or(KIRO_MIGRATION_KEY, false))
202+
}

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)