Skip to content

Commit d3c0868

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

File tree

10 files changed

+224
-48
lines changed

10 files changed

+224
-48
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: 14 additions & 11 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
@@ -739,22 +742,22 @@ mod tests {
739742
fn snapshot_figterm_socket_path() {
740743
linux!(figterm_socket_path("$SESSION_ID"), @"$XDG_RUNTIME_DIR/cwrun/t/$SESSION_ID.sock");
741744
macos!(figterm_socket_path("$SESSION_ID"), @"$TMPDIR/cwrun/t/$SESSION_ID.sock");
742-
windows!(figterm_socket_path("$SESSION_ID"), @r"C:\Users\$USER\AppData\Local\Temp\AmazonQ\sockets\t\$SESSION_ID.sock");
745+
windows!(figterm_socket_path("$SESSION_ID"), @r"C:\Users\$USER\AppData\Local\Temp\KiroCli\sockets\t\$SESSION_ID.sock");
743746
}
744747

745748
#[test]
746749
fn snapshot_settings_path() {
747-
linux!(settings_path(), @"$HOME/.local/share/amazon-q/settings.json");
748-
macos!(settings_path(), @"$HOME/.aws/kiro-cli/settings.json");
749-
windows!(settings_path(), @r"C:\Users\$USER\AppData\Local\AmazonQ\settings.json");
750+
linux!(settings_path(), @"$HOME/.local/share/.kiro/settings/cli.json");
751+
macos!(settings_path(), @"$HOME/.kiro/settings/cli.json");
752+
windows!(settings_path(), @r"C:\Users\$USER\AppData\Local\.kiro\settings\cli.json");
750753
}
751754

752755
#[test]
753756
fn snapshot_update_lock_path() {
754757
let ctx = Context::new();
755-
linux!(update_lock_path(&ctx), @"$HOME/.local/share/amazon-q/update.lock");
758+
linux!(update_lock_path(&ctx), @"$HOME/.local/share/kiro-cli/update.lock");
756759
macos!(update_lock_path(&ctx), @"$HOME/Library/Application Support/kiro-cli/update.lock");
757-
windows!(update_lock_path(&ctx), @r"C:\Users\$USER\AppData\Local\AmazonQ\update.lock");
760+
windows!(update_lock_path(&ctx), @r"C:\Users\$USER\AppData\Local\KiroCli\update.lock");
758761
}
759762

760763
#[test]

crates/q_cli/src/cli/mod.rs

Lines changed: 12 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,14 @@ pub struct Cli {
303302

304303
impl Cli {
305304
pub async fn execute(self) -> Result<ExitCode> {
305+
let is_hidden_command = matches!(
306+
self.subcommand,
307+
Some(CliRootCommands::Internal(_) | CliRootCommands::Hook(_))
308+
);
309+
306310
// 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-
}
311+
if self.show_legacy_warning && !is_hidden_command {
312+
eprintln!("\x1b[33m{}\x1b[0m", LEGACY_WARNING);
312313
}
313314

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

344345
// 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");
346+
// Only run for non-hidden commands
347+
if !is_hidden_command {
348+
if let Err(err) = fig_install::migrate::migrate_if_needed().await {
349+
debug!(%err, "Failed to migrate");
350+
}
355351
}
356352

357353
self.send_telemetry().await;

0 commit comments

Comments
 (0)