contextzip 0.3.6__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {contextzip-0.3.6 → contextzip-0.3.8}/PKG-INFO +87 -62
- {contextzip-0.3.6 → contextzip-0.3.8}/README.md +86 -61
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/__init__.py +7 -1
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/api.py +81 -0
- contextzip-0.3.8/contextzip/applier.py +496 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli.py +240 -1
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli_display.py +92 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/config.py +30 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/filters.py +145 -2
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/packager.py +47 -0
- contextzip-0.3.8/contextzip/webui/__init__.py +9 -0
- contextzip-0.3.8/contextzip/webui/assets.py +1012 -0
- contextzip-0.3.8/contextzip/webui/persist.py +59 -0
- contextzip-0.3.8/contextzip/webui/server.py +426 -0
- contextzip-0.3.8/contextzip/webui/suggestions.py +136 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/PKG-INFO +87 -62
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/SOURCES.txt +7 -1
- {contextzip-0.3.6 → contextzip-0.3.8}/pyproject.toml +1 -1
- {contextzip-0.3.6 → contextzip-0.3.8}/LICENSE +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/ai/selector.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli_ai.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/clipboard.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/detector.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/error_parser.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/git.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/project_config.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/base.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/go.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/node.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/python.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/rules/rust.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip/watcher.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,6 +43,8 @@ Every AI session starts the same way: hunt down the relevant files, manually ski
|
|
|
43
43
|
|
|
44
44
|
contextzip eliminates that entirely. Run it from your project root — it detects your stack, applies smart exclusions, produces a lean ZIP, and opens your file manager with the archive already selected. One `Ctrl+C` and you're done.
|
|
45
45
|
|
|
46
|
+
And when the AI tool hands you a ZIP back with the changes, `contextzip apply-zip` closes the loop — no manual unzip-and-hope, no losing track of what actually changed.
|
|
47
|
+
|
|
46
48
|
---
|
|
47
49
|
|
|
48
50
|
## Features
|
|
@@ -51,9 +53,10 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
51
53
|
- **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
|
|
52
54
|
- **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
|
|
53
55
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
56
|
+
- **Apply AI-returned changes back** — `contextzip apply-zip` takes the ZIP an AI tool hands you back and writes it into your project safely: diffed against what was actually sent, backed up before anything is overwritten, and never silently deleting anything
|
|
54
57
|
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
55
|
-
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip
|
|
56
|
-
- **Persistent workspace** — all generated ZIPs land in `.contextzip
|
|
58
|
+
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip.json`
|
|
59
|
+
- **Persistent workspace** — all generated ZIPs land in `.contextzip/`, discoverable, reusable, and git-ignored automatically
|
|
57
60
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
58
61
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
59
62
|
- **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
|
|
@@ -95,7 +98,7 @@ contextzip will:
|
|
|
95
98
|
|
|
96
99
|
1. Detect your framework (e.g. `Next.js + Node.js`)
|
|
97
100
|
2. Apply the appropriate exclusion rules
|
|
98
|
-
3. Create a compressed ZIP in `.contextzip
|
|
101
|
+
3. Create a compressed ZIP in `.contextzip/` at your project root
|
|
99
102
|
4. Open your file manager with the ZIP selected and ready to copy
|
|
100
103
|
|
|
101
104
|
---
|
|
@@ -106,8 +109,6 @@ contextzip will:
|
|
|
106
109
|
contextzip [OPTIONS]
|
|
107
110
|
```
|
|
108
111
|
|
|
109
|
-
> `cz` is a shorthand alias for `contextzip` — both commands are identical and support every option and subcommand shown below (e.g. `cz --git-changes`, `cz exclude node_modules`).
|
|
110
|
-
|
|
111
112
|
| Option | Description |
|
|
112
113
|
|---|---|
|
|
113
114
|
| `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
|
|
@@ -120,7 +121,7 @@ contextzip [OPTIONS]
|
|
|
120
121
|
| `--no-clipboard` | Skip the clipboard / folder-open step |
|
|
121
122
|
| `--no-gitignore` | Ignore the project's `.gitignore` |
|
|
122
123
|
|
|
123
|
-
**Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
|
|
124
|
+
**Subcommands:** `exclude`, `include`, `apply-zip`, `watch`, `config` — run `contextzip --help` for full details.
|
|
124
125
|
|
|
125
126
|
---
|
|
126
127
|
|
|
@@ -147,6 +148,9 @@ contextzip --prompt "Refactor auth middleware" --dry-run
|
|
|
147
148
|
|
|
148
149
|
# Save to a custom path
|
|
149
150
|
contextzip --output ~/Desktop/project-context.zip
|
|
151
|
+
|
|
152
|
+
# Apply the ZIP an AI tool handed back
|
|
153
|
+
contextzip apply-zip
|
|
150
154
|
```
|
|
151
155
|
|
|
152
156
|
---
|
|
@@ -156,7 +160,7 @@ contextzip --output ~/Desktop/project-context.zip
|
|
|
156
160
|
contextzip is also usable as a library. All CLI capabilities are available as plain Python functions — no Click, no Rich output, no `SystemExit`.
|
|
157
161
|
|
|
158
162
|
```python
|
|
159
|
-
from contextzip import get_git_changes, get_files, create_zip
|
|
163
|
+
from contextzip import get_git_changes, get_files, create_zip, apply_zip
|
|
160
164
|
|
|
161
165
|
# Get changed files and use them directly
|
|
162
166
|
collection = get_git_changes()
|
|
@@ -172,6 +176,10 @@ with open(pkg.zip_path, "rb") as f:
|
|
|
172
176
|
collection = get_files(include=["src/"], exclude=["tests/"])
|
|
173
177
|
pkg = create_zip(collection, output="/tmp/upload.zip")
|
|
174
178
|
print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
179
|
+
|
|
180
|
+
# Apply a ZIP an AI tool returned (auto-detects from .contextzip/inbox/)
|
|
181
|
+
result = apply_zip()
|
|
182
|
+
print(f"Wrote {len(result.written)} files, backup at {result.backup_dir}")
|
|
175
183
|
```
|
|
176
184
|
|
|
177
185
|
| Function | Description |
|
|
@@ -179,9 +187,10 @@ print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
|
179
187
|
| `get_git_changes(path?)` | Modified, added, and untracked files from git |
|
|
180
188
|
| `get_files(path?, include?, exclude?)` | All project files after exclusion rules |
|
|
181
189
|
| `create_zip(collection, output?)` | Write a `FileCollection` to a ZIP archive |
|
|
190
|
+
| `apply_zip(zip_path?, project_dir?, manifest?)` | Apply an AI-returned ZIP back into the project |
|
|
182
191
|
| `detect_ecosystem(path?)` | Detect framework and confidence level |
|
|
183
192
|
|
|
184
|
-
All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, etc.) rather than exiting.
|
|
193
|
+
All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, `ZipNotFoundError`, etc.) rather than exiting.
|
|
185
194
|
|
|
186
195
|
---
|
|
187
196
|
|
|
@@ -211,6 +220,61 @@ contextzip config --reset-key # clear and re-run setup
|
|
|
211
220
|
|
|
212
221
|
---
|
|
213
222
|
|
|
223
|
+
## Applying AI-returned changes
|
|
224
|
+
|
|
225
|
+
Once an AI tool has made its edits and handed you back a ZIP, `contextzip apply-zip` writes those changes into your project — safely.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Drop the AI's returned zip into .contextzip/inbox/, then:
|
|
229
|
+
contextzip apply-zip
|
|
230
|
+
|
|
231
|
+
# Or point at it directly, wherever it landed:
|
|
232
|
+
contextzip apply-zip ~/Downloads/fixed-project.zip
|
|
233
|
+
|
|
234
|
+
# Preview first, with full per-file detail:
|
|
235
|
+
contextzip apply-zip --dry-run --verbose
|
|
236
|
+
|
|
237
|
+
# Skip the confirmation prompt (e.g. in a script):
|
|
238
|
+
contextzip apply-zip --yes
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### How it knows what's safe to apply
|
|
242
|
+
|
|
243
|
+
Every ZIP `contextzip` creates gets a small manifest written next to it in `.contextzip/output/` — a hash of each included file, taken at zip-time. This manifest is **never added to the ZIP itself**. It stays local, so it's never uploaded and never visible to whatever AI tool the ZIP is pasted into — nothing for a model, or a curious teammate, to notice or ask about.
|
|
244
|
+
|
|
245
|
+
When you run `apply-zip`, it diffs the returned ZIP against that manifest and classifies every file:
|
|
246
|
+
|
|
247
|
+
| Status | Meaning | Applied automatically? |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| **New** | Wasn't part of the original ZIP | Yes |
|
|
250
|
+
| **Modified** | Was sent, content changed, and your local copy hasn't moved since | Yes |
|
|
251
|
+
| **Unchanged** | Identical to what's already on disk | Skipped — nothing to do |
|
|
252
|
+
| **Drifted** | Your local file changed (or was deleted) since you zipped it | No — flagged, asks first |
|
|
253
|
+
| **Untracked** | In the returned ZIP but has no baseline to compare against | No — flagged, asks first |
|
|
254
|
+
|
|
255
|
+
The common case — you zip some files, the AI edits them, nothing else touched your project meanwhile — applies straight through with just a summary printed, no prompt. Anything that could clobber your own work, or introduce a path you didn't expect, stops and asks before writing.
|
|
256
|
+
|
|
257
|
+
If a ZIP arrives with no matching manifest at all (e.g. it wasn't produced by a `contextzip` round trip), every already-existing path is treated as untracked and you'll be asked to confirm the whole batch.
|
|
258
|
+
|
|
259
|
+
### What it does and doesn't do
|
|
260
|
+
|
|
261
|
+
- **Only adds and modifies files.** Deletions are never inferred from a ZIP's contents — a file that's simply missing from the returned ZIP is left alone.
|
|
262
|
+
- **Backs up before overwriting.** Every file about to be touched is copied into `.contextzip/backups/<timestamp>/` first, preserving its relative path.
|
|
263
|
+
- **Rejects unsafe paths outright.** Any ZIP entry that would resolve outside your project (`../../etc/passwd`-style path traversal) is refused before anything is written — no override flag, no exceptions.
|
|
264
|
+
- **Archives what it consumes.** Once applied, the ZIP is moved to `.contextzip/inbox/applied/<timestamp>-name.zip` — it won't be picked up again by accident, and stays around as an audit trail. ZIPs applied via an explicit path outside the inbox are left where you put them.
|
|
265
|
+
|
|
266
|
+
### Finding the right ZIP and manifest
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
contextzip apply-zip # exactly one *.zip in .contextzip/inbox/ → uses it
|
|
270
|
+
contextzip apply-zip path/to/fix.zip # explicit path always overrides the inbox
|
|
271
|
+
contextzip apply-zip --manifest path/to/codebase.manifest.json # override auto-detection
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
If `.contextzip/inbox/` has more than one ZIP and you don't specify which, `apply-zip` lists them and asks you to be explicit rather than guessing. Manifest auto-detection picks the most recently created `*.manifest.json` under `.contextzip/output/` — correct for the normal one-round-trip-at-a-time flow; pass `--manifest` explicitly if you've generated several ZIPs before getting a response back.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
214
278
|
## Terminal error watcher
|
|
215
279
|
|
|
216
280
|
The `watch` command wraps your dev server, buffers its output, and packages a debug-ready ZIP the moment you spot an error — no manual file hunting, no copy-pasting stack traces.
|
|
@@ -278,7 +342,7 @@ detects both Node.js and Python from `root/`, without either marker existing at
|
|
|
278
342
|
By default, `.contextzip/` is created at your git root — that's `_find_git_root()` walking up from the current directory until it finds a `.git` folder; outside a repo, it falls back to the current directory. This can be overridden two ways, in order of priority:
|
|
279
343
|
|
|
280
344
|
1. **`CONTEXTZIP_WORKSPACE_LOCATION` environment variable** — for a one-off override on a single run
|
|
281
|
-
2. **A committed `.contextzip
|
|
345
|
+
2. **A committed `.contextzip.json` at the project root** — team-shared, applies to everyone who clones the repo:
|
|
282
346
|
```json
|
|
283
347
|
{ "workspace_location": "git-root" }
|
|
284
348
|
```
|
|
@@ -292,64 +356,25 @@ By default, `.contextzip/` is created at your git root — that's `_find_git_roo
|
|
|
292
356
|
|
|
293
357
|
Accepted values are `"git-root"`, `"cwd"`, or any path (absolute, or relative to the git root). Run `contextzip config` with no flags to see which value is currently active and where it came from.
|
|
294
358
|
|
|
295
|
-
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip
|
|
296
|
-
|
|
297
|
-
---
|
|
298
|
-
|
|
299
|
-
## Project configuration
|
|
359
|
+
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip.json` instead if you'd rather each subproject keep its own.
|
|
300
360
|
|
|
301
|
-
|
|
361
|
+
**Workspace layout:**
|
|
302
362
|
|
|
303
363
|
```
|
|
304
364
|
.contextzip/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
365
|
+
config.json # team-shared preferences (committed)
|
|
366
|
+
.gitignore # ignores everything below except itself + config.json
|
|
367
|
+
output/
|
|
368
|
+
codebase.zip # what you generate and send out
|
|
369
|
+
codebase.manifest.json # local-only — never uploaded, used by apply-zip
|
|
370
|
+
inbox/
|
|
371
|
+
<ai-returned>.zip # drop AI-returned zips here for apply-zip to pick up
|
|
372
|
+
applied/
|
|
373
|
+
<timestamp>-name.zip # archived after a successful apply-zip
|
|
374
|
+
backups/
|
|
375
|
+
<timestamp>/ # pre-overwrite copies, one folder per apply-zip run
|
|
311
376
|
```
|
|
312
377
|
|
|
313
|
-
`.contextzip/` is **not** committed to Git by default — `.contextzip/.gitignore` is written automatically the first time you run contextzip in a repo, and it ignores everything in the folder *except* `config.json` and itself. That's deliberate: `output/` is per-machine, disposable scratch space, while `config.json` holds team-shared preferences you'll usually want everyone on the same page about.
|
|
314
|
-
|
|
315
|
-
`config.json` currently supports:
|
|
316
|
-
|
|
317
|
-
```json
|
|
318
|
-
{
|
|
319
|
-
"workspace_location": "git-root",
|
|
320
|
-
"scan_depth": null,
|
|
321
|
-
"always_include": [],
|
|
322
|
-
"always_exclude": [],
|
|
323
|
-
"ai": {
|
|
324
|
-
"enabled": true,
|
|
325
|
-
"provider": "gemini",
|
|
326
|
-
"max_files": 10
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
All keys are optional — start with just the ones you need.
|
|
332
|
-
|
|
333
|
-
| Key | What it does |
|
|
334
|
-
| --- | --- |
|
|
335
|
-
| `workspace_location` | Same as `contextzip config --set-workspace`, but team-shared. See [Workspace location](#workspace-location). |
|
|
336
|
-
| `scan_depth` | Reserved for a future bounded-depth scan mode. |
|
|
337
|
-
| `always_include` | A standing "force include" list (gitwildmatch patterns). Files matching it are packaged even if an auto-rule or `.gitignore` would otherwise exclude them — e.g. `["docs/architecture.md"]` to always pull in a doc that lives in an otherwise-excluded folder. |
|
|
338
|
-
| `always_exclude` | A standing exclusion list, applied on every run without retyping `-e`/`--exclude`. |
|
|
339
|
-
| `ai.enabled` | Set to `false` to disable `--prompt` entirely for this project; contextzip will refuse with a clear message instead of silently ignoring it. |
|
|
340
|
-
| `ai.provider` | Reserved for future AI providers. Only `"gemini"` is currently supported. |
|
|
341
|
-
| `ai.max_files` | Caps how many files `--prompt` mode may return, for both the Gemini and keyword-heuristic paths. |
|
|
342
|
-
|
|
343
|
-
`always_include`/`always_exclude` are additive on top of `--include`/`--exclude` for that run — an explicit `contextzip include PATH` still has the final say over what's actually packaged. Persistent preferences belong in `config.json` rather than an ever-growing list of CLI flags; CLI flags stay for one-off, explicit actions (`--dry-run`, `--prompt "…"`, `--output FILE`, etc.).
|
|
344
|
-
|
|
345
|
-
A **web-based configuration UI** that generates `config.json` for you is on the roadmap — the schema above is designed to grow additively, so future preferences (including ones set visually) won't require another migration.
|
|
346
|
-
|
|
347
|
-
### Deprecation: `.contextzip.json`
|
|
348
|
-
|
|
349
|
-
Earlier versions of contextzip read a `.contextzip.json` file at the project root for `workspace_location`/`scan_depth`. That file is now **deprecated** in favor of `.contextzip/config.json` — contextzip still reads it automatically if `.contextzip/config.json` doesn't exist yet (so nothing breaks), and prints a one-time reminder to migrate. To migrate, just move its contents into the `"workspace_location"`/`"scan_depth"` keys of a new `.contextzip/config.json` and delete the old file.
|
|
350
|
-
|
|
351
|
-
`~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
|
|
352
|
-
|
|
353
378
|
---
|
|
354
379
|
|
|
355
380
|
## Contributing
|
|
@@ -14,6 +14,8 @@ Every AI session starts the same way: hunt down the relevant files, manually ski
|
|
|
14
14
|
|
|
15
15
|
contextzip eliminates that entirely. Run it from your project root — it detects your stack, applies smart exclusions, produces a lean ZIP, and opens your file manager with the archive already selected. One `Ctrl+C` and you're done.
|
|
16
16
|
|
|
17
|
+
And when the AI tool hands you a ZIP back with the changes, `contextzip apply-zip` closes the loop — no manual unzip-and-hope, no losing track of what actually changed.
|
|
18
|
+
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Features
|
|
@@ -22,9 +24,10 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
22
24
|
- **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
|
|
23
25
|
- **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
|
|
24
26
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
27
|
+
- **Apply AI-returned changes back** — `contextzip apply-zip` takes the ZIP an AI tool hands you back and writes it into your project safely: diffed against what was actually sent, backed up before anything is overwritten, and never silently deleting anything
|
|
25
28
|
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
26
|
-
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip
|
|
27
|
-
- **Persistent workspace** — all generated ZIPs land in `.contextzip
|
|
29
|
+
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip.json`
|
|
30
|
+
- **Persistent workspace** — all generated ZIPs land in `.contextzip/`, discoverable, reusable, and git-ignored automatically
|
|
28
31
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
29
32
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
30
33
|
- **Full CLI control** — `--include`, `--exclude`, `--dry-run`, `--output`, all composable
|
|
@@ -66,7 +69,7 @@ contextzip will:
|
|
|
66
69
|
|
|
67
70
|
1. Detect your framework (e.g. `Next.js + Node.js`)
|
|
68
71
|
2. Apply the appropriate exclusion rules
|
|
69
|
-
3. Create a compressed ZIP in `.contextzip
|
|
72
|
+
3. Create a compressed ZIP in `.contextzip/` at your project root
|
|
70
73
|
4. Open your file manager with the ZIP selected and ready to copy
|
|
71
74
|
|
|
72
75
|
---
|
|
@@ -77,8 +80,6 @@ contextzip will:
|
|
|
77
80
|
contextzip [OPTIONS]
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
> `cz` is a shorthand alias for `contextzip` — both commands are identical and support every option and subcommand shown below (e.g. `cz --git-changes`, `cz exclude node_modules`).
|
|
81
|
-
|
|
82
83
|
| Option | Description |
|
|
83
84
|
|---|---|
|
|
84
85
|
| `-p`, `--prompt TEXT` | Describe your task in plain English — Gemini selects only the relevant files |
|
|
@@ -91,7 +92,7 @@ contextzip [OPTIONS]
|
|
|
91
92
|
| `--no-clipboard` | Skip the clipboard / folder-open step |
|
|
92
93
|
| `--no-gitignore` | Ignore the project's `.gitignore` |
|
|
93
94
|
|
|
94
|
-
**Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
|
|
95
|
+
**Subcommands:** `exclude`, `include`, `apply-zip`, `watch`, `config` — run `contextzip --help` for full details.
|
|
95
96
|
|
|
96
97
|
---
|
|
97
98
|
|
|
@@ -118,6 +119,9 @@ contextzip --prompt "Refactor auth middleware" --dry-run
|
|
|
118
119
|
|
|
119
120
|
# Save to a custom path
|
|
120
121
|
contextzip --output ~/Desktop/project-context.zip
|
|
122
|
+
|
|
123
|
+
# Apply the ZIP an AI tool handed back
|
|
124
|
+
contextzip apply-zip
|
|
121
125
|
```
|
|
122
126
|
|
|
123
127
|
---
|
|
@@ -127,7 +131,7 @@ contextzip --output ~/Desktop/project-context.zip
|
|
|
127
131
|
contextzip is also usable as a library. All CLI capabilities are available as plain Python functions — no Click, no Rich output, no `SystemExit`.
|
|
128
132
|
|
|
129
133
|
```python
|
|
130
|
-
from contextzip import get_git_changes, get_files, create_zip
|
|
134
|
+
from contextzip import get_git_changes, get_files, create_zip, apply_zip
|
|
131
135
|
|
|
132
136
|
# Get changed files and use them directly
|
|
133
137
|
collection = get_git_changes()
|
|
@@ -143,6 +147,10 @@ with open(pkg.zip_path, "rb") as f:
|
|
|
143
147
|
collection = get_files(include=["src/"], exclude=["tests/"])
|
|
144
148
|
pkg = create_zip(collection, output="/tmp/upload.zip")
|
|
145
149
|
print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
150
|
+
|
|
151
|
+
# Apply a ZIP an AI tool returned (auto-detects from .contextzip/inbox/)
|
|
152
|
+
result = apply_zip()
|
|
153
|
+
print(f"Wrote {len(result.written)} files, backup at {result.backup_dir}")
|
|
146
154
|
```
|
|
147
155
|
|
|
148
156
|
| Function | Description |
|
|
@@ -150,9 +158,10 @@ print(f"{pkg.file_count} files, {pkg.compressed_bytes} bytes")
|
|
|
150
158
|
| `get_git_changes(path?)` | Modified, added, and untracked files from git |
|
|
151
159
|
| `get_files(path?, include?, exclude?)` | All project files after exclusion rules |
|
|
152
160
|
| `create_zip(collection, output?)` | Write a `FileCollection` to a ZIP archive |
|
|
161
|
+
| `apply_zip(zip_path?, project_dir?, manifest?)` | Apply an AI-returned ZIP back into the project |
|
|
153
162
|
| `detect_ecosystem(path?)` | Detect framework and confidence level |
|
|
154
163
|
|
|
155
|
-
All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, etc.) rather than exiting.
|
|
164
|
+
All functions default `path` to `Path.cwd()`. Errors raise typed exceptions (`NotARepositoryError`, `GitNotFoundError`, `NoFilesError`, `ZipNotFoundError`, etc.) rather than exiting.
|
|
156
165
|
|
|
157
166
|
---
|
|
158
167
|
|
|
@@ -182,6 +191,61 @@ contextzip config --reset-key # clear and re-run setup
|
|
|
182
191
|
|
|
183
192
|
---
|
|
184
193
|
|
|
194
|
+
## Applying AI-returned changes
|
|
195
|
+
|
|
196
|
+
Once an AI tool has made its edits and handed you back a ZIP, `contextzip apply-zip` writes those changes into your project — safely.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Drop the AI's returned zip into .contextzip/inbox/, then:
|
|
200
|
+
contextzip apply-zip
|
|
201
|
+
|
|
202
|
+
# Or point at it directly, wherever it landed:
|
|
203
|
+
contextzip apply-zip ~/Downloads/fixed-project.zip
|
|
204
|
+
|
|
205
|
+
# Preview first, with full per-file detail:
|
|
206
|
+
contextzip apply-zip --dry-run --verbose
|
|
207
|
+
|
|
208
|
+
# Skip the confirmation prompt (e.g. in a script):
|
|
209
|
+
contextzip apply-zip --yes
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### How it knows what's safe to apply
|
|
213
|
+
|
|
214
|
+
Every ZIP `contextzip` creates gets a small manifest written next to it in `.contextzip/output/` — a hash of each included file, taken at zip-time. This manifest is **never added to the ZIP itself**. It stays local, so it's never uploaded and never visible to whatever AI tool the ZIP is pasted into — nothing for a model, or a curious teammate, to notice or ask about.
|
|
215
|
+
|
|
216
|
+
When you run `apply-zip`, it diffs the returned ZIP against that manifest and classifies every file:
|
|
217
|
+
|
|
218
|
+
| Status | Meaning | Applied automatically? |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| **New** | Wasn't part of the original ZIP | Yes |
|
|
221
|
+
| **Modified** | Was sent, content changed, and your local copy hasn't moved since | Yes |
|
|
222
|
+
| **Unchanged** | Identical to what's already on disk | Skipped — nothing to do |
|
|
223
|
+
| **Drifted** | Your local file changed (or was deleted) since you zipped it | No — flagged, asks first |
|
|
224
|
+
| **Untracked** | In the returned ZIP but has no baseline to compare against | No — flagged, asks first |
|
|
225
|
+
|
|
226
|
+
The common case — you zip some files, the AI edits them, nothing else touched your project meanwhile — applies straight through with just a summary printed, no prompt. Anything that could clobber your own work, or introduce a path you didn't expect, stops and asks before writing.
|
|
227
|
+
|
|
228
|
+
If a ZIP arrives with no matching manifest at all (e.g. it wasn't produced by a `contextzip` round trip), every already-existing path is treated as untracked and you'll be asked to confirm the whole batch.
|
|
229
|
+
|
|
230
|
+
### What it does and doesn't do
|
|
231
|
+
|
|
232
|
+
- **Only adds and modifies files.** Deletions are never inferred from a ZIP's contents — a file that's simply missing from the returned ZIP is left alone.
|
|
233
|
+
- **Backs up before overwriting.** Every file about to be touched is copied into `.contextzip/backups/<timestamp>/` first, preserving its relative path.
|
|
234
|
+
- **Rejects unsafe paths outright.** Any ZIP entry that would resolve outside your project (`../../etc/passwd`-style path traversal) is refused before anything is written — no override flag, no exceptions.
|
|
235
|
+
- **Archives what it consumes.** Once applied, the ZIP is moved to `.contextzip/inbox/applied/<timestamp>-name.zip` — it won't be picked up again by accident, and stays around as an audit trail. ZIPs applied via an explicit path outside the inbox are left where you put them.
|
|
236
|
+
|
|
237
|
+
### Finding the right ZIP and manifest
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
contextzip apply-zip # exactly one *.zip in .contextzip/inbox/ → uses it
|
|
241
|
+
contextzip apply-zip path/to/fix.zip # explicit path always overrides the inbox
|
|
242
|
+
contextzip apply-zip --manifest path/to/codebase.manifest.json # override auto-detection
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
If `.contextzip/inbox/` has more than one ZIP and you don't specify which, `apply-zip` lists them and asks you to be explicit rather than guessing. Manifest auto-detection picks the most recently created `*.manifest.json` under `.contextzip/output/` — correct for the normal one-round-trip-at-a-time flow; pass `--manifest` explicitly if you've generated several ZIPs before getting a response back.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
185
249
|
## Terminal error watcher
|
|
186
250
|
|
|
187
251
|
The `watch` command wraps your dev server, buffers its output, and packages a debug-ready ZIP the moment you spot an error — no manual file hunting, no copy-pasting stack traces.
|
|
@@ -249,7 +313,7 @@ detects both Node.js and Python from `root/`, without either marker existing at
|
|
|
249
313
|
By default, `.contextzip/` is created at your git root — that's `_find_git_root()` walking up from the current directory until it finds a `.git` folder; outside a repo, it falls back to the current directory. This can be overridden two ways, in order of priority:
|
|
250
314
|
|
|
251
315
|
1. **`CONTEXTZIP_WORKSPACE_LOCATION` environment variable** — for a one-off override on a single run
|
|
252
|
-
2. **A committed `.contextzip
|
|
316
|
+
2. **A committed `.contextzip.json` at the project root** — team-shared, applies to everyone who clones the repo:
|
|
253
317
|
```json
|
|
254
318
|
{ "workspace_location": "git-root" }
|
|
255
319
|
```
|
|
@@ -263,64 +327,25 @@ By default, `.contextzip/` is created at your git root — that's `_find_git_roo
|
|
|
263
327
|
|
|
264
328
|
Accepted values are `"git-root"`, `"cwd"`, or any path (absolute, or relative to the git root). Run `contextzip config` with no flags to see which value is currently active and where it came from.
|
|
265
329
|
|
|
266
|
-
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## Project configuration
|
|
330
|
+
This matters most for monorepos where you sometimes run contextzip from a subdirectory (`cd frontend && contextzip`) — with the default `git-root` setting, the workspace still lands at the repo root no matter where you ran it from, so you don't end up with scattered `.contextzip/` folders across `frontend/`, `backend/`, etc. Set `workspace_location: "cwd"` in a project's `.contextzip.json` instead if you'd rather each subproject keep its own.
|
|
271
331
|
|
|
272
|
-
|
|
332
|
+
**Workspace layout:**
|
|
273
333
|
|
|
274
334
|
```
|
|
275
335
|
.contextzip/
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
336
|
+
config.json # team-shared preferences (committed)
|
|
337
|
+
.gitignore # ignores everything below except itself + config.json
|
|
338
|
+
output/
|
|
339
|
+
codebase.zip # what you generate and send out
|
|
340
|
+
codebase.manifest.json # local-only — never uploaded, used by apply-zip
|
|
341
|
+
inbox/
|
|
342
|
+
<ai-returned>.zip # drop AI-returned zips here for apply-zip to pick up
|
|
343
|
+
applied/
|
|
344
|
+
<timestamp>-name.zip # archived after a successful apply-zip
|
|
345
|
+
backups/
|
|
346
|
+
<timestamp>/ # pre-overwrite copies, one folder per apply-zip run
|
|
282
347
|
```
|
|
283
348
|
|
|
284
|
-
`.contextzip/` is **not** committed to Git by default — `.contextzip/.gitignore` is written automatically the first time you run contextzip in a repo, and it ignores everything in the folder *except* `config.json` and itself. That's deliberate: `output/` is per-machine, disposable scratch space, while `config.json` holds team-shared preferences you'll usually want everyone on the same page about.
|
|
285
|
-
|
|
286
|
-
`config.json` currently supports:
|
|
287
|
-
|
|
288
|
-
```json
|
|
289
|
-
{
|
|
290
|
-
"workspace_location": "git-root",
|
|
291
|
-
"scan_depth": null,
|
|
292
|
-
"always_include": [],
|
|
293
|
-
"always_exclude": [],
|
|
294
|
-
"ai": {
|
|
295
|
-
"enabled": true,
|
|
296
|
-
"provider": "gemini",
|
|
297
|
-
"max_files": 10
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
All keys are optional — start with just the ones you need.
|
|
303
|
-
|
|
304
|
-
| Key | What it does |
|
|
305
|
-
| --- | --- |
|
|
306
|
-
| `workspace_location` | Same as `contextzip config --set-workspace`, but team-shared. See [Workspace location](#workspace-location). |
|
|
307
|
-
| `scan_depth` | Reserved for a future bounded-depth scan mode. |
|
|
308
|
-
| `always_include` | A standing "force include" list (gitwildmatch patterns). Files matching it are packaged even if an auto-rule or `.gitignore` would otherwise exclude them — e.g. `["docs/architecture.md"]` to always pull in a doc that lives in an otherwise-excluded folder. |
|
|
309
|
-
| `always_exclude` | A standing exclusion list, applied on every run without retyping `-e`/`--exclude`. |
|
|
310
|
-
| `ai.enabled` | Set to `false` to disable `--prompt` entirely for this project; contextzip will refuse with a clear message instead of silently ignoring it. |
|
|
311
|
-
| `ai.provider` | Reserved for future AI providers. Only `"gemini"` is currently supported. |
|
|
312
|
-
| `ai.max_files` | Caps how many files `--prompt` mode may return, for both the Gemini and keyword-heuristic paths. |
|
|
313
|
-
|
|
314
|
-
`always_include`/`always_exclude` are additive on top of `--include`/`--exclude` for that run — an explicit `contextzip include PATH` still has the final say over what's actually packaged. Persistent preferences belong in `config.json` rather than an ever-growing list of CLI flags; CLI flags stay for one-off, explicit actions (`--dry-run`, `--prompt "…"`, `--output FILE`, etc.).
|
|
315
|
-
|
|
316
|
-
A **web-based configuration UI** that generates `config.json` for you is on the roadmap — the schema above is designed to grow additively, so future preferences (including ones set visually) won't require another migration.
|
|
317
|
-
|
|
318
|
-
### Deprecation: `.contextzip.json`
|
|
319
|
-
|
|
320
|
-
Earlier versions of contextzip read a `.contextzip.json` file at the project root for `workspace_location`/`scan_depth`. That file is now **deprecated** in favor of `.contextzip/config.json` — contextzip still reads it automatically if `.contextzip/config.json` doesn't exist yet (so nothing breaks), and prints a one-time reminder to migrate. To migrate, just move its contents into the `"workspace_location"`/`"scan_depth"` keys of a new `.contextzip/config.json` and delete the old file.
|
|
321
|
-
|
|
322
|
-
`~/.config/contextzip/config.json` (your personal, per-machine config — API key, personal workspace override) is unrelated and unaffected by any of this.
|
|
323
|
-
|
|
324
349
|
---
|
|
325
350
|
|
|
326
351
|
## Contributing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""contextzip — intelligent codebase packager for AI tools."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.3.
|
|
3
|
+
__version__ = "0.3.8"
|
|
4
4
|
|
|
5
5
|
from contextzip.api import (
|
|
6
6
|
FileCollection,
|
|
@@ -9,26 +9,32 @@ from contextzip.api import (
|
|
|
9
9
|
GitNotFoundError,
|
|
10
10
|
GitCommandError,
|
|
11
11
|
NoFilesError,
|
|
12
|
+
ZipNotFoundError,
|
|
12
13
|
get_git_changes,
|
|
13
14
|
get_files,
|
|
14
15
|
create_zip,
|
|
16
|
+
apply_zip,
|
|
15
17
|
detect_ecosystem,
|
|
16
18
|
)
|
|
17
19
|
from contextzip.packager import PackageResult
|
|
20
|
+
from contextzip.applier import ApplyResult
|
|
18
21
|
|
|
19
22
|
__all__ = [
|
|
20
23
|
# Functions
|
|
21
24
|
"get_git_changes",
|
|
22
25
|
"get_files",
|
|
23
26
|
"create_zip",
|
|
27
|
+
"apply_zip",
|
|
24
28
|
"detect_ecosystem",
|
|
25
29
|
# Data types
|
|
26
30
|
"FileCollection",
|
|
27
31
|
"PackageResult",
|
|
32
|
+
"ApplyResult",
|
|
28
33
|
# Exceptions
|
|
29
34
|
"ContextzipError",
|
|
30
35
|
"NotARepositoryError",
|
|
31
36
|
"GitNotFoundError",
|
|
32
37
|
"GitCommandError",
|
|
33
38
|
"NoFilesError",
|
|
39
|
+
"ZipNotFoundError",
|
|
34
40
|
]
|
|
@@ -39,6 +39,7 @@ import os
|
|
|
39
39
|
from dataclasses import dataclass, field
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
|
+
from contextzip.applier import ApplyResult
|
|
42
43
|
from contextzip.detector import DetectionResult, detect
|
|
43
44
|
from contextzip.filters import (
|
|
44
45
|
ResolveResult,
|
|
@@ -127,6 +128,10 @@ class NoFilesError(ContextzipError):
|
|
|
127
128
|
"""Raised when file resolution produces an empty result."""
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
class ZipNotFoundError(ContextzipError):
|
|
132
|
+
"""Raised when apply_zip() can't resolve which zip to apply."""
|
|
133
|
+
|
|
134
|
+
|
|
130
135
|
# ---------------------------------------------------------------------------
|
|
131
136
|
# Public API
|
|
132
137
|
# ---------------------------------------------------------------------------
|
|
@@ -335,6 +340,82 @@ def create_zip(
|
|
|
335
340
|
)
|
|
336
341
|
|
|
337
342
|
|
|
343
|
+
def apply_zip(
|
|
344
|
+
zip_path: str | Path | None = None,
|
|
345
|
+
*,
|
|
346
|
+
project_dir: str | Path | None = None,
|
|
347
|
+
manifest: str | Path | None = None,
|
|
348
|
+
) -> ApplyResult:
|
|
349
|
+
"""
|
|
350
|
+
Apply an AI-returned ZIP back into *project_dir* (defaults to cwd).
|
|
351
|
+
|
|
352
|
+
Diffs the zip against the local sidecar manifest most recently written
|
|
353
|
+
to ``.contextzip/output/`` (or the one at *manifest*, if given) to
|
|
354
|
+
classify every file as new, modified, unchanged, or one with no safe
|
|
355
|
+
baseline (locally edited since zipping, or never part of the original
|
|
356
|
+
manifest). Only adds and modifies files — nothing is ever deleted.
|
|
357
|
+
Every overwritten file is backed up first, under
|
|
358
|
+
``.contextzip/backups/<timestamp>/``.
|
|
359
|
+
|
|
360
|
+
Unlike the CLI, this applies immediately and does not prompt — check
|
|
361
|
+
``contextzip.applier.build_plan()`` yourself first if you want to
|
|
362
|
+
inspect risk before writing (see ``ApplyPlan.is_risky`` /
|
|
363
|
+
``.risky_entries``).
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
zip_path:
|
|
368
|
+
Path to the returned zip. If omitted, auto-detects from
|
|
369
|
+
``.contextzip/inbox/`` — exactly one zip must be present there.
|
|
370
|
+
project_dir:
|
|
371
|
+
Project root to apply into. Defaults to ``Path.cwd()``.
|
|
372
|
+
manifest:
|
|
373
|
+
Explicit manifest path to diff against, overriding auto-detection.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
ApplyResult
|
|
378
|
+
``written`` (list of relative paths written), ``backup_dir``
|
|
379
|
+
(``Path`` or ``None`` if nothing needed backing up), and
|
|
380
|
+
``applied_zip_path`` (where the consumed zip ended up).
|
|
381
|
+
|
|
382
|
+
Raises
|
|
383
|
+
------
|
|
384
|
+
ZipNotFoundError
|
|
385
|
+
If no zip could be resolved — missing, or multiple candidates in
|
|
386
|
+
the inbox with none specified.
|
|
387
|
+
|
|
388
|
+
Example
|
|
389
|
+
-------
|
|
390
|
+
::
|
|
391
|
+
|
|
392
|
+
from contextzip import apply_zip
|
|
393
|
+
|
|
394
|
+
result = apply_zip() # picks up .contextzip/inbox/*.zip
|
|
395
|
+
print(f"Wrote {len(result.written)} files")
|
|
396
|
+
if result.backup_dir:
|
|
397
|
+
print(f"Backup at {result.backup_dir}")
|
|
398
|
+
"""
|
|
399
|
+
from contextzip.applier import (
|
|
400
|
+
ApplyError,
|
|
401
|
+
build_plan,
|
|
402
|
+
execute_plan,
|
|
403
|
+
find_latest_manifest,
|
|
404
|
+
find_zip_to_apply,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
pdir = _resolve_dir(project_dir)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
resolved_zip = find_zip_to_apply(pdir, zip_path)
|
|
411
|
+
except ApplyError as exc:
|
|
412
|
+
raise ZipNotFoundError(str(exc)) from exc
|
|
413
|
+
|
|
414
|
+
manifest_path = find_latest_manifest(pdir, manifest)
|
|
415
|
+
plan = build_plan(resolved_zip, pdir, manifest_path)
|
|
416
|
+
return execute_plan(plan, pdir)
|
|
417
|
+
|
|
418
|
+
|
|
338
419
|
def detect_ecosystem(
|
|
339
420
|
path: str | Path | None = None,
|
|
340
421
|
) -> DetectionResult:
|