pwdnote 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """pwdnote — encrypted, project-local notes for your terminal."""
2
+
3
+ __version__ = "0.1.0"
pwdnote/cli.py ADDED
@@ -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()
pwdnote/config.py ADDED
@@ -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
pwdnote/crypto.py ADDED
@@ -0,0 +1,47 @@
1
+ """Crypto abstraction layer.
2
+
3
+ This module isolates all cryptographic details behind a tiny interface so the
4
+ backend can be swapped later without touching the rest of the codebase.
5
+
6
+ Current backend: Fernet (AES-128-CBC + HMAC-SHA256) from the ``cryptography``
7
+ library, which provides authenticated, integrity-protected encryption with a
8
+ versioned token format. We intentionally do NOT implement custom cryptography.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from cryptography.fernet import Fernet, InvalidToken
14
+
15
+
16
+ class DecryptionError(Exception):
17
+ """Raised when a note cannot be decrypted (wrong key or corrupted data)."""
18
+
19
+
20
+ def generate_key() -> bytes:
21
+ """Generate a fresh, URL-safe base64-encoded encryption key."""
22
+ return Fernet.generate_key()
23
+
24
+
25
+ def _fernet(key: bytes) -> Fernet:
26
+ try:
27
+ return Fernet(key)
28
+ except (ValueError, TypeError) as exc:
29
+ raise DecryptionError("Invalid encryption key.") from exc
30
+
31
+
32
+ def encrypt_text(plaintext: str, key: bytes) -> bytes:
33
+ """Encrypt ``plaintext`` and return an opaque, integrity-protected token."""
34
+ return _fernet(key).encrypt(plaintext.encode("utf-8"))
35
+
36
+
37
+ def decrypt_text(token: bytes, key: bytes) -> str:
38
+ """Decrypt and authenticate ``token``, returning the original text.
39
+
40
+ Raises:
41
+ DecryptionError: if the key is wrong, the key is malformed, or the
42
+ token has been tampered with / corrupted.
43
+ """
44
+ try:
45
+ return _fernet(key).decrypt(token).decode("utf-8")
46
+ except InvalidToken as exc:
47
+ raise DecryptionError("Unable to decrypt project note.") from exc
pwdnote/editor.py ADDED
@@ -0,0 +1,54 @@
1
+ """Editor integration for ``pwdnote edit``.
2
+
3
+ Decrypted content is written to a temporary file with restrictive permissions,
4
+ opened in the user's editor, and deleted afterwards so that plaintext is never
5
+ left behind.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shlex
12
+ import shutil
13
+ import subprocess
14
+ import tempfile
15
+ from pathlib import Path
16
+
17
+ _FALLBACK_EDITORS = ("nano", "vi")
18
+
19
+
20
+ def resolve_editor() -> str:
21
+ """Resolve the editor command using the standard precedence.
22
+
23
+ Order: ``$VISUAL``, ``$EDITOR``, ``nano``, ``vi``.
24
+ """
25
+ for env_var in ("VISUAL", "EDITOR"):
26
+ value = os.environ.get(env_var)
27
+ if value:
28
+ return value
29
+ for candidate in _FALLBACK_EDITORS:
30
+ if shutil.which(candidate):
31
+ return candidate
32
+ return _FALLBACK_EDITORS[-1]
33
+
34
+
35
+ def edit_text(initial: str, directory: Path) -> str:
36
+ """Open ``initial`` in an editor and return the edited result.
37
+
38
+ A temporary file is created in ``directory`` with ``0600`` permissions and
39
+ is always removed before returning, even if the editor fails.
40
+ """
41
+ fd, tmp_name = tempfile.mkstemp(prefix=".pwdnote", suffix=".tmp", dir=str(directory))
42
+ tmp_path = Path(tmp_name)
43
+ try:
44
+ os.chmod(tmp_path, 0o600)
45
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
46
+ handle.write(initial)
47
+ editor_cmd = shlex.split(resolve_editor())
48
+ subprocess.run([*editor_cmd, str(tmp_path)], check=True)
49
+ return tmp_path.read_text(encoding="utf-8")
50
+ finally:
51
+ try:
52
+ tmp_path.unlink()
53
+ except OSError:
54
+ pass
pwdnote/notes.py ADDED
@@ -0,0 +1,54 @@
1
+ """Reading and writing encrypted project notes.
2
+
3
+ All persistence goes through the crypto layer, so plaintext notes are never
4
+ written to disk.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from .crypto import decrypt_text, encrypt_text
12
+
13
+ INITIAL_CONTENT = "# Project Notes\n"
14
+
15
+
16
+ class NoteNotFoundError(Exception):
17
+ """Raised when a note is expected but does not exist."""
18
+
19
+
20
+ class NoteExistsError(Exception):
21
+ """Raised when initializing a note that already exists."""
22
+
23
+
24
+ def note_exists(path: Path) -> bool:
25
+ return path.is_file()
26
+
27
+
28
+ def read_note(path: Path, key: bytes) -> str:
29
+ """Decrypt and return the contents of the note at ``path``."""
30
+ if not path.is_file():
31
+ raise NoteNotFoundError(str(path))
32
+ return decrypt_text(path.read_bytes(), key)
33
+
34
+
35
+ def write_note(path: Path, key: bytes, text: str) -> None:
36
+ """Encrypt ``text`` and write it to ``path``, overwriting any existing note."""
37
+ path.write_bytes(encrypt_text(text, key))
38
+
39
+
40
+ def init_note(path: Path, key: bytes) -> None:
41
+ """Create a new note with the default starter content."""
42
+ if path.exists():
43
+ raise NoteExistsError(str(path))
44
+ write_note(path, key, INITIAL_CONTENT)
45
+
46
+
47
+ def append_line(path: Path, key: bytes, text: str) -> str:
48
+ """Append ``text`` as a bullet point and return the updated note."""
49
+ current = read_note(path, key)
50
+ if current and not current.endswith("\n"):
51
+ current += "\n"
52
+ updated = f"{current}- {text}\n"
53
+ write_note(path, key, updated)
54
+ return updated
pwdnote/project.py ADDED
@@ -0,0 +1,57 @@
1
+ """Project root detection.
2
+
3
+ Starting from the current working directory we search upward:
4
+
5
+ 1. If ``.pwdnote.enc`` exists, use that location.
6
+ 2. Otherwise, if ``.git`` exists, treat that location as the project root.
7
+ 3. Stop at the filesystem root.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+ from typing import Iterator, Optional
14
+
15
+ NOTE_FILENAME = ".pwdnote.enc"
16
+
17
+
18
+ def note_path_for(root: Path) -> Path:
19
+ """Return the encrypted note path for a given project root."""
20
+ return root / NOTE_FILENAME
21
+
22
+
23
+ def _iter_up(start: Path) -> Iterator[Path]:
24
+ start = start.resolve()
25
+ yield start
26
+ yield from start.parents
27
+
28
+
29
+ def find_existing_note(start: Path) -> Optional[Path]:
30
+ """Walk upward from ``start`` and return the first ``.pwdnote.enc`` found."""
31
+ for directory in _iter_up(start):
32
+ candidate = directory / NOTE_FILENAME
33
+ if candidate.is_file():
34
+ return candidate
35
+ return None
36
+
37
+
38
+ def find_git_root(start: Path) -> Optional[Path]:
39
+ """Walk upward from ``start`` and return the first directory with ``.git``."""
40
+ for directory in _iter_up(start):
41
+ if (directory / ".git").exists():
42
+ return directory
43
+ return None
44
+
45
+
46
+ def resolve_project_root(start: Path) -> Path:
47
+ """Determine where a note should live for ``start``.
48
+
49
+ Prefers an existing note's directory, then the git root, then ``start``.
50
+ """
51
+ note = find_existing_note(start)
52
+ if note is not None:
53
+ return note.parent
54
+ git_root = find_git_root(start)
55
+ if git_root is not None:
56
+ return git_root
57
+ return start.resolve()
@@ -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)
@@ -0,0 +1,12 @@
1
+ pwdnote/__init__.py,sha256=cgtxqbtXmQimQNKnWnSrMus11Eqds3wOSqwV_BADngU,91
2
+ pwdnote/cli.py,sha256=_DxinJ7ZzvIbXgPDovc2k90_rj6U8_daDsxrfnFzFlU,4446
3
+ pwdnote/config.py,sha256=iv3K-Ai9ef4R7_ut11KtddF6X4U8BYQZnKuobh5Yh9Y,1818
4
+ pwdnote/crypto.py,sha256=69pJ81rmgK-7kFjcdvv8VW6O1K_OHej6r7pRBdUmcxQ,1585
5
+ pwdnote/editor.py,sha256=nhLYdJGi_nNvXOWSWgh2-lvyNCC55X8ZJUAibwCW0hU,1610
6
+ pwdnote/notes.py,sha256=33HTAZ7043tBWdQF4Tww9sWNk-YYpxnNS-cIObOLcDw,1530
7
+ pwdnote/project.py,sha256=-8OwmvOhtnoF0xUFi9PxF4e5_74CvgI6cPZkgViZsxU,1601
8
+ pwdnote-0.1.0.dist-info/METADATA,sha256=dyIquFIWc-p3QIIEUoAL_IQpsY30T6GSCOtlMO2ABr8,5065
9
+ pwdnote-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ pwdnote-0.1.0.dist-info/entry_points.txt,sha256=KirbXytguyvMBrurbHmJrOLMEKmm3lmygMhFQFgRQOQ,44
11
+ pwdnote-0.1.0.dist-info/licenses/LICENSE,sha256=SsQsQQBPBbwnM7W1YKUyvz_MhSP8NcG789uKP8jT2Hg,1070
12
+ pwdnote-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pwdnote = pwdnote.cli:app
@@ -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.