oxicode/cli/commands/
migrate.rs1use std::path::PathBuf;
27
28use crate::cli::{MigrateBrainArgs, MigrateHomeArgs, MigrationCommands};
29
30pub async fn handle_migrate(cmd: MigrationCommands) -> i32 {
32 match cmd {
33 MigrationCommands::Brain(args) => handle_migrate_brain(args).await,
34 MigrationCommands::Home(args) => handle_migrate_home(args),
35 }
36}
37
38fn handle_migrate_home(args: MigrateHomeArgs) -> i32 {
43 use crate::home_migrate::{MigrationState, RunOutcome};
44 use oxicode_catalog::oxi_home;
45
46 println!("Oxi home layout migration");
47 println!("-------------------------");
48 println!("dry-run: {}", args.dry_run);
49
50 let Some(source) = oxi_home::legacy_home_dir() else {
51 println!("source: <no legacy home>");
52 if std::env::var_os("OXICODE_HOME").is_some() {
53 println!(
54 "required action: nothing to do (explicit OXICODE_HOME never merges legacy)"
55 );
56 } else {
57 println!("required action: nothing to do");
58 }
59 return 0;
60 };
61 let Some(destination) = oxi_home::oxicode_home() else {
62 eprintln!("error: cannot resolve the canonical oxicode home");
63 return 1;
64 };
65 let Some(journal_path) = oxi_home::migration_journal_path() else {
66 eprintln!("error: cannot resolve the oxi home for the migration journal");
67 return 1;
68 };
69
70 let plan = match crate::home_migrate::preflight(&source, &destination) {
72 Ok(plan) => plan,
73 Err(e) => {
74 eprintln!("error: preflight failed: {e}");
75 return 1;
76 }
77 };
78 println!("source: {} (legacy)", source.display());
79 println!("destination: {}", destination.display());
80 println!("journal: {}", journal_path.display());
81 println!("files: {}", plan.file_count);
82 println!("bytes: {}", plan.total_bytes);
83
84 let outcome = match crate::home_migrate::run(&source, &destination, &journal_path, args.dry_run)
85 {
86 Ok(outcome) => outcome,
87 Err(e) => {
88 eprintln!("error: {e}");
89 return 1;
90 }
91 };
92
93 match outcome {
94 RunOutcome::DryRun(plan) => match plan.state {
95 MigrationState::NothingToDo => {
96 println!("state: nothing to do");
97 println!("required action: none");
98 }
99 MigrationState::Ready => {
100 println!(
101 "state: ready ({} file(s) to copy)",
102 plan.pending.len()
103 );
104 println!("required action: rerun without --dry-run to migrate");
105 }
106 MigrationState::AlreadyMigrated => {
107 println!("state: already migrated (destination identical)");
108 println!("required action: none");
109 }
110 MigrationState::Conflict { conflicts } => {
111 println!("state: conflict");
112 for (src, dst) in &conflicts {
113 println!(" differs:");
114 println!(" source: {}", src.display());
115 println!(" destination: {}", dst.display());
116 }
117 println!(
118 "required action: resolve the differing files manually (nothing was touched)"
119 );
120 }
121 },
122 RunOutcome::NothingToDo => {
123 println!("state: nothing to do");
124 println!("required action: none");
125 }
126 RunOutcome::Conflict { conflicts } => {
127 println!("state: conflict (nothing was touched)");
128 for (src, dst) in &conflicts {
129 println!(" differs:");
130 println!(" source: {}", src.display());
131 println!(" destination: {}", dst.display());
132 }
133 println!("required action: resolve the differing files manually");
134 return 1;
135 }
136 RunOutcome::AlreadyMigrated { completed_journal } => {
137 println!("state: already migrated (destination identical)");
138 if completed_journal {
139 println!("journal: stale in_progress entry marked complete");
140 }
141 println!("required action: none");
142 }
143 RunOutcome::Copied { copied, skipped } => {
144 println!("copied: {copied} file(s)");
145 println!("skipped (identical): {skipped} file(s)");
146 println!("required action: none — legacy home left intact");
147 }
148 }
149
150 0
151}
152
153async fn handle_migrate_brain(args: MigrateBrainArgs) -> i32 {
154 let socket = args
155 .socket
156 .unwrap_or_else(crate::foundation::brain::default_socket_path);
157
158 println!("Foundation v1 brain migration");
159 println!("-----------------------------");
160 println!("socket: {}", socket.display());
161 println!("dry-run: {}", args.dry_run);
162 println!("archive-legacy: {}", args.archive_legacy);
163 println!("checkpoint: {}", args.checkpoint.display());
164 println!("batch size: {}", args.batch_size);
165
166 if args.dry_run {
167 println!("\nDRY RUN: no memory will be written, no checkpoint advanced.");
168 println!("The legacy store will be enumerated but not read for content.");
169 }
170
171 let backend = crate::foundation::brain::BrainMemoryBackend::new(socket.clone());
172 let probe = backend.ping().await;
175 println!(
176 "\nbackend health: {}",
177 match &probe {
178 Ok(()) => "ok: oxibrain daemon connected".to_string(),
179 Err(e) => format!("degraded ({e})"),
180 }
181 );
182
183 if args.dry_run {
184 println!("\nDry run complete. Re-run without --dry-run to perform the migration.");
185 return 0;
186 }
187
188 let checkpoint = crate::foundation::migrate::Checkpoint::load(&args.checkpoint);
189 if let Some(last) = checkpoint.last_id() {
190 println!("resuming after id: {last}");
191 } else {
192 println!("starting fresh (no checkpoint found)");
193 }
194
195 let legacy = crate::foundation::migrate::LegacyMemoryReader::for_default_home();
196 let mut migrated = 0usize;
197 let mut failed = 0usize;
198 let mut tx = crate::foundation::migrate::Migration::new(&backend, &args.checkpoint);
199
200 for batch in legacy.batches(args.batch_size) {
201 for item in batch {
202 match tx.migrate_one(item) {
203 Ok(crate::foundation::migrate::MigrationOutcome::Inserted(id)) => {
204 println!(" + {id}");
205 migrated += 1;
206 }
207 Ok(crate::foundation::migrate::MigrationOutcome::Skipped(id)) => {
208 println!(" ~ {id} (already in brain)");
209 }
210 Err(e) => {
211 println!(" ! {e}");
212 failed += 1;
213 }
214 }
215 }
216 }
217
218 println!("\nMigration summary");
219 println!(" inserted: {migrated}");
220 println!(" failed: {failed}");
221
222 if args.archive_legacy {
223 match crate::foundation::migrate::archive_legacy_default() {
224 Ok(archive_path) => {
225 println!("\nLegacy store archived to: {}", archive_path.display());
226 }
227 Err(e) => {
228 println!("\nArchive failed: {e}");
229 return 2;
230 }
231 }
232 }
233
234 if failed > 0 { 1 } else { 0 }
235}
236
237impl Default for MigrateBrainArgs {
238 fn default() -> Self {
239 Self {
240 socket: None,
241 dry_run: false,
242 archive_legacy: false,
243 checkpoint: PathBuf::from("~/.oxicode/migration/brain.json"),
244 batch_size: 64,
245 }
246 }
247}