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
+7
View File
@@ -57,6 +57,13 @@ App-level commands (typed inside the app, available in both modes):
load Open the project picker.
rebuild Rebuild playground.db from project.yaml
+ data/, with confirmation.
export [<path>] Write a zip of project.yaml + data/ to
<path> (relative paths under the data
root). Excludes playground.db and
history.log.
import <zip> [as <t>] Unpack <zip> into a new project and
switch to it. <t> overrides the target
name (else taken from the zip).
";
#[derive(Debug, thiserror::Error)]