lambda-watcher 0.1.0__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.
Files changed (54) hide show
  1. lambda_watcher-0.1.0/LICENSE +201 -0
  2. lambda_watcher-0.1.0/PKG-INFO +409 -0
  3. lambda_watcher-0.1.0/README.md +378 -0
  4. lambda_watcher-0.1.0/pyproject.toml +79 -0
  5. lambda_watcher-0.1.0/setup.cfg +4 -0
  6. lambda_watcher-0.1.0/src/lambda_watcher/__init__.py +4 -0
  7. lambda_watcher-0.1.0/src/lambda_watcher/__main__.py +4 -0
  8. lambda_watcher-0.1.0/src/lambda_watcher/analysis/__init__.py +115 -0
  9. lambda_watcher-0.1.0/src/lambda_watcher/analysis/deps.py +291 -0
  10. lambda_watcher-0.1.0/src/lambda_watcher/analysis/envvars.py +80 -0
  11. lambda_watcher-0.1.0/src/lambda_watcher/analysis/handler.py +111 -0
  12. lambda_watcher-0.1.0/src/lambda_watcher/analysis/inventory.py +118 -0
  13. lambda_watcher-0.1.0/src/lambda_watcher/analysis/runtime.py +117 -0
  14. lambda_watcher-0.1.0/src/lambda_watcher/analysis/secrets.py +178 -0
  15. lambda_watcher-0.1.0/src/lambda_watcher/analysis/services.py +76 -0
  16. lambda_watcher-0.1.0/src/lambda_watcher/cli.py +1406 -0
  17. lambda_watcher-0.1.0/src/lambda_watcher/config.py +324 -0
  18. lambda_watcher-0.1.0/src/lambda_watcher/db.py +466 -0
  19. lambda_watcher-0.1.0/src/lambda_watcher/diffing/__init__.py +14 -0
  20. lambda_watcher-0.1.0/src/lambda_watcher/diffing/build.py +51 -0
  21. lambda_watcher-0.1.0/src/lambda_watcher/diffing/compare.py +525 -0
  22. lambda_watcher-0.1.0/src/lambda_watcher/diffing/highlight.py +312 -0
  23. lambda_watcher-0.1.0/src/lambda_watcher/diffing/icons.py +132 -0
  24. lambda_watcher-0.1.0/src/lambda_watcher/diffing/intraline.py +162 -0
  25. lambda_watcher-0.1.0/src/lambda_watcher/diffing/render_html.py +697 -0
  26. lambda_watcher-0.1.0/src/lambda_watcher/diffing/render_text.py +198 -0
  27. lambda_watcher-0.1.0/src/lambda_watcher/extract.py +227 -0
  28. lambda_watcher-0.1.0/src/lambda_watcher/gitmirror.py +151 -0
  29. lambda_watcher-0.1.0/src/lambda_watcher/identify.py +201 -0
  30. lambda_watcher-0.1.0/src/lambda_watcher/ingest.py +480 -0
  31. lambda_watcher-0.1.0/src/lambda_watcher/notify.py +59 -0
  32. lambda_watcher-0.1.0/src/lambda_watcher/reindex.py +158 -0
  33. lambda_watcher-0.1.0/src/lambda_watcher/service.py +553 -0
  34. lambda_watcher-0.1.0/src/lambda_watcher/store.py +209 -0
  35. lambda_watcher-0.1.0/src/lambda_watcher/templates.py +124 -0
  36. lambda_watcher-0.1.0/src/lambda_watcher/utils.py +314 -0
  37. lambda_watcher-0.1.0/src/lambda_watcher/watcher.py +241 -0
  38. lambda_watcher-0.1.0/src/lambda_watcher.egg-info/PKG-INFO +409 -0
  39. lambda_watcher-0.1.0/src/lambda_watcher.egg-info/SOURCES.txt +52 -0
  40. lambda_watcher-0.1.0/src/lambda_watcher.egg-info/dependency_links.txt +1 -0
  41. lambda_watcher-0.1.0/src/lambda_watcher.egg-info/entry_points.txt +3 -0
  42. lambda_watcher-0.1.0/src/lambda_watcher.egg-info/requires.txt +15 -0
  43. lambda_watcher-0.1.0/src/lambda_watcher.egg-info/top_level.txt +1 -0
  44. lambda_watcher-0.1.0/tests/test_analysis.py +132 -0
  45. lambda_watcher-0.1.0/tests/test_cli.py +409 -0
  46. lambda_watcher-0.1.0/tests/test_diff.py +159 -0
  47. lambda_watcher-0.1.0/tests/test_docs.py +167 -0
  48. lambda_watcher-0.1.0/tests/test_extract.py +121 -0
  49. lambda_watcher-0.1.0/tests/test_identify.py +119 -0
  50. lambda_watcher-0.1.0/tests/test_ingest.py +201 -0
  51. lambda_watcher-0.1.0/tests/test_render_html.py +341 -0
  52. lambda_watcher-0.1.0/tests/test_service.py +160 -0
  53. lambda_watcher-0.1.0/tests/test_utils.py +154 -0
  54. lambda_watcher-0.1.0/tests/test_watcher.py +202 -0
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,409 @@
1
+ Metadata-Version: 2.4
2
+ Name: lambda-watcher
3
+ Version: 0.1.0
4
+ Summary: Watch your Downloads folder for AWS Lambda deployment zips, archive them as versions, analyse them, and diff any two versions.
5
+ Author: Lambda Watcher contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/utkarsh5026/lambwatch
8
+ Project-URL: Documentation, https://utkarsh5026.github.io/lambwatch/
9
+ Project-URL: Issues, https://github.com/utkarsh5026/lambwatch/issues
10
+ Keywords: aws,lambda,diff,watchdog,versioning,backup
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Version Control
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: watchdog>=4.0
20
+ Requires-Dist: typer>=0.12
21
+ Requires-Dist: rich>=13.0
22
+ Requires-Dist: PyYAML>=6.0
23
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Requires-Dist: pytest-cov; extra == "dev"
27
+ Requires-Dist: ruff>=0.15; extra == "dev"
28
+ Provides-Extra: lint
29
+ Requires-Dist: ruff>=0.15; extra == "lint"
30
+ Dynamic: license-file
31
+
32
+ # lambda-watcher
33
+
34
+ Watch your Downloads folder for AWS Lambda deployment zips, archive every
35
+ version automatically, and review what actually changed between any two of
36
+ them.
37
+
38
+ If your deploy ritual is *"download the current function zip as a backup, then
39
+ push the new one"*, you end up with a folder of near-identical archives and no
40
+ practical way to answer **"what changed between the 2nd one and the 10th?"**.
41
+ This tool turns that pile into a queryable, diffable history — with no change
42
+ to how you work. You keep downloading zips; it does the rest.
43
+
44
+ **[How it works, step by step →](https://utkarsh5026.github.io/lambwatch/)**
45
+
46
+ ```
47
+ $ lambda-watcher watch
48
+ lambda-watcher 0.1.0 — archiving into ~/.lambda-watcher
49
+ watching ~/Downloads. Press Ctrl-C to stop.
50
+ new order-processor v0001 order-processor.zip — archived a new version
51
+ new order-processor v0002 order-processor (1).zip — 2 added, 2 modified, 3 renamed, 52 vendored
52
+ +24/-5 lines · new: 1 env var, 1 AWS service, 3 secrets
53
+ report: ~/.lambda-watcher/reports/order-processor/v0001-v0002.html
54
+ unchanged order-processor v0002 order-processor (2).zip — identical to version 2
55
+ ```
56
+
57
+ That third line is the one that matters: the same code, downloaded again, is
58
+ recorded as `unchanged` rather than becoming a bogus version 3.
59
+
60
+ ---
61
+
62
+ ## What it does
63
+
64
+ Every time a `.zip` lands in your Downloads folder, lambda-watcher:
65
+
66
+ 1. **waits** until the download has actually finished (no half-written files);
67
+ 2. **works out which Lambda it is** from the filename, a sidecar
68
+ `get-function` JSON, or the archive's contents — `order-processor.zip`,
69
+ `order-processor (1).zip` and `order-processor-2026-03-01.zip` all land
70
+ under the same function;
71
+ 3. **extracts** it safely (path traversal, zip bombs and encrypted archives are
72
+ refused, not trusted);
73
+ 4. **hashes the content, not the file** — re-downloading unchanged code is
74
+ recorded as `unchanged` instead of creating a bogus new version;
75
+ 5. **analyses** it: runtime, handler, dependencies (declared *and* the versions
76
+ actually vendored in the zip), environment variables the code reads, AWS
77
+ services it calls, and hardcoded credentials;
78
+ 6. **archives** it as version *N* with a `manifest.json`, indexes it in SQLite,
79
+ and commits it to a per-function git repo tagged `v0004`.
80
+
81
+ Then you review:
82
+
83
+ ```bash
84
+ lw diff order-processor # last two versions, in the terminal
85
+ lw diff order-processor --from 2 --to 10 # any two versions
86
+ lw diff order-processor --html --open # a shareable HTML report
87
+ lw report order-processor # the whole history, browsable
88
+ lw open order-processor # the whole archive, in your editor
89
+ lw git order-processor log -p # or just use git
90
+ ```
91
+
92
+ ## Why the diffs are actually readable
93
+
94
+ A raw `diff -r` between two Lambda zips is unusable: thousands of vendored
95
+ dependency files drown three lines of real change. lambda-watcher fixes that by
96
+ answering the questions you actually have, in order:
97
+
98
+ | | |
99
+ |---|---|
100
+ | **Your code, separated from theirs** | `node_modules/`, `site-packages/` and friends are classified as vendored and hidden by default. Three changed files, not 3,000. |
101
+ | **Dependencies as versions, not files** | A `boto3` upgrade shows as `boto3 1.34.0 → 1.35.20`, parsed from the `dist-info` actually shipped in the zip — not 400 changed files. |
102
+ | **Config impact, called out** | A new `os.environ["QUEUE_URL"]` is flagged as *"this must exist in the function's environment before you deploy"*. A new `boto3.client("sqs")` is flagged as *"the execution role may need new IAM permissions"*. These are the changes that break a deploy and never show up in a file diff. |
103
+ | **Renames survive edits** | A file that moved *and* changed is shown as one rename with a diff, not an unrelated add plus delete. |
104
+ | **Secrets are diffed too** | An AWS key or Stripe token that appears between v7 and v8 gets its own section. Values are stored redacted; the secret itself never enters the index. |
105
+
106
+ Concretely — the two versions above, where a `diff -rq` reports 61 changed
107
+ files and 56 of them are `site-packages/`:
108
+
109
+ ```
110
+ $ lambda-watcher diff order-processor
111
+ ╭──────────────────────────────────────────────────────────────────────╮
112
+ │ order-processor v0001 → v0002 │
113
+ │ 2 added 2 modified 3 renamed 52 vendored (hidden) +24 / -5 lines │
114
+ ╰──────────────────────────────────────────────────────────────────────╯
115
+ size 8.1 KB → 8.9 KB (+782 B)
116
+
117
+ Dependencies
118
+ manager package from to origin
119
+ + pip pydantic — 2.9.0 installed
120
+ ~ pip boto3 1.34.0 1.35.20 installed
121
+ ~ pip botocore 1.34.0 1.35.20 installed
122
+
123
+ Env vars added: QUEUE_URL
124
+ AWS services added: sqs
125
+ ↑ these need to exist in the function's environment configuration
126
+
127
+ New findings
128
+ high aws-access-key-id config.py:6 AKIA…LE (20 chars)
129
+ high stripe-key config.py:7 sk_l…dc (32 chars)
130
+ low debug-flag config.py:8 DEBUG = True
131
+
132
+ Files
133
+ path + − size
134
+ ~ lambda_function.py 9 2 +322
135
+ ~ requirements.txt 2 1 +17
136
+ + config.py 10 +235
137
+ + helpers/__init__.py
138
+ → {db → helpers/db}.py 1 +33
139
+ → site-packages/boto3-1.{34.0 → 35.20}.dist-info/METADATA 1 1 +1
140
+ → site-packages/botocore-1.{34.0 → 35.20}.dist-info/METADATA 1 1 +1
141
+ ```
142
+
143
+ The 52 vendored files became three version numbers, `db.py` moving into a
144
+ package is one rename rather than a delete plus an add, and the new environment
145
+ variable, the new AWS service and the three secret findings are changes that a
146
+ file diff cannot express at all.
147
+
148
+ That capture is not illustrative — it is the output of
149
+ [`docs/examples/build_demo.py`](docs/examples/build_demo.py), which builds the
150
+ two zips and runs the real pipeline over them. Run it yourself:
151
+
152
+ ```bash
153
+ .venv/bin/python docs/examples/build_demo.py
154
+ ```
155
+
156
+ The same comparison as a shareable HTML page — `--html --open` on any diff, or
157
+ `report` for the whole history — is published from this repository:
158
+ **[see the generated report](https://utkarsh5026.github.io/lambwatch/examples/report/v0001-v0002.html)**.
159
+
160
+ ## Install
161
+
162
+ Python 3.10+.
163
+
164
+ ```bash
165
+ uv tool install lambda-watcher # or: pipx install lambda-watcher
166
+ ```
167
+
168
+ Either one puts `lambda-watcher` — and the shorter alias `lw`, used throughout
169
+ this README — on your `PATH` in its own environment. Plain `pip install
170
+ lambda-watcher` works too if you would rather manage the environment yourself.
171
+
172
+ <details>
173
+ <summary>From a checkout instead</summary>
174
+
175
+ ```bash
176
+ git clone https://github.com/utkarsh5026/lambwatch.git
177
+ cd lambwatch
178
+ python3 -m venv .venv
179
+ .venv/bin/pip install -e .
180
+ ```
181
+
182
+ </details>
183
+
184
+ ## Quick start
185
+
186
+ ```bash
187
+ lw setup
188
+ ```
189
+
190
+ That is the whole thing. `setup` writes a config you can edit, finds your
191
+ downloads folder, offers to import any zips already sitting in it, and starts
192
+ the watcher in the background — as a launchd agent on macOS, a systemd user
193
+ service on Linux, a scheduled task on Windows — so it comes back after a
194
+ reboot without you thinking about it.
195
+
196
+ Then just keep downloading zips. When you want to know what happened:
197
+
198
+ ```bash
199
+ lw # is it running, and what has it caught?
200
+ lw diff order-processor # what changed in the last version
201
+ ```
202
+
203
+ Every new version also writes its own comparison to
204
+ `~/.lambda-watcher/reports/<function>/latest.html` as it is archived, so the
205
+ answer is a bookmark rather than a command.
206
+
207
+ <details>
208
+ <summary>Running it by hand, or setting it up piece by piece</summary>
209
+
210
+ ```bash
211
+ lw watch # run in the foreground instead; Ctrl-C stops it
212
+ lw start # install and start the background watcher
213
+ lw stop # stop it (--remove also unregisters it)
214
+ lw restart # after editing the config
215
+ lw doctor # check the config, watch folders, store, git and disk space
216
+ ```
217
+
218
+ Already have a folder of old backups? Import them oldest-first so the version
219
+ numbers match real history:
220
+
221
+ ```bash
222
+ lw backfill ~/Downloads/lambda-backups --dry-run # check the names first
223
+ lw backfill ~/Downloads/lambda-backups
224
+ ```
225
+
226
+ If your platform's service manager is unavailable — WSL without a systemd user
227
+ session, say — `lw start` falls back to a plain background process and tells
228
+ you it will not survive a reboot. [docs/autostart.md](docs/autostart.md) has
229
+ the manual recipes.
230
+
231
+ </details>
232
+
233
+ ## Commands
234
+
235
+ | Command | What it does |
236
+ |---|---|
237
+ | `setup` | Config, background watcher and any history already on disk, in one go. `--no-service` skips the background watcher, `--yes` takes every default. |
238
+ | `status` | Is it running, and what has it archived? Also what bare `lw` prints. |
239
+ | `start` / `stop` | Register the background watcher with the OS, or stop it. `stop --remove` unregisters it too. |
240
+ | `restart` | Stop and start it — use after editing the config. |
241
+ | `watch` | Watch the download folders in the foreground. `--once` processes what is already there and exits. |
242
+ | `ingest FILE...` | Archive specific zips by hand. `--as NAME` overrides the detected function, `--label` annotates the version. |
243
+ | `backfill DIR` | Import a folder of old downloads, oldest first. `--dry-run` shows the names it would assign. |
244
+ | `ls` | Every function archived so far. |
245
+ | `versions FN` | Every archived version of one function. |
246
+ | `show FN [V]` | Runtime, handler, dependencies, env vars, services and findings for one version. `--files`, `--json`. |
247
+ | `diff FN` | Compare two versions. Defaults to the last two. `--from`/`--to`, `--html`, `--open`, `--vendor`, `--no-patch`, `--json`. |
248
+ | `report FN` | Build a browsable HTML history: an index plus a diff for every step. |
249
+ | `export FN [V]` | Get a version back out as a deployable zip (`--zip`) or a plain folder (`--tree`). |
250
+ | `open FN [V]` | Open the function's mirror in your editor — every version in one folder, with history. Name a version to open just its files. |
251
+ | `git FN ...` | Run git inside that function's mirror repo: `lw git order-processor log --oneline`. |
252
+ | `rename OLD NEW` | Fix a misidentified name. `--alias FRAGMENT` remembers the mapping for next time. |
253
+ | `merge SRC DST` | Combine two entries that are really the same Lambda, renumbering by archive time. |
254
+ | `label FN V TEXT` | Annotate a version, e.g. `label order-processor 7 "prod deploy 2026-03-01"`. |
255
+ | `search TERM` | Search filenames and dependencies across everything archived. |
256
+ | `log` | Recent activity, including downloads that were skipped and why. |
257
+ | `path FN [V]` | Print a path, for `cd "$(lw path order-processor 7)"`. |
258
+ | `rm FN` | Delete a function and everything archived for it. |
259
+ | `reindex` | Rebuild the SQLite index from the manifests on disk. |
260
+ | `doctor` | Check the config, watch folders, store, git and disk space. |
261
+
262
+ Version arguments accept `7`, `v7`, `latest`, `first`, or `-1` / `-2` counting
263
+ back from the newest.
264
+
265
+ ## Where things are kept
266
+
267
+ ```
268
+ ~/.lambda-watcher/
269
+ ├── config.yaml
270
+ ├── index.db # rebuildable index (see `reindex`)
271
+ ├── logs/watcher.log
272
+ ├── reports/ # generated HTML
273
+ ├── quarantine/ # archives that failed, with a reason file
274
+ ├── repos/
275
+ │ └── order-processor/ # git mirror: one commit per version, tagged v0001…
276
+ └── functions/
277
+ └── order-processor/
278
+ └── versions/
279
+ ├── 0001-bd9f77c8/
280
+ │ ├── code/ # the extracted tree
281
+ │ ├── manifest.json # the full analysis
282
+ │ └── package.zip # the original download
283
+ └── 0002-73d375ad/
284
+ ```
285
+
286
+ The directories are the source of truth. `index.db` is a cache you can delete
287
+ and rebuild with `lw reindex`, and the whole store is portable —
288
+ copy it to another machine and reindex.
289
+
290
+ ### The git mirror
291
+
292
+ Each function gets its own git repository at `repos/<name>/`, whose working tree
293
+ is the latest version, with one commit per archived version tagged `v0001`,
294
+ `v0002`, … The folder is named after the function on purpose: it is what an
295
+ editor shows as the workspace root, so an open window says `order-processor`
296
+ rather than something generic. Every tool you already know works on it:
297
+
298
+ ```bash
299
+ lw open order-processor # VS Code, on the whole repo
300
+
301
+ cd "$(lw path order-processor --repo)"
302
+ git diff v0002 v0010 # the diff you originally wanted
303
+ git log --oneline --stat
304
+ ```
305
+
306
+ `open` finds VS Code, Cursor, Windsurf, VSCodium, Zed or Sublime on your `PATH`
307
+ — set `editor:` in the config (or `LAMBDA_WATCHER_EDITOR`, or `--editor`) to
308
+ name a different one. What you get is a folder of real files, not a diff: the
309
+ sidebar reads `order-processor`, and the editor's own file tree, search, Source
310
+ Control panel and timeline all work, with every earlier version a tag away. Name
311
+ a version — `lw open order-processor 3` — to open that version's
312
+ files on their own instead.
313
+
314
+ One repo per function is the point: your 2nd and 10th version of *one* Lambda
315
+ sit next to each other, with no other function's history in the way.
316
+
317
+ ## Configuration
318
+
319
+ `lw init` writes an annotated `~/.lambda-watcher/config.yaml`.
320
+ Everything is optional. The settings worth knowing:
321
+
322
+ ```yaml
323
+ watch:
324
+ dirs: ["~/Downloads"] # add more if you download from several places
325
+ stable_seconds: 2.0 # how long a file must stop changing before it is read
326
+ force_polling: false # turn on for network shares, VM mounts, WSL
327
+ arrival_max_age_seconds: 300 # ignore "modified" events for files older than this
328
+
329
+ store:
330
+ on_ingest: copy # copy | move | leave
331
+ # `move` takes the zip out of Downloads once archived
332
+ strip_wrapper_dir: true # lift a lone `myrepo-1.2.3/` wrapper to the root, so a
333
+ # source archive's ref does not read as a full rewrite
334
+ max_versions_per_function: 0 # 0 keeps everything
335
+
336
+ naming:
337
+ rules: # explicit filename → function name mappings
338
+ - pattern: "^prod[-_](.+?)[-_]deploy"
339
+ name: '\1'
340
+
341
+ diff:
342
+ ignore_vendor: true # hide vendored dependency files in diffs
343
+ context_lines: 3
344
+ ```
345
+
346
+ Set `LAMBDA_WATCHER_HOME` to relocate the whole archive, or
347
+ `LAMBDA_WATCHER_CONFIG` to point at a different config file — useful for
348
+ keeping work and personal archives separate.
349
+
350
+ ### When it guesses the wrong name
351
+
352
+ The filename is a guess, and `lw log` records which strategy was
353
+ used and how confident it was. Two commands fix any mistake:
354
+
355
+ ```bash
356
+ lw rename unknown-a1b2c3d4 order-processor --alias "a1b2c3d4"
357
+ lw merge order-processor-old order-processor
358
+ ```
359
+
360
+ `--alias` teaches it permanently: any future download whose filename contains
361
+ that fragment maps straight to the right function.
362
+
363
+ ## Notes and limits
364
+
365
+ - **Only the code is archived.** A deployment package does not contain the
366
+ function's configuration — memory, timeout, environment variable *values*,
367
+ IAM role, layers or triggers. lambda-watcher infers what it can from the code
368
+ (which env vars are read, which services are called) and flags it, but if you
369
+ want the real configuration archived too, save
370
+ `aws lambda get-function --function-name X > X.json` next to the zip: it is
371
+ picked up as a naming hint, and it is a genuinely useful thing to keep.
372
+ - **Secret scanning is a tripwire, not a security tool.** It catches the
373
+ obvious cases — an AWS key, a private key block, a live Stripe token — and it
374
+ skips placeholders. Treat a finding as a prompt to look, not a verdict.
375
+ - **Layers are separate functions in AWS**, and they are downloaded separately;
376
+ they will be archived as their own entries.
377
+ - **Source archives work too.** A zip from GitHub (or npm, or `git archive`)
378
+ wraps everything in a directory named after the ref — `myrepo-1.2.3/` — and
379
+ names the file the same way. Both are handled: the wrapper is lifted to the
380
+ root so a re-download diffs as an edit rather than a total rewrite, the ref
381
+ becomes the version's label, and the ref is stripped from the name so
382
+ `myrepo-1.2.3.zip`, `myrepo-main.zip` and `myrepo-a1b2c3d.zip` all land as
383
+ versions of one `myrepo`. A trailing `-v2` is still left alone: it is part of
384
+ a name far more often than it is a tag.
385
+ - **A filesystem event is not proof that a file was written.** Windows reports
386
+ a zip as modified when an antivirus scan, the search indexer or OneDrive so
387
+ much as touches it — watchdog asks the OS for attribute and last-access
388
+ changes too — so a background sweep re-announces every zip in the folder at
389
+ once. Events claiming a write to a file nothing has written to are ignored
390
+ (`watch.arrival_max_age_seconds`), and `store.on_ingest: move` only clears
391
+ out a download the watcher saw arrive: a zip that a startup scan or a
392
+ `backfill` merely found is archived where it lies, never deleted.
393
+ - Large vendored packages make for large archives. `store.on_ingest: leave`
394
+ and `store.keep_zip: false` trade the original zips for disk space, and
395
+ `store.max_versions_per_function` caps history.
396
+
397
+ ## Development
398
+
399
+ ```bash
400
+ .venv/bin/pip install -e ".[dev]"
401
+ .venv/bin/python -m pytest
402
+ ```
403
+
404
+ The design decisions behind the analysis and diff layers are written up in
405
+ [docs/design.md](docs/design.md).
406
+
407
+ ## Licence
408
+
409
+ Apache 2.0. See [LICENSE](LICENSE).