Skip to content

Commit 897b937

Browse files
committed
Show restored Skill progress in CLI
1 parent 457fa52 commit 897b937

7 files changed

Lines changed: 139 additions & 35 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cc-switchy"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
edition = "2021"
55
rust-version = "1.95"
66

src/commands/sync.rs

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,19 @@ pub struct SyncService {
4040
pub cancellation: CancellationToken,
4141
}
4242

43+
struct SyncRunDetails {
44+
outcome: SyncOutcome,
45+
restored_skills: usize,
46+
}
47+
4348
impl SyncService {
4449
pub async fn run(&mut self, request: SyncRequest) -> Result<SyncOutcome, AppError> {
50+
self.run_with_details(request)
51+
.await
52+
.map(|details| details.outcome)
53+
}
54+
55+
async fn run_with_details(&mut self, request: SyncRequest) -> Result<SyncRunDetails, AppError> {
4556
let started = Instant::now();
4657
let result = self.run_inner(request, started).await;
4758
if let Err(error) = &result {
@@ -64,7 +75,7 @@ impl SyncService {
6475
&mut self,
6576
request: SyncRequest,
6677
started: Instant,
67-
) -> Result<SyncOutcome, AppError> {
78+
) -> Result<SyncRunDetails, AppError> {
6879
let source = self
6980
.catalog
7081
.resolve(request.source_name.as_deref())?
@@ -94,7 +105,9 @@ impl SyncService {
94105
Arc::clone(&self.progress),
95106
backup_config,
96107
);
97-
let restored = restore.apply(snapshot, &lock, &source_name)?;
108+
let restored = restore.apply_with_details(snapshot, &lock, &source_name)?;
109+
let restored_skills = restored.restored_skills;
110+
let restored = restored.outcome;
98111

99112
let settings_path = self.paths.cc_switch_dir.join("settings.json");
100113
let mut settings = DeviceSettings::load(&settings_path)?;
@@ -131,12 +144,15 @@ impl SyncService {
131144
snapshot_id: snapshot_id.clone(),
132145
});
133146

134-
Ok(SyncOutcome {
135-
source_name,
136-
snapshot_id,
137-
backup_dir: restored.backup_dir,
138-
projection,
139-
duration,
147+
Ok(SyncRunDetails {
148+
outcome: SyncOutcome {
149+
source_name,
150+
snapshot_id,
151+
backup_dir: restored.backup_dir,
152+
projection,
153+
duration,
154+
},
155+
restored_skills,
140156
})
141157
}
142158
}
@@ -158,9 +174,14 @@ pub async fn run_cli(
158174
progress,
159175
cancellation,
160176
};
161-
let outcome = service.run(SyncRequest { source_name }).await?;
162-
println!("{}", render_outcome(translator, &outcome));
163-
Ok(outcome)
177+
let details = service
178+
.run_with_details(SyncRequest { source_name })
179+
.await?;
180+
println!(
181+
"{}",
182+
render_outcome(translator, &details.outcome, details.restored_skills)
183+
);
184+
Ok(details.outcome)
164185
}
165186

166187
pub struct CliProgress {
@@ -293,39 +314,53 @@ impl ProgressSink for CliProgress {
293314
return;
294315
};
295316
if self.tty {
296-
let terminal = matches!(
297-
event,
298-
ProgressEvent::Completed { .. }
299-
| ProgressEvent::Failed { .. }
300-
| ProgressEvent::Warning { .. }
301-
);
302-
if terminal {
303-
eprintln!("\r\x1b[2K{line}");
304-
} else {
317+
if rewrites_current_line(&event) {
305318
eprint!("\r\x1b[2K{line}");
306319
let _ = std::io::stderr().flush();
320+
} else {
321+
eprintln!("\r\x1b[2K{line}");
307322
}
308323
} else {
309324
eprintln!("{line}");
310325
}
311326
}
312327

328+
fn emit_restored_skills(&self, total: usize) {
329+
let line = format!(
330+
"{}: {total}",
331+
Translator::new(self.language)
332+
.text(MessageKey::ProgressRestoringSkills, &MessageArgs::default(),)
333+
);
334+
if self.tty {
335+
eprintln!("\r\x1b[2K{line}");
336+
} else {
337+
eprintln!("{line}");
338+
}
339+
}
340+
313341
fn emit_skill(&self, agent: String, skill: String, completed: usize, total: usize) {
314342
let line = format!(
315343
"{}: {agent} · {skill} {completed}/{total}",
316344
Translator::new(self.language)
317345
.text(MessageKey::ProgressApplyingSkills, &MessageArgs::default())
318346
);
319347
if self.tty {
320-
eprint!("\r\x1b[2K{line}");
321-
let _ = std::io::stderr().flush();
348+
eprintln!("\r\x1b[2K{line}");
322349
} else {
323350
eprintln!("{line}");
324351
}
325352
}
326353
}
327354

328-
fn render_outcome(translator: &Translator, outcome: &SyncOutcome) -> String {
355+
fn rewrites_current_line(event: &ProgressEvent) -> bool {
356+
matches!(event, ProgressEvent::Downloading { .. })
357+
}
358+
359+
fn render_outcome(
360+
translator: &Translator,
361+
outcome: &SyncOutcome,
362+
restored_skills: usize,
363+
) -> String {
329364
let mut args = MessageArgs::default();
330365
args.0.insert("source", outcome.source_name.clone());
331366
args.0.insert("snapshot", outcome.snapshot_id.clone());
@@ -337,6 +372,7 @@ fn render_outcome(translator: &Translator, outcome: &SyncOutcome) -> String {
337372
"applied",
338373
outcome.projection.applied_agents.len().to_string(),
339374
);
375+
args.0.insert("skills", restored_skills.to_string());
340376
args.0
341377
.insert("warnings", outcome.projection.warnings.len().to_string());
342378
let backup = outcome.backup_dir.as_ref().map_or_else(
@@ -425,3 +461,24 @@ fn set_private_file(path: &Path) -> Result<(), AppError> {
425461
fn set_private_file(_path: &Path) -> Result<(), AppError> {
426462
Ok(())
427463
}
464+
465+
#[cfg(test)]
466+
mod tests {
467+
use super::rewrites_current_line;
468+
use crate::progress::ProgressEvent;
469+
470+
#[test]
471+
fn tty_rewrites_only_download_progress() {
472+
assert!(rewrites_current_line(&ProgressEvent::Downloading {
473+
artifact: "db.sql".to_string(),
474+
downloaded: 1,
475+
total: 2,
476+
}));
477+
assert!(!rewrites_current_line(&ProgressEvent::RestoringSkills));
478+
assert!(!rewrites_current_line(&ProgressEvent::ApplyingSkills {
479+
agent: "Codex".to_string(),
480+
completed: 1,
481+
total: 1,
482+
}));
483+
}
484+
}

src/i18n.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ impl Translator {
227227
let snapshot = || message_arg(_args, "snapshot");
228228
let duration = || message_arg(_args, "duration");
229229
let applied = || message_arg(_args, "applied");
230+
let skills = || message_arg(_args, "skills");
230231
let warnings = || message_arg(_args, "warnings");
231232
let backup = || message_arg(_args, "backup");
232233
let agent = || message_arg(_args, "agent");
@@ -291,10 +292,11 @@ impl Translator {
291292
(Language::ZhCn, MessageKey::ProgressCompleted) => "同步完成",
292293
(Language::ZhCn, MessageKey::SyncSummary) => {
293294
return format!(
294-
"同步成功\n来源:{}\n快照:{}\n耗时:{}\n应用步骤:{}\n警告:{}\n备份:{}",
295+
"同步成功\n来源:{}\n快照:{}\n耗时:{}\n恢复 Skills:{}\n应用步骤:{}\n警告:{}\n备份:{}",
295296
source(),
296297
snapshot(),
297298
duration(),
299+
skills(),
298300
applied(),
299301
warnings(),
300302
backup()
@@ -583,10 +585,11 @@ impl Translator {
583585
(Language::Auto | Language::EnUs, MessageKey::ProgressCompleted) => "Sync completed",
584586
(Language::Auto | Language::EnUs, MessageKey::SyncSummary) => {
585587
return format!(
586-
"Sync succeeded\nSource: {}\nSnapshot: {}\nDuration: {}\nApplied steps: {}\nWarnings: {}\nBackup: {}",
588+
"Sync succeeded\nSource: {}\nSnapshot: {}\nDuration: {}\nRestored Skills: {}\nApplied steps: {}\nWarnings: {}\nBackup: {}",
587589
source(),
588590
snapshot(),
589591
duration(),
592+
skills(),
590593
applied(),
591594
warnings(),
592595
backup()

src/progress.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ pub enum ProgressEvent {
5858
pub trait ProgressSink: Send + Sync {
5959
fn emit(&self, event: ProgressEvent);
6060

61+
fn emit_restored_skills(&self, _total: usize) {
62+
self.emit(ProgressEvent::RestoringSkills);
63+
}
64+
6165
fn emit_skill(&self, agent: String, _skill: String, completed: usize, total: usize) {
6266
self.emit(ProgressEvent::ApplyingSkills {
6367
agent,

src/restore/service.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ pub struct RestoreOutcome {
5858
pub skills_path: PathBuf,
5959
}
6060

61+
pub(crate) struct RestoreDetails {
62+
pub outcome: RestoreOutcome,
63+
pub restored_skills: usize,
64+
}
65+
6166
pub struct RestoreService {
6267
paths: AppPaths,
6368
progress: Arc<dyn ProgressSink>,
@@ -80,9 +85,19 @@ impl RestoreService {
8085
pub fn apply(
8186
&self,
8287
snapshot: DownloadedSnapshot,
83-
_lock: &SyncLockGuard,
88+
lock: &SyncLockGuard,
8489
source: &str,
8590
) -> Result<RestoreOutcome, AppError> {
91+
self.apply_with_details(snapshot, lock, source)
92+
.map(|details| details.outcome)
93+
}
94+
95+
pub(crate) fn apply_with_details(
96+
&self,
97+
snapshot: DownloadedSnapshot,
98+
_lock: &SyncLockGuard,
99+
source: &str,
100+
) -> Result<RestoreDetails, AppError> {
86101
let result = self.apply_inner(&snapshot, source);
87102
cleanup_downloaded_files(&snapshot);
88103
result
@@ -92,9 +107,10 @@ impl RestoreService {
92107
&self,
93108
snapshot: &DownloadedSnapshot,
94109
source: &str,
95-
) -> Result<RestoreOutcome, AppError> {
110+
) -> Result<RestoreDetails, AppError> {
96111
let skills_path = resolve_skills_path(&self.paths)?;
97112
let prepared_skills = prepare_skills(&snapshot.skills_zip_path)?;
113+
let restored_skills = count_skill_directories(prepared_skills.extracted_dir.path())?;
98114
let database_path = self.paths.cc_switch_dir.join("cc-switch.db");
99115
let prepared_database = prepare_database(
100116
&snapshot.db_sql_path,
@@ -115,7 +131,7 @@ impl RestoreService {
115131
None
116132
};
117133

118-
self.progress.emit(ProgressEvent::RestoringSkills);
134+
self.progress.emit_restored_skills(restored_skills);
119135
if let Err(restore_error) =
120136
install_prepared_skills(prepared_skills.extracted_dir.path(), &skills_path)
121137
{
@@ -151,14 +167,34 @@ impl RestoreService {
151167
)));
152168
}
153169

154-
Ok(RestoreOutcome {
155-
backup_dir: backup.map(|backup| backup.backup_dir),
156-
database_path,
157-
skills_path,
170+
Ok(RestoreDetails {
171+
outcome: RestoreOutcome {
172+
backup_dir: backup.map(|backup| backup.backup_dir),
173+
database_path,
174+
skills_path,
175+
},
176+
restored_skills,
158177
})
159178
}
160179
}
161180

181+
fn count_skill_directories(root: &Path) -> Result<usize, AppError> {
182+
let mut count = 0;
183+
for entry in fs::read_dir(root).map_err(|error| AppError::io(root, error))? {
184+
let entry = entry.map_err(|error| AppError::io(root, error))?;
185+
let path = entry.path();
186+
if entry
187+
.file_type()
188+
.map_err(|error| AppError::io(&path, error))?
189+
.is_dir()
190+
&& path.join("SKILL.md").is_file()
191+
{
192+
count += 1;
193+
}
194+
}
195+
Ok(count)
196+
}
197+
162198
fn rollback_unavailable(error: AppError) -> AppError {
163199
AppError::Restore(format!(
164200
"{error}; rollback unavailable because backups are disabled"

tests/sync_end_to_end.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,14 @@ fn redirected_cli_prints_stage_lines_summary_and_exit_codes() {
398398
.code(2)
399399
.stdout(predicate::str::contains("Sync succeeded"))
400400
.stdout(predicate::str::contains("Source: home"))
401+
.stdout(predicate::str::contains("Restored Skills: 1"))
401402
.stderr(predicate::str::contains("Acquiring the sync lock"))
402403
.stderr(predicate::str::contains("Downloading db.sql"))
404+
.stderr(predicate::str::contains("Restoring Skills: 1"))
403405
.stderr(predicate::str::contains("Applying providers"))
404-
.stderr(predicate::str::contains("Codex · Demo"));
406+
.stderr(predicate::str::contains(
407+
"Applying Skills: Codex · Demo 1/1",
408+
));
405409
manifest.assert_calls(1);
406410
database.assert_calls(1);
407411
skills.assert_calls(1);

0 commit comments

Comments
 (0)