pwdnote 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.
- pwdnote-0.1.0/.github/workflows/publish.yml +33 -0
- pwdnote-0.1.0/.gitignore +24 -0
- pwdnote-0.1.0/LICENSE +21 -0
- pwdnote-0.1.0/PKG-INFO +169 -0
- pwdnote-0.1.0/README.md +145 -0
- pwdnote-0.1.0/pyproject.toml +52 -0
- pwdnote-0.1.0/src/pwdnote/__init__.py +3 -0
- pwdnote-0.1.0/src/pwdnote/cli.py +147 -0
- pwdnote-0.1.0/src/pwdnote/config.py +61 -0
- pwdnote-0.1.0/src/pwdnote/crypto.py +47 -0
- pwdnote-0.1.0/src/pwdnote/editor.py +54 -0
- pwdnote-0.1.0/src/pwdnote/notes.py +54 -0
- pwdnote-0.1.0/src/pwdnote/project.py +57 -0
- pwdnote-0.1.0/tests/conftest.py +16 -0
- pwdnote-0.1.0/tests/test_cli.py +84 -0
- pwdnote-0.1.0/tests/test_crypto.py +46 -0
- pwdnote-0.1.0/tests/test_editor.py +35 -0
- pwdnote-0.1.0/tests/test_notes.py +58 -0
- pwdnote-0.1.0/tests/test_project.py +43 -0
- pwdnote-0.1.0/uv.lock +359 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
run: uv python install 3.12
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: uv run pytest
|
|
28
|
+
|
|
29
|
+
- name: Build package
|
|
30
|
+
run: uv build
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
run: uv publish
|
pwdnote-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# pwdnote runtime artifacts (never commit decrypted/temporary notes)
|
|
2
|
+
.pwdnote.tmp
|
|
3
|
+
.pwdnote.cache
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.eggs/
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
|
|
15
|
+
# Tooling
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
htmlcov/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
pwdnote-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Avi Bobrovsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pwdnote-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pwdnote
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Encrypted, project-local notes for your terminal.
|
|
5
|
+
Project-URL: Homepage, https://github.com/pwdnote/pwdnote
|
|
6
|
+
Project-URL: Repository, https://github.com/pwdnote/pwdnote
|
|
7
|
+
Project-URL: Issues, https://github.com/pwdnote/pwdnote/issues
|
|
8
|
+
Author: pwdnote maintainers
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,encryption,notes,project,terminal
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: cryptography>=42.0
|
|
21
|
+
Requires-Dist: rich>=13.7
|
|
22
|
+
Requires-Dist: typer>=0.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# pwdnote
|
|
26
|
+
|
|
27
|
+
**Encrypted, project-local notes for your terminal.**
|
|
28
|
+
|
|
29
|
+
`pwdnote` keeps project-specific notes — TODOs, deployment notes, AWS account
|
|
30
|
+
details, session IDs, customer context, reminders — encrypted on disk, right
|
|
31
|
+
next to your code, without ever exposing plaintext inside the repository.
|
|
32
|
+
|
|
33
|
+
It is **local-first**, **encrypted-by-default**, **Git-friendly**, and
|
|
34
|
+
**terminal-native**. The single encrypted file (`.pwdnote.enc`) is safe to
|
|
35
|
+
commit; without your key it is just ciphertext.
|
|
36
|
+
|
|
37
|
+
`pwdnote` is *not* a cloud service, a note-taking app, a password manager, a
|
|
38
|
+
database, or a sync platform. It does one small thing well.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install pwdnote
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That's it — no further setup. The encryption key is generated automatically on
|
|
49
|
+
first use.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd my-project
|
|
57
|
+
pwdnote init # create .pwdnote.enc
|
|
58
|
+
pwdnote edit # open it in your editor
|
|
59
|
+
pwdnote # print the decrypted note
|
|
60
|
+
pwdnote add "Remember to rotate AWS credentials"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
| Command | Description |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `pwdnote` | Show the decrypted project note. |
|
|
70
|
+
| `pwdnote init` | Create an encrypted note (`# Project Notes`). |
|
|
71
|
+
| `pwdnote edit` | Decrypt, open in `$VISUAL`/`$EDITOR`, re-encrypt on save. |
|
|
72
|
+
| `pwdnote add "text"` | Append `- text` to the note without opening an editor. |
|
|
73
|
+
| `pwdnote status` | Show the project root, note file, and encryption status. |
|
|
74
|
+
| `pwdnote gitignore` | Add recommended ignore entries (`.pwdnote.tmp`, `.pwdnote.cache`). |
|
|
75
|
+
|
|
76
|
+
### Examples
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
$ pwdnote
|
|
80
|
+
TODO:
|
|
81
|
+
- rotate AWS keys
|
|
82
|
+
- update deployment docs
|
|
83
|
+
Notes:
|
|
84
|
+
Client requested staging environment.
|
|
85
|
+
|
|
86
|
+
$ pwdnote status
|
|
87
|
+
Project root:
|
|
88
|
+
~/projects/example
|
|
89
|
+
Note file:
|
|
90
|
+
.pwdnote.enc
|
|
91
|
+
Encrypted:
|
|
92
|
+
Yes
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If no note exists yet:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
No project note found.
|
|
99
|
+
Run:
|
|
100
|
+
pwdnote init
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Project root detection
|
|
106
|
+
|
|
107
|
+
`pwdnote` does not operate only on the current directory. Starting from your
|
|
108
|
+
working directory it searches **upward**:
|
|
109
|
+
|
|
110
|
+
1. If `.pwdnote.enc` exists, that location is used.
|
|
111
|
+
2. Otherwise, if `.git` exists, that location is treated as the project root.
|
|
112
|
+
3. The search stops at the filesystem root.
|
|
113
|
+
|
|
114
|
+
So from `project/backend/api`, running `pwdnote` finds
|
|
115
|
+
`project/.pwdnote.enc`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Security model
|
|
120
|
+
|
|
121
|
+
- **Authenticated encryption.** Notes are encrypted with
|
|
122
|
+
[Fernet](https://cryptography.io/en/latest/fernet/) (AES-128-CBC with an
|
|
123
|
+
HMAC-SHA256 authentication tag) from the well-maintained `cryptography`
|
|
124
|
+
library. We do not implement custom cryptography.
|
|
125
|
+
- **Integrity protection.** Tampered or corrupted files fail to decrypt rather
|
|
126
|
+
than returning garbage.
|
|
127
|
+
- **Key storage.** A single key is generated on first use and stored at
|
|
128
|
+
`~/.config/pwdnote/key` (honouring `XDG_CONFIG_HOME`) with `0600`
|
|
129
|
+
permissions inside a `0700` directory.
|
|
130
|
+
- **No plaintext on disk.** `pwdnote edit` writes to a temporary file with
|
|
131
|
+
restrictive permissions and always deletes it afterwards.
|
|
132
|
+
- **Commit-safe.** `.pwdnote.enc` is meant to be committed; it is ciphertext.
|
|
133
|
+
Do **not** ignore it. (The temporary/cache artifacts are ignored instead.)
|
|
134
|
+
|
|
135
|
+
The crypto backend lives behind a small abstraction (`encrypt_text` /
|
|
136
|
+
`decrypt_text`), so it can be replaced later — and future versions may add
|
|
137
|
+
macOS Keychain, 1Password, `age`, or GPG key backends.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Limitations
|
|
142
|
+
|
|
143
|
+
- The key lives on your machine. If you lose `~/.config/pwdnote/key`, encrypted
|
|
144
|
+
notes cannot be recovered. Back the key up somewhere safe.
|
|
145
|
+
- There is no built-in sync. Sharing a note across machines means sharing the
|
|
146
|
+
same key (e.g. via a secrets manager).
|
|
147
|
+
- One note per project root. `pwdnote` is intentionally simple — no databases,
|
|
148
|
+
no cloud, no plugins, no AI features.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Contributing
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git clone https://github.com/pwdnote/pwdnote
|
|
156
|
+
cd pwdnote
|
|
157
|
+
uv sync # install deps + dev tools
|
|
158
|
+
uv run pytest # run the test suite
|
|
159
|
+
uv run pwdnote --help # try the CLI from source
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Issues and pull requests are welcome. Please keep the tool small and reliable —
|
|
163
|
+
new storage/key backends should slot in behind the existing abstractions.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
[MIT](LICENSE)
|
pwdnote-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# pwdnote
|
|
2
|
+
|
|
3
|
+
**Encrypted, project-local notes for your terminal.**
|
|
4
|
+
|
|
5
|
+
`pwdnote` keeps project-specific notes — TODOs, deployment notes, AWS account
|
|
6
|
+
details, session IDs, customer context, reminders — encrypted on disk, right
|
|
7
|
+
next to your code, without ever exposing plaintext inside the repository.
|
|
8
|
+
|
|
9
|
+
It is **local-first**, **encrypted-by-default**, **Git-friendly**, and
|
|
10
|
+
**terminal-native**. The single encrypted file (`.pwdnote.enc`) is safe to
|
|
11
|
+
commit; without your key it is just ciphertext.
|
|
12
|
+
|
|
13
|
+
`pwdnote` is *not* a cloud service, a note-taking app, a password manager, a
|
|
14
|
+
database, or a sync platform. It does one small thing well.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv tool install pwdnote
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That's it — no further setup. The encryption key is generated automatically on
|
|
25
|
+
first use.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd my-project
|
|
33
|
+
pwdnote init # create .pwdnote.enc
|
|
34
|
+
pwdnote edit # open it in your editor
|
|
35
|
+
pwdnote # print the decrypted note
|
|
36
|
+
pwdnote add "Remember to rotate AWS credentials"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Command | Description |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `pwdnote` | Show the decrypted project note. |
|
|
46
|
+
| `pwdnote init` | Create an encrypted note (`# Project Notes`). |
|
|
47
|
+
| `pwdnote edit` | Decrypt, open in `$VISUAL`/`$EDITOR`, re-encrypt on save. |
|
|
48
|
+
| `pwdnote add "text"` | Append `- text` to the note without opening an editor. |
|
|
49
|
+
| `pwdnote status` | Show the project root, note file, and encryption status. |
|
|
50
|
+
| `pwdnote gitignore` | Add recommended ignore entries (`.pwdnote.tmp`, `.pwdnote.cache`). |
|
|
51
|
+
|
|
52
|
+
### Examples
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
$ pwdnote
|
|
56
|
+
TODO:
|
|
57
|
+
- rotate AWS keys
|
|
58
|
+
- update deployment docs
|
|
59
|
+
Notes:
|
|
60
|
+
Client requested staging environment.
|
|
61
|
+
|
|
62
|
+
$ pwdnote status
|
|
63
|
+
Project root:
|
|
64
|
+
~/projects/example
|
|
65
|
+
Note file:
|
|
66
|
+
.pwdnote.enc
|
|
67
|
+
Encrypted:
|
|
68
|
+
Yes
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If no note exists yet:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
No project note found.
|
|
75
|
+
Run:
|
|
76
|
+
pwdnote init
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Project root detection
|
|
82
|
+
|
|
83
|
+
`pwdnote` does not operate only on the current directory. Starting from your
|
|
84
|
+
working directory it searches **upward**:
|
|
85
|
+
|
|
86
|
+
1. If `.pwdnote.enc` exists, that location is used.
|
|
87
|
+
2. Otherwise, if `.git` exists, that location is treated as the project root.
|
|
88
|
+
3. The search stops at the filesystem root.
|
|
89
|
+
|
|
90
|
+
So from `project/backend/api`, running `pwdnote` finds
|
|
91
|
+
`project/.pwdnote.enc`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Security model
|
|
96
|
+
|
|
97
|
+
- **Authenticated encryption.** Notes are encrypted with
|
|
98
|
+
[Fernet](https://cryptography.io/en/latest/fernet/) (AES-128-CBC with an
|
|
99
|
+
HMAC-SHA256 authentication tag) from the well-maintained `cryptography`
|
|
100
|
+
library. We do not implement custom cryptography.
|
|
101
|
+
- **Integrity protection.** Tampered or corrupted files fail to decrypt rather
|
|
102
|
+
than returning garbage.
|
|
103
|
+
- **Key storage.** A single key is generated on first use and stored at
|
|
104
|
+
`~/.config/pwdnote/key` (honouring `XDG_CONFIG_HOME`) with `0600`
|
|
105
|
+
permissions inside a `0700` directory.
|
|
106
|
+
- **No plaintext on disk.** `pwdnote edit` writes to a temporary file with
|
|
107
|
+
restrictive permissions and always deletes it afterwards.
|
|
108
|
+
- **Commit-safe.** `.pwdnote.enc` is meant to be committed; it is ciphertext.
|
|
109
|
+
Do **not** ignore it. (The temporary/cache artifacts are ignored instead.)
|
|
110
|
+
|
|
111
|
+
The crypto backend lives behind a small abstraction (`encrypt_text` /
|
|
112
|
+
`decrypt_text`), so it can be replaced later — and future versions may add
|
|
113
|
+
macOS Keychain, 1Password, `age`, or GPG key backends.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Limitations
|
|
118
|
+
|
|
119
|
+
- The key lives on your machine. If you lose `~/.config/pwdnote/key`, encrypted
|
|
120
|
+
notes cannot be recovered. Back the key up somewhere safe.
|
|
121
|
+
- There is no built-in sync. Sharing a note across machines means sharing the
|
|
122
|
+
same key (e.g. via a secrets manager).
|
|
123
|
+
- One note per project root. `pwdnote` is intentionally simple — no databases,
|
|
124
|
+
no cloud, no plugins, no AI features.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone https://github.com/pwdnote/pwdnote
|
|
132
|
+
cd pwdnote
|
|
133
|
+
uv sync # install deps + dev tools
|
|
134
|
+
uv run pytest # run the test suite
|
|
135
|
+
uv run pwdnote --help # try the CLI from source
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Issues and pull requests are welcome. Please keep the tool small and reliable —
|
|
139
|
+
new storage/key backends should slot in behind the existing abstractions.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pwdnote"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Encrypted, project-local notes for your terminal."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "pwdnote maintainers" }]
|
|
9
|
+
keywords = ["notes", "encryption", "cli", "terminal", "project"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Utilities",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer>=0.12",
|
|
21
|
+
"rich>=13.7",
|
|
22
|
+
"cryptography>=42.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/pwdnote/pwdnote"
|
|
27
|
+
Repository = "https://github.com/pwdnote/pwdnote"
|
|
28
|
+
Issues = "https://github.com/pwdnote/pwdnote/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
pwdnote = "pwdnote.cli:app"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/pwdnote"]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8",
|
|
43
|
+
"pytest-cov>=5",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
addopts = "-q"
|
|
49
|
+
|
|
50
|
+
[tool.coverage.run]
|
|
51
|
+
branch = true
|
|
52
|
+
source = ["pwdnote"]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Command-line interface for pwdnote."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import NoReturn
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from . import editor as editor_mod
|
|
12
|
+
from . import notes, project
|
|
13
|
+
from .config import load_or_create_key
|
|
14
|
+
from .crypto import DecryptionError
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="pwdnote",
|
|
18
|
+
help="Encrypted, project-local notes for your terminal.",
|
|
19
|
+
no_args_is_help=False,
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fail(message: str) -> NoReturn:
|
|
27
|
+
console.print(message)
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _no_note() -> NoReturn:
|
|
32
|
+
console.print("No project note found.")
|
|
33
|
+
console.print("Run:")
|
|
34
|
+
console.print(" pwdnote init")
|
|
35
|
+
raise typer.Exit(code=1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_existing() -> tuple[Path, bytes, str]:
|
|
39
|
+
"""Locate the note, load the key, and decrypt — or fail with a message."""
|
|
40
|
+
note_path = project.find_existing_note(Path.cwd())
|
|
41
|
+
if note_path is None:
|
|
42
|
+
_no_note()
|
|
43
|
+
key = load_or_create_key()
|
|
44
|
+
try:
|
|
45
|
+
text = notes.read_note(note_path, key)
|
|
46
|
+
except DecryptionError:
|
|
47
|
+
_fail("Unable to decrypt project note.")
|
|
48
|
+
except PermissionError:
|
|
49
|
+
_fail("Unable to access note file.")
|
|
50
|
+
return note_path, key, text
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.callback(invoke_without_command=True)
|
|
54
|
+
def main(ctx: typer.Context) -> None:
|
|
55
|
+
"""Show the decrypted project note when no subcommand is given."""
|
|
56
|
+
if ctx.invoked_subcommand is not None:
|
|
57
|
+
return
|
|
58
|
+
_, _, text = _read_existing()
|
|
59
|
+
console.print(text, end="" if text.endswith("\n") else "\n", highlight=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def init() -> None:
|
|
64
|
+
"""Create an encrypted project note."""
|
|
65
|
+
root = project.resolve_project_root(Path.cwd())
|
|
66
|
+
note_path = project.note_path_for(root)
|
|
67
|
+
key = load_or_create_key()
|
|
68
|
+
try:
|
|
69
|
+
notes.init_note(note_path, key)
|
|
70
|
+
except notes.NoteExistsError:
|
|
71
|
+
_fail("Project note already exists.")
|
|
72
|
+
except PermissionError:
|
|
73
|
+
_fail("Unable to access note file.")
|
|
74
|
+
console.print(f"Created {note_path}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command()
|
|
78
|
+
def edit() -> None:
|
|
79
|
+
"""Edit the project note in your editor."""
|
|
80
|
+
note_path, key, text = _read_existing()
|
|
81
|
+
edited = editor_mod.edit_text(text, note_path.parent)
|
|
82
|
+
try:
|
|
83
|
+
notes.write_note(note_path, key, edited)
|
|
84
|
+
except PermissionError:
|
|
85
|
+
_fail("Unable to access note file.")
|
|
86
|
+
console.print("Note saved.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command()
|
|
90
|
+
def add(text: str = typer.Argument(..., help="Text to append as a bullet point.")) -> None:
|
|
91
|
+
"""Append a line to the project note without opening an editor."""
|
|
92
|
+
note_path, key, _ = _read_existing()
|
|
93
|
+
try:
|
|
94
|
+
notes.append_line(note_path, key, text)
|
|
95
|
+
except PermissionError:
|
|
96
|
+
_fail("Unable to access note file.")
|
|
97
|
+
console.print(f"Added: - {text}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command()
|
|
101
|
+
def status() -> None:
|
|
102
|
+
"""Show the project root, note file, and encryption status."""
|
|
103
|
+
start = Path.cwd()
|
|
104
|
+
note_path = project.find_existing_note(start)
|
|
105
|
+
if note_path is None:
|
|
106
|
+
root = project.resolve_project_root(start)
|
|
107
|
+
console.print("Project root:")
|
|
108
|
+
console.print(f" {root}")
|
|
109
|
+
console.print("Note file:")
|
|
110
|
+
console.print(" (none — run 'pwdnote init')")
|
|
111
|
+
console.print("Encrypted:")
|
|
112
|
+
console.print(" No note yet")
|
|
113
|
+
return
|
|
114
|
+
console.print("Project root:")
|
|
115
|
+
console.print(f" {note_path.parent}")
|
|
116
|
+
console.print("Note file:")
|
|
117
|
+
console.print(f" {note_path.name}")
|
|
118
|
+
console.print("Encrypted:")
|
|
119
|
+
console.print(" Yes")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command()
|
|
123
|
+
def gitignore() -> None:
|
|
124
|
+
"""Add recommended pwdnote entries to the project's .gitignore."""
|
|
125
|
+
root = project.resolve_project_root(Path.cwd())
|
|
126
|
+
gitignore_path = root / ".gitignore"
|
|
127
|
+
recommended = [".pwdnote.tmp", ".pwdnote.cache"]
|
|
128
|
+
|
|
129
|
+
content = gitignore_path.read_text(encoding="utf-8") if gitignore_path.exists() else ""
|
|
130
|
+
existing = set(content.splitlines())
|
|
131
|
+
to_add = [entry for entry in recommended if entry not in existing]
|
|
132
|
+
|
|
133
|
+
if not to_add:
|
|
134
|
+
console.print("All recommended entries are already present.")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
prefix = "" if content == "" or content.endswith("\n") else "\n"
|
|
138
|
+
with gitignore_path.open("a", encoding="utf-8") as handle:
|
|
139
|
+
handle.write(prefix + "".join(f"{entry}\n" for entry in to_add))
|
|
140
|
+
|
|
141
|
+
console.print(f"Added to {gitignore_path}:")
|
|
142
|
+
for entry in to_add:
|
|
143
|
+
console.print(f" {entry}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__": # pragma: no cover
|
|
147
|
+
app()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Key management and configuration.
|
|
2
|
+
|
|
3
|
+
Version 1 stores a single auto-generated key on disk with restrictive
|
|
4
|
+
permissions. The lookup is structured so alternative backends (macOS Keychain,
|
|
5
|
+
1Password, age, GPG) can be added later behind ``load_or_create_key``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .crypto import generate_key
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_config_dir() -> Path:
|
|
17
|
+
"""Return the directory that holds pwdnote configuration and the key.
|
|
18
|
+
|
|
19
|
+
Honours ``PWDNOTE_CONFIG_DIR`` and ``XDG_CONFIG_HOME`` overrides, falling
|
|
20
|
+
back to ``~/.config/pwdnote``.
|
|
21
|
+
"""
|
|
22
|
+
override = os.environ.get("PWDNOTE_CONFIG_DIR")
|
|
23
|
+
if override:
|
|
24
|
+
return Path(override)
|
|
25
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
26
|
+
if xdg:
|
|
27
|
+
return Path(xdg) / "pwdnote"
|
|
28
|
+
return Path.home() / ".config" / "pwdnote"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_key_path() -> Path:
|
|
32
|
+
"""Return the path to the encryption key file."""
|
|
33
|
+
override = os.environ.get("PWDNOTE_KEY_FILE")
|
|
34
|
+
if override:
|
|
35
|
+
return Path(override)
|
|
36
|
+
return get_config_dir() / "key"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_or_create_key() -> bytes:
|
|
40
|
+
"""Load the encryption key, creating it on first use.
|
|
41
|
+
|
|
42
|
+
The key file is created with ``0600`` permissions inside a ``0700``
|
|
43
|
+
directory so that other users on the system cannot read it.
|
|
44
|
+
"""
|
|
45
|
+
path = get_key_path()
|
|
46
|
+
if path.exists():
|
|
47
|
+
return path.read_bytes().strip()
|
|
48
|
+
|
|
49
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
try:
|
|
51
|
+
os.chmod(path.parent, 0o700)
|
|
52
|
+
except OSError:
|
|
53
|
+
# Best effort; not all filesystems support chmod.
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
key = generate_key()
|
|
57
|
+
# O_EXCL guards against a concurrent writer; 0o600 keeps it private.
|
|
58
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
59
|
+
with os.fdopen(fd, "wb") as handle:
|
|
60
|
+
handle.write(key)
|
|
61
|
+
return key
|