Skip to content

Commit 274ed15

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

File tree

10 files changed

+284
-39
lines changed

10 files changed

+284
-39
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: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use std::path::PathBuf;
2+
3+
use eyre::Result;
4+
use fig_os_shim::Context;
5+
use rustix::fs::{
6+
FlockOperation,
7+
flock,
8+
};
9+
use serde_json::Value;
10+
use tokio::fs;
11+
use tracing::debug;
12+
13+
const KIRO_MIGRATION_KEY: &str = "migration.kiro.completed";
14+
15+
pub async fn migrate_if_needed() -> Result<bool> {
16+
let status = detect_migration().await?;
17+
18+
match status {
19+
MigrationStatus::Completed => {
20+
debug!("Migration already completed");
21+
return Ok(false);
22+
},
23+
MigrationStatus::NotNeeded => {
24+
debug!("No migration needed");
25+
return Ok(false);
26+
},
27+
MigrationStatus::Needed => {
28+
debug!("Migrating database and settings");
29+
},
30+
}
31+
32+
let _lock = match acquire_migration_lock()? {
33+
Some(lock) => lock,
34+
None => {
35+
debug!("Migration already in progress");
36+
return Ok(false);
37+
},
38+
};
39+
40+
let old_dir = fig_util::directories::old_fig_data_dir()?;
41+
let new_dir = fig_util::directories::fig_data_dir()?;
42+
43+
debug!("Old directory: {}", old_dir.display());
44+
debug!("New directory: {}", new_dir.display());
45+
debug!("Old directory exists: {}", old_dir.exists());
46+
debug!("Old directory is_symlink: {}", old_dir.is_symlink());
47+
debug!("New directory exists: {}", new_dir.exists());
48+
49+
// Move old data directory to new location if needed
50+
if old_dir.exists() && !old_dir.is_symlink() {
51+
if !new_dir.exists() {
52+
debug!("Renaming old directory to new directory");
53+
fs::rename(&old_dir, &new_dir).await?;
54+
debug!("Temporary: Creating symlink from old to new");
55+
symlink(&new_dir, &old_dir).await?;
56+
} else {
57+
debug!("New directory already exists, copying contents");
58+
copy_dir_all(&old_dir, &new_dir).await?;
59+
debug!("Removing old directory");
60+
fs::remove_dir_all(&old_dir).await?;
61+
debug!("Temporary: Creating symlink from old to new");
62+
symlink(&new_dir, &old_dir).await?;
63+
}
64+
} else {
65+
debug!("Skipping directory move - old directory doesn't exist or is already a symlink");
66+
}
67+
68+
// Migrate settings to new location
69+
debug!("Migrating settings");
70+
migrate_settings().await?;
71+
72+
// Mark migration as completed in database
73+
debug!("Marking migration as completed");
74+
mark_migration_completed()?;
75+
76+
debug!("Migration completed successfully");
77+
Ok(true)
78+
}
79+
80+
#[derive(Debug)]
81+
enum MigrationStatus {
82+
NotNeeded,
83+
Needed,
84+
Completed,
85+
}
86+
87+
async fn detect_migration() -> Result<MigrationStatus> {
88+
let old_dir = fig_util::directories::old_fig_data_dir()?;
89+
let new_dir = fig_util::directories::fig_data_dir()?;
90+
91+
// If new directory doesn't exist yet, check if old directory exists
92+
if !new_dir.exists() {
93+
if old_dir.exists() && old_dir.is_dir() {
94+
return Ok(MigrationStatus::Needed);
95+
} else {
96+
return Ok(MigrationStatus::NotNeeded);
97+
}
98+
}
99+
100+
// New directory exists, check database flag (safe now since new_dir exists)
101+
let migration_completed = is_migration_completed()?;
102+
103+
if migration_completed {
104+
Ok(MigrationStatus::Completed)
105+
} else if old_dir.exists() && old_dir.is_dir() {
106+
Ok(MigrationStatus::Needed)
107+
} else {
108+
Ok(MigrationStatus::NotNeeded)
109+
}
110+
}
111+
112+
async fn migrate_settings() -> Result<()> {
113+
let old_settings = fig_util::directories::fig_data_dir()?.join("settings.json");
114+
let new_settings = fig_util::directories::settings_path()?;
115+
116+
if !old_settings.exists() || new_settings.exists() {
117+
return Ok(());
118+
}
119+
120+
let content = fs::read_to_string(&old_settings).await?;
121+
let mut settings: serde_json::Map<String, Value> = serde_json::from_str(&content)?;
122+
123+
// Transform api.q.service → api.kiro.service
124+
if let Some(q_service) = settings.remove("api.q.service") {
125+
settings.insert("api.kiro.service".to_string(), q_service);
126+
}
127+
128+
if let Some(parent) = new_settings.parent() {
129+
fs::create_dir_all(parent).await?;
130+
}
131+
132+
let json = serde_json::to_string_pretty(&settings)?;
133+
fs::write(new_settings, json).await?;
134+
135+
Ok(())
136+
}
137+
138+
async fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> Result<()> {
139+
let mut entries = fs::read_dir(src).await?;
140+
141+
while let Some(entry) = entries.next_entry().await? {
142+
let src_path = entry.path();
143+
let dst_path = dst.join(entry.file_name());
144+
145+
if entry.file_type().await?.is_file() {
146+
debug!("Copying {} to {}", src_path.display(), dst_path.display());
147+
fs::copy(&src_path, &dst_path).await?;
148+
}
149+
}
150+
151+
Ok(())
152+
}
153+
154+
async fn symlink(src: &std::path::Path, dst: &std::path::Path) -> Result<()> {
155+
use std::io::ErrorKind;
156+
157+
// Check if the link already exists
158+
match fs::symlink_metadata(dst).await {
159+
Ok(metadata) => {
160+
// If it's a symlink, check if it points to the right place
161+
if metadata.file_type().is_symlink() {
162+
if let Ok(read_link) = fs::read_link(dst).await {
163+
if read_link == src {
164+
return Ok(());
165+
}
166+
}
167+
}
168+
169+
// If it's not a symlink or it points to the wrong place, delete it
170+
fs::remove_file(dst).await?;
171+
},
172+
Err(err) if err.kind() == ErrorKind::NotFound => {},
173+
Err(err) => return Err(err.into()),
174+
}
175+
176+
// Create the symlink using fig_os_shim
177+
let ctx = Context::new();
178+
ctx.fs().symlink(src, dst).await?;
179+
180+
Ok(())
181+
}
182+
183+
fn mark_migration_completed() -> Result<()> {
184+
let db = fig_settings::sqlite::database()?;
185+
db.set_state_value(KIRO_MIGRATION_KEY, true)?;
186+
Ok(())
187+
}
188+
189+
struct MigrationLock {
190+
_file: std::fs::File,
191+
path: PathBuf,
192+
}
193+
194+
impl Drop for MigrationLock {
195+
fn drop(&mut self) {
196+
let _ = std::fs::remove_file(&self.path);
197+
}
198+
}
199+
200+
fn acquire_migration_lock() -> Result<Option<MigrationLock>> {
201+
let lock_path = migration_lock_path()?;
202+
203+
if let Some(parent) = lock_path.parent() {
204+
std::fs::create_dir_all(parent)?;
205+
}
206+
207+
let file = std::fs::OpenOptions::new()
208+
.create(true)
209+
.write(true)
210+
.truncate(false)
211+
.open(&lock_path)?;
212+
213+
match flock(&file, FlockOperation::NonBlockingLockExclusive) {
214+
Ok(()) => Ok(Some(MigrationLock {
215+
_file: file,
216+
path: lock_path,
217+
})),
218+
Err(_) => {
219+
if let Ok(metadata) = std::fs::metadata(&lock_path) {
220+
if let Ok(modified) = metadata.modified() {
221+
if let Ok(elapsed) = modified.elapsed() {
222+
if elapsed.as_secs() > 60 {
223+
std::fs::remove_file(&lock_path)?;
224+
let file = std::fs::OpenOptions::new()
225+
.create(true)
226+
.write(true)
227+
.truncate(false)
228+
.open(&lock_path)?;
229+
return match flock(&file, FlockOperation::NonBlockingLockExclusive) {
230+
Ok(()) => Ok(Some(MigrationLock {
231+
_file: file,
232+
path: lock_path,
233+
})),
234+
Err(_) => Ok(None),
235+
};
236+
}
237+
}
238+
}
239+
}
240+
Ok(None)
241+
},
242+
}
243+
}
244+
245+
fn migration_lock_path() -> Result<PathBuf> {
246+
Ok(fig_util::directories::fig_data_dir()?.join("migration.lock"))
247+
}
248+
249+
fn is_migration_completed() -> Result<bool> {
250+
let db = fig_settings::sqlite::database()?;
251+
Ok(db
252+
.get_state_value(KIRO_MIGRATION_KEY)?
253+
.and_then(|v| v.as_bool())
254+
.unwrap_or(false))
255+
}

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/{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: 5 additions & 2 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
@@ -450,7 +453,7 @@ pub fn bundle_metadata_path<Ctx: EnvProvider + PlatformProvider>(ctx: &Ctx) -> R
450453
/// - MacOS: `$HOME/.aws/kiro-cli/settings.json`
451454
/// - Windows: `$HOME/.aws/kiro-cli/settings.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

0 commit comments

Comments
 (0)