Iteration 5: export / import commands

Implements the `export` and `import` app-level commands per
ADR-0015 §11 + ADR-0007 amendment 1.

- `export [<path>]` writes a zip of project.yaml + data/ to
  <data-root>/YYYYMMDD-<projectname>-export-NN.zip by default,
  preserving the project's directory name as the single
  top-level folder inside the archive.
- `import <zip> [as <target>]` extracts an exported zip into
  a new named project and switches to it. Target name is
  derived from the zip's top-level folder by default; on
  collision the destination auto-suffixes -02, -03, ... up
  to -99 instead of refusing (deviates from §2's refuse-on-
  collision rule for save/save as; recorded as an amendment
  to ADR-0015 §11).
- Excludes playground.db and history.log from the zip.
- Path-traversal protection via zip::enclosed_name + post-
  resolution check that the extraction path stays inside
  the target directory.

Adds the zip = "5" dep with default-features = false +
features = ["deflate"] to keep the binary-size cost modest.

Test baseline: 370 passing, 0 failing, 0 skipped.
This commit is contained in:
claude@clouddev1
2026-05-08 08:24:45 +00:00
parent ca71184678
commit c6cf3df6dc
11 changed files with 1419 additions and 15 deletions
+13 -3
View File
@@ -77,9 +77,9 @@ pub enum AppEvent {
LoadPickerReady {
entries: Vec<crate::app::LoadPickerEntry>,
},
/// A project switch (load / new / save-as) succeeded.
/// Carries the new display name + temp flag so App can
/// update the status bar.
/// A project switch (load / new / save-as / import)
/// succeeded. Carries the new display name + temp flag
/// so App can update the status bar.
ProjectSwitched {
display_name: String,
is_temp: bool,
@@ -90,4 +90,14 @@ pub enum AppEvent {
ProjectSwitchFailed {
error: String,
},
/// Export wrote a zip successfully. Carries the resolved
/// final path so the user gets a "wrote to: …" note.
ExportSucceeded {
path: std::path::PathBuf,
},
/// Export failed in a non-fatal way (target exists, IO
/// error, sequence range exhausted, …).
ExportFailed {
error: String,
},
}