keepassxc-cli 0.2.0__tar.gz → 1.0.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.

Potentially problematic release.


This version of keepassxc-cli might be problematic. Click here for more details.

Files changed (41) hide show
  1. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.gitignore +1 -0
  2. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/CLAUDE.md +4 -6
  3. {keepassxc_cli-0.2.0/keepassxc_cli.egg-info → keepassxc_cli-1.0.0}/PKG-INFO +11 -42
  4. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/README.md +9 -40
  5. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/__main__.py +1 -4
  6. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/edit.py +1 -1
  7. keepassxc_cli-1.0.0/keepassxc_cli/output.py +55 -0
  8. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0/keepassxc_cli.egg-info}/PKG-INFO +11 -42
  9. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/SOURCES.txt +0 -3
  10. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/requires.txt +1 -1
  11. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/pyproject.toml +1 -1
  12. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/tests/conftest.py +0 -3
  13. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/tests/test_commands.py +13 -66
  14. keepassxc_cli-1.0.0/tests/test_output.py +80 -0
  15. keepassxc_cli-0.2.0/keepassxc_cli/commands/generate.py +0 -49
  16. keepassxc_cli-0.2.0/keepassxc_cli/commands/ls.py +0 -43
  17. keepassxc_cli-0.2.0/keepassxc_cli/commands/search.py +0 -39
  18. keepassxc_cli-0.2.0/keepassxc_cli/output.py +0 -132
  19. keepassxc_cli-0.2.0/tests/test_output.py +0 -137
  20. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/auto-merge-dependabot.yml +0 -0
  21. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/auto-release.yml +0 -0
  22. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/lint_and_test.yml +0 -0
  23. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/pypi.yml +0 -0
  24. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/LICENSE +0 -0
  25. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/__init__.py +0 -0
  26. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/__init__.py +0 -0
  27. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/add.py +0 -0
  28. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/clip.py +0 -0
  29. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/lock.py +0 -0
  30. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/mkdir.py +0 -0
  31. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/rm.py +0 -0
  32. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/setup.py +0 -0
  33. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/show.py +0 -0
  34. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/status.py +0 -0
  35. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/totp.py +0 -0
  36. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/config.py +0 -0
  37. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/dependency_links.txt +0 -0
  38. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/entry_points.txt +0 -0
  39. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/top_level.txt +0 -0
  40. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/setup.cfg +0 -0
  41. {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/tests/test_config.py +0 -0
@@ -205,3 +205,4 @@ cython_debug/
205
205
  marimo/_static/
206
206
  marimo/_lsp/
207
207
  __marimo__/
208
+ .DS_Store
@@ -4,7 +4,7 @@ This document provides context for AI assistants working on this project.
4
4
 
5
5
  ## Project Purpose
6
6
 
7
- `keepassxc-cli` is a Python command-line tool that communicates with a running KeePassXC instance using the KeePassXC Browser Extension protocol (native messaging over a Unix socket). It enables terminal users to interact with KeePassXC — listing, searching, adding, editing, and deleting entries — while leveraging KeePassXC's biometric (TouchID/fingerprint) unlock support.
7
+ `keepassxc-cli` is a Python command-line tool that communicates with a running KeePassXC instance using the KeePassXC Browser Extension protocol (native messaging over a Unix socket). It enables terminal users to interact with KeePassXC — adding, editing, and deleting entries — while leveraging KeePassXC's biometric (TouchID/fingerprint) unlock support.
8
8
 
9
9
  ## Package Structure
10
10
 
@@ -13,20 +13,17 @@ keepassxc_cli/
13
13
  ├── __init__.py # empty (just from __future__ import annotations)
14
14
  ├── __main__.py # CLI entry point; argument parsing; dispatches to commands
15
15
  ├── config.py # CliConfig dataclass; save/load ~/.keepassxc/cli.json
16
- ├── output.py # Output formatting: table, json, tsv
16
+ ├── output.py # Output formatting: table, json
17
17
  └── commands/
18
18
  ├── __init__.py # empty
19
19
  ├── setup.py # associate with KeePassXC
20
20
  ├── status.py # show connection/association status
21
21
  ├── show.py # show entries by URL
22
- ├── search.py # search all entries
23
- ├── ls.py # list entries or groups
24
22
  ├── add.py # add new entry
25
23
  ├── edit.py # edit existing entry by UUID
26
24
  ├── rm.py # delete entry by UUID
27
25
  ├── totp.py # get TOTP code
28
26
  ├── clip.py # copy field to clipboard
29
- ├── generate.py # generate password
30
27
  ├── lock.py # lock database
31
28
  └── mkdir.py # create group
32
29
 
@@ -103,6 +100,7 @@ ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
103
100
  - **Config permissions**: Config files are written with `0o600` (owner read/write only).
104
101
  - **Venv**: Always use `.venv` for development.
105
102
  - **Python ≥ 3.10** required.
103
+ - **Password visibility**: `show` omits password and TOTP entirely when `-p` is not passed (no masking).
106
104
 
107
105
  ## Config Files
108
106
 
@@ -113,4 +111,4 @@ ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
113
111
 
114
112
  ## Output Formats
115
113
 
116
- Three formats are supported everywhere: `table` (default), `json`, `tsv`. The `--format` global flag or `default_format` in `cli.json` controls the default.
114
+ Two formats are supported: `table` (default) and `json`. The `-j / --json` flag on individual subcommands or `default_format` in `cli.json` controls the default.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keepassxc-cli
3
- Version: 0.2.0
3
+ Version: 1.0.0
4
4
  Summary: CLI for KeePassXC using the browser extension protocol with biometric unlock
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: keepassxc-browser-api==0.1.3
9
+ Requires-Dist: keepassxc-browser-api==1.0.0
10
10
  Requires-Dist: pyperclip==1.8.0
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -23,7 +23,7 @@ A command-line interface for [KeePassXC](https://keepassxc.org/) that communicat
23
23
 
24
24
  - **Biometric unlock**: On macOS with TouchID (or similar) configured in KeePassXC, you can authenticate via fingerprint rather than typing your master password.
25
25
  - **No master password in shell history**: Authentication happens through KeePassXC's GUI, not the terminal.
26
- - **Full CRUD**: List, search, add, edit, delete entries and groups.
26
+ - **CRUD**: Add, edit, delete entries and groups.
27
27
  - **TOTP**: Retrieve time-based one-time passwords.
28
28
  - **Clipboard**: Copy credentials directly to the clipboard.
29
29
 
@@ -71,11 +71,11 @@ keepassxc-cli [--config PATH] [--browser-api-config PATH] [-v] COMMAND [COMMAND
71
71
  | `--browser-api-config` | Path to browser API config file (default: `~/.keepassxc/browser-api.json`) |
72
72
  | `-v, --verbose` | Enable verbose/debug logging |
73
73
 
74
- Most commands support a `-j / --json` flag for JSON output — pass it anywhere after the subcommand name:
74
+ Some commands support a `-j / --json` flag for JSON output — pass it anywhere after the subcommand name:
75
75
 
76
76
  ```bash
77
77
  keepassxc-cli show https://github.com -j
78
- keepassxc-cli ls -j
78
+ keepassxc-cli status -j
79
79
  ```
80
80
 
81
81
  ### Commands
@@ -93,29 +93,6 @@ keepassxc-cli status
93
93
  keepassxc-cli status -j
94
94
  ```
95
95
 
96
- #### `ls` — List all entries or groups
97
-
98
- > **Requires** "Allow access to all entries" to be enabled in
99
- > KeePassXC → Settings → Browser Integration.
100
-
101
- ```bash
102
- keepassxc-cli ls # list all entries (includes UUID column)
103
- keepassxc-cli ls --groups # list groups as a tree
104
- keepassxc-cli ls -j # output as JSON
105
- ```
106
-
107
- The UUIDs shown here are needed for `edit` and `rm`.
108
-
109
- #### `search` — Search entries by URL or hostname
110
-
111
- ```bash
112
- keepassxc-cli search github.com
113
- keepassxc-cli search https://github.com -p # reveal passwords
114
- keepassxc-cli search github.com -j
115
- ```
116
-
117
- KeePassXC matches entries by URL/hostname (the same mechanism the browser extension uses).
118
-
119
96
  #### `show` — Show entries for a URL
120
97
 
121
98
  ```bash
@@ -124,6 +101,8 @@ keepassxc-cli show https://github.com -p # reveal password and TOTP
124
101
  keepassxc-cli show https://github.com -j
125
102
  ```
126
103
 
104
+ Without `-p`, password and TOTP are omitted from the output entirely.
105
+
127
106
  #### `totp` — Get TOTP code
128
107
 
129
108
  ```bash
@@ -151,7 +130,7 @@ keepassxc-cli add --url https://example.com --username user --password mypass
151
130
 
152
131
  ```bash
153
132
  # Get the UUID first
154
- keepassxc-cli show https://github.com
133
+ keepassxc-cli show https://github.com -p
155
134
 
156
135
  # Then edit — --url is required to resolve the current entry
157
136
  keepassxc-cli edit <uuid> --url https://github.com --username newuser
@@ -165,16 +144,6 @@ keepassxc-cli rm <uuid> # prompts for confirmation
165
144
  keepassxc-cli rm <uuid> --yes # skip confirmation
166
145
  ```
167
146
 
168
- #### `generate` — Generate a password
169
-
170
- ```bash
171
- keepassxc-cli generate # prints a password
172
- keepassxc-cli generate --clip # copy to clipboard instead
173
- keepassxc-cli generate -j
174
- ```
175
-
176
- KeePassXC uses its own configured password generator profile (set in KeePassXC → Tools → Password Generator).
177
-
178
147
  #### `lock` — Lock the database
179
148
 
180
149
  ```bash
@@ -237,7 +206,7 @@ ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
237
206
  ## Known Limitations
238
207
 
239
208
  - Requires KeePassXC to be **running** and the database to be **open** (or biometric auto-unlock configured).
240
- - The `ls` command requires "Allow access to all entries" to be enabled in KeePassXC → Settings → Browser Integration.
241
- - The `clip` and `generate --clip` commands require `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
209
+ - The `clip` command requires `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
242
210
  - The browser integration protocol does not support moving entries between groups directly.
243
- - Entry URLs in the database are stored as `KPH: url` string fields; entries without a URL field may not appear in `show`/`search` results.
211
+ - Entry lookup is by URL/hostname only (same as the browser extension). Title-based search is not supported by the protocol.
212
+ - **String fields** (`string_fields` in JSON output) require the KeePassXC setting "Support KPH fields" to be enabled, and custom attributes must be prefixed with `KPH: ` in the KeePassXC entry's "Advanced" tab. This is a server-side KeePassXC requirement, not something the CLI can control.
@@ -8,7 +8,7 @@ A command-line interface for [KeePassXC](https://keepassxc.org/) that communicat
8
8
 
9
9
  - **Biometric unlock**: On macOS with TouchID (or similar) configured in KeePassXC, you can authenticate via fingerprint rather than typing your master password.
10
10
  - **No master password in shell history**: Authentication happens through KeePassXC's GUI, not the terminal.
11
- - **Full CRUD**: List, search, add, edit, delete entries and groups.
11
+ - **CRUD**: Add, edit, delete entries and groups.
12
12
  - **TOTP**: Retrieve time-based one-time passwords.
13
13
  - **Clipboard**: Copy credentials directly to the clipboard.
14
14
 
@@ -56,11 +56,11 @@ keepassxc-cli [--config PATH] [--browser-api-config PATH] [-v] COMMAND [COMMAND
56
56
  | `--browser-api-config` | Path to browser API config file (default: `~/.keepassxc/browser-api.json`) |
57
57
  | `-v, --verbose` | Enable verbose/debug logging |
58
58
 
59
- Most commands support a `-j / --json` flag for JSON output — pass it anywhere after the subcommand name:
59
+ Some commands support a `-j / --json` flag for JSON output — pass it anywhere after the subcommand name:
60
60
 
61
61
  ```bash
62
62
  keepassxc-cli show https://github.com -j
63
- keepassxc-cli ls -j
63
+ keepassxc-cli status -j
64
64
  ```
65
65
 
66
66
  ### Commands
@@ -78,29 +78,6 @@ keepassxc-cli status
78
78
  keepassxc-cli status -j
79
79
  ```
80
80
 
81
- #### `ls` — List all entries or groups
82
-
83
- > **Requires** "Allow access to all entries" to be enabled in
84
- > KeePassXC → Settings → Browser Integration.
85
-
86
- ```bash
87
- keepassxc-cli ls # list all entries (includes UUID column)
88
- keepassxc-cli ls --groups # list groups as a tree
89
- keepassxc-cli ls -j # output as JSON
90
- ```
91
-
92
- The UUIDs shown here are needed for `edit` and `rm`.
93
-
94
- #### `search` — Search entries by URL or hostname
95
-
96
- ```bash
97
- keepassxc-cli search github.com
98
- keepassxc-cli search https://github.com -p # reveal passwords
99
- keepassxc-cli search github.com -j
100
- ```
101
-
102
- KeePassXC matches entries by URL/hostname (the same mechanism the browser extension uses).
103
-
104
81
  #### `show` — Show entries for a URL
105
82
 
106
83
  ```bash
@@ -109,6 +86,8 @@ keepassxc-cli show https://github.com -p # reveal password and TOTP
109
86
  keepassxc-cli show https://github.com -j
110
87
  ```
111
88
 
89
+ Without `-p`, password and TOTP are omitted from the output entirely.
90
+
112
91
  #### `totp` — Get TOTP code
113
92
 
114
93
  ```bash
@@ -136,7 +115,7 @@ keepassxc-cli add --url https://example.com --username user --password mypass
136
115
 
137
116
  ```bash
138
117
  # Get the UUID first
139
- keepassxc-cli show https://github.com
118
+ keepassxc-cli show https://github.com -p
140
119
 
141
120
  # Then edit — --url is required to resolve the current entry
142
121
  keepassxc-cli edit <uuid> --url https://github.com --username newuser
@@ -150,16 +129,6 @@ keepassxc-cli rm <uuid> # prompts for confirmation
150
129
  keepassxc-cli rm <uuid> --yes # skip confirmation
151
130
  ```
152
131
 
153
- #### `generate` — Generate a password
154
-
155
- ```bash
156
- keepassxc-cli generate # prints a password
157
- keepassxc-cli generate --clip # copy to clipboard instead
158
- keepassxc-cli generate -j
159
- ```
160
-
161
- KeePassXC uses its own configured password generator profile (set in KeePassXC → Tools → Password Generator).
162
-
163
132
  #### `lock` — Lock the database
164
133
 
165
134
  ```bash
@@ -222,7 +191,7 @@ ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
222
191
  ## Known Limitations
223
192
 
224
193
  - Requires KeePassXC to be **running** and the database to be **open** (or biometric auto-unlock configured).
225
- - The `ls` command requires "Allow access to all entries" to be enabled in KeePassXC → Settings → Browser Integration.
226
- - The `clip` and `generate --clip` commands require `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
194
+ - The `clip` command requires `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
227
195
  - The browser integration protocol does not support moving entries between groups directly.
228
- - Entry URLs in the database are stored as `KPH: url` string fields; entries without a URL field may not appear in `show`/`search` results.
196
+ - Entry lookup is by URL/hostname only (same as the browser extension). Title-based search is not supported by the protocol.
197
+ - **String fields** (`string_fields` in JSON output) require the KeePassXC setting "Support KPH fields" to be enabled, and custom attributes must be prefixed with `KPH: ` in the KeePassXC entry's "Advanced" tab. This is a server-side KeePassXC requirement, not something the CLI can control.
@@ -11,7 +11,7 @@ from keepassxc_browser_api import BrowserClient, BrowserConfig
11
11
  from keepassxc_browser_api.exceptions import KeePassXCError, ConnectionError
12
12
 
13
13
  from .config import CliConfig, DEFAULT_CLI_CONFIG_PATH
14
- from .commands import setup, status, show, search, ls, add, edit, rm, totp, clip, generate, lock, mkdir
14
+ from .commands import setup, status, show, add, edit, rm, totp, clip, lock, mkdir
15
15
 
16
16
  # Shared parent parser that injects -j/--json into each subparser that supports it.
17
17
  # Defined at module level so command modules can import it if needed.
@@ -48,14 +48,11 @@ def main() -> None:
48
48
  setup.add_parser(subparsers)
49
49
  status.add_parser(subparsers, fmt_parent)
50
50
  show.add_parser(subparsers, fmt_parent)
51
- search.add_parser(subparsers, fmt_parent)
52
- ls.add_parser(subparsers, fmt_parent)
53
51
  add.add_parser(subparsers)
54
52
  edit.add_parser(subparsers)
55
53
  rm.add_parser(subparsers)
56
54
  totp.add_parser(subparsers, fmt_parent)
57
55
  clip.add_parser(subparsers)
58
- generate.add_parser(subparsers, fmt_parent)
59
56
  lock.add_parser(subparsers)
60
57
  mkdir.add_parser(subparsers)
61
58
 
@@ -14,7 +14,7 @@ def add_parser(subparsers: argparse._SubParsersAction) -> None:
14
14
  "edit",
15
15
  help="Edit an existing entry by UUID",
16
16
  description=(
17
- "Edit an existing entry. The UUID must be known (use 'ls' or 'show' to find it).\n"
17
+ "Edit an existing entry. The UUID must be known (use 'show' to find it).\n"
18
18
  "Provide --url so the entry can be resolved; omitted fields are left unchanged."
19
19
  ),
20
20
  )
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from keepassxc_browser_api import Entry
6
+
7
+
8
+ def print_entry_detail(entry: Entry, fmt: str = "table", show_password: bool = False) -> None:
9
+ totp = entry.totp if show_password else None
10
+ if fmt == "json":
11
+ data = {
12
+ "uuid": entry.uuid,
13
+ "name": entry.name,
14
+ "login": entry.login,
15
+ "group": entry.group,
16
+ "group_uuid": entry.group_uuid,
17
+ "string_fields": entry.string_fields,
18
+ }
19
+ if show_password:
20
+ data["password"] = entry.password
21
+ if totp is not None:
22
+ data["totp"] = totp
23
+ print(json.dumps(data, indent=2))
24
+ return
25
+
26
+ print(f"UUID: {entry.uuid}")
27
+ print(f"Title: {entry.name}")
28
+ print(f"Username: {entry.login}")
29
+ if show_password:
30
+ print(f"Password: {entry.password}")
31
+ if totp:
32
+ print(f"TOTP: {totp}")
33
+ if entry.group:
34
+ print(f"Group: {entry.group}")
35
+ if entry.group_uuid:
36
+ print(f"Group UUID: {entry.group_uuid}")
37
+ if entry.string_fields:
38
+ for sf in entry.string_fields:
39
+ for k, v in sf.items():
40
+ print(f"{k}: {v}")
41
+
42
+
43
+ def print_totp(totp: str, fmt: str = "table") -> None:
44
+ if fmt == "json":
45
+ print(json.dumps({"totp": totp}, indent=2))
46
+ return
47
+ print(totp)
48
+
49
+
50
+ def print_status(info: dict, fmt: str = "table") -> None:
51
+ if fmt == "json":
52
+ print(json.dumps(info, indent=2))
53
+ return
54
+ for k, v in info.items():
55
+ print(f"{k}: {v}")
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keepassxc-cli
3
- Version: 0.2.0
3
+ Version: 1.0.0
4
4
  Summary: CLI for KeePassXC using the browser extension protocol with biometric unlock
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: keepassxc-browser-api==0.1.3
9
+ Requires-Dist: keepassxc-browser-api==1.0.0
10
10
  Requires-Dist: pyperclip==1.8.0
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=7.0; extra == "dev"
@@ -23,7 +23,7 @@ A command-line interface for [KeePassXC](https://keepassxc.org/) that communicat
23
23
 
24
24
  - **Biometric unlock**: On macOS with TouchID (or similar) configured in KeePassXC, you can authenticate via fingerprint rather than typing your master password.
25
25
  - **No master password in shell history**: Authentication happens through KeePassXC's GUI, not the terminal.
26
- - **Full CRUD**: List, search, add, edit, delete entries and groups.
26
+ - **CRUD**: Add, edit, delete entries and groups.
27
27
  - **TOTP**: Retrieve time-based one-time passwords.
28
28
  - **Clipboard**: Copy credentials directly to the clipboard.
29
29
 
@@ -71,11 +71,11 @@ keepassxc-cli [--config PATH] [--browser-api-config PATH] [-v] COMMAND [COMMAND
71
71
  | `--browser-api-config` | Path to browser API config file (default: `~/.keepassxc/browser-api.json`) |
72
72
  | `-v, --verbose` | Enable verbose/debug logging |
73
73
 
74
- Most commands support a `-j / --json` flag for JSON output — pass it anywhere after the subcommand name:
74
+ Some commands support a `-j / --json` flag for JSON output — pass it anywhere after the subcommand name:
75
75
 
76
76
  ```bash
77
77
  keepassxc-cli show https://github.com -j
78
- keepassxc-cli ls -j
78
+ keepassxc-cli status -j
79
79
  ```
80
80
 
81
81
  ### Commands
@@ -93,29 +93,6 @@ keepassxc-cli status
93
93
  keepassxc-cli status -j
94
94
  ```
95
95
 
96
- #### `ls` — List all entries or groups
97
-
98
- > **Requires** "Allow access to all entries" to be enabled in
99
- > KeePassXC → Settings → Browser Integration.
100
-
101
- ```bash
102
- keepassxc-cli ls # list all entries (includes UUID column)
103
- keepassxc-cli ls --groups # list groups as a tree
104
- keepassxc-cli ls -j # output as JSON
105
- ```
106
-
107
- The UUIDs shown here are needed for `edit` and `rm`.
108
-
109
- #### `search` — Search entries by URL or hostname
110
-
111
- ```bash
112
- keepassxc-cli search github.com
113
- keepassxc-cli search https://github.com -p # reveal passwords
114
- keepassxc-cli search github.com -j
115
- ```
116
-
117
- KeePassXC matches entries by URL/hostname (the same mechanism the browser extension uses).
118
-
119
96
  #### `show` — Show entries for a URL
120
97
 
121
98
  ```bash
@@ -124,6 +101,8 @@ keepassxc-cli show https://github.com -p # reveal password and TOTP
124
101
  keepassxc-cli show https://github.com -j
125
102
  ```
126
103
 
104
+ Without `-p`, password and TOTP are omitted from the output entirely.
105
+
127
106
  #### `totp` — Get TOTP code
128
107
 
129
108
  ```bash
@@ -151,7 +130,7 @@ keepassxc-cli add --url https://example.com --username user --password mypass
151
130
 
152
131
  ```bash
153
132
  # Get the UUID first
154
- keepassxc-cli show https://github.com
133
+ keepassxc-cli show https://github.com -p
155
134
 
156
135
  # Then edit — --url is required to resolve the current entry
157
136
  keepassxc-cli edit <uuid> --url https://github.com --username newuser
@@ -165,16 +144,6 @@ keepassxc-cli rm <uuid> # prompts for confirmation
165
144
  keepassxc-cli rm <uuid> --yes # skip confirmation
166
145
  ```
167
146
 
168
- #### `generate` — Generate a password
169
-
170
- ```bash
171
- keepassxc-cli generate # prints a password
172
- keepassxc-cli generate --clip # copy to clipboard instead
173
- keepassxc-cli generate -j
174
- ```
175
-
176
- KeePassXC uses its own configured password generator profile (set in KeePassXC → Tools → Password Generator).
177
-
178
147
  #### `lock` — Lock the database
179
148
 
180
149
  ```bash
@@ -237,7 +206,7 @@ ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
237
206
  ## Known Limitations
238
207
 
239
208
  - Requires KeePassXC to be **running** and the database to be **open** (or biometric auto-unlock configured).
240
- - The `ls` command requires "Allow access to all entries" to be enabled in KeePassXC → Settings → Browser Integration.
241
- - The `clip` and `generate --clip` commands require `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
209
+ - The `clip` command requires `pyperclip` and a working clipboard (e.g., `xclip`/`xsel` on Linux, built-in on macOS/Windows).
242
210
  - The browser integration protocol does not support moving entries between groups directly.
243
- - Entry URLs in the database are stored as `KPH: url` string fields; entries without a URL field may not appear in `show`/`search` results.
211
+ - Entry lookup is by URL/hostname only (same as the browser extension). Title-based search is not supported by the protocol.
212
+ - **String fields** (`string_fields` in JSON output) require the KeePassXC setting "Support KPH fields" to be enabled, and custom attributes must be prefixed with `KPH: ` in the KeePassXC entry's "Advanced" tab. This is a server-side KeePassXC requirement, not something the CLI can control.
@@ -21,12 +21,9 @@ keepassxc_cli/commands/__init__.py
21
21
  keepassxc_cli/commands/add.py
22
22
  keepassxc_cli/commands/clip.py
23
23
  keepassxc_cli/commands/edit.py
24
- keepassxc_cli/commands/generate.py
25
24
  keepassxc_cli/commands/lock.py
26
- keepassxc_cli/commands/ls.py
27
25
  keepassxc_cli/commands/mkdir.py
28
26
  keepassxc_cli/commands/rm.py
29
- keepassxc_cli/commands/search.py
30
27
  keepassxc_cli/commands/setup.py
31
28
  keepassxc_cli/commands/show.py
32
29
  keepassxc_cli/commands/status.py
@@ -1,4 +1,4 @@
1
- keepassxc-browser-api==0.1.3
1
+ keepassxc-browser-api==1.0.0
2
2
  pyperclip==1.8.0
3
3
 
4
4
  [dev]
@@ -10,7 +10,7 @@ readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  dependencies = [
13
- "keepassxc-browser-api==0.1.3",
13
+ "keepassxc-browser-api==1.0.0",
14
14
  "pyperclip==1.8.0",
15
15
  ]
16
16
 
@@ -70,11 +70,8 @@ def mock_client():
70
70
  client.test_associate.return_value = True
71
71
  client.get_logins.return_value = [make_entry()]
72
72
  client.set_login.return_value = True
73
- client.get_database_entries.return_value = [make_entry()]
74
- client.get_database_groups.return_value = [make_group()]
75
73
  client.create_group.return_value = make_group(uuid="new-uuid", name="NewGroup")
76
74
  client.get_totp.return_value = "123456"
77
75
  client.delete_entry.return_value = True
78
76
  client.lock_database.return_value = True
79
- client.generate_password.return_value = "GeneratedPass123"
80
77
  return client
@@ -7,10 +7,10 @@ from unittest.mock import MagicMock, patch
7
7
 
8
8
  import pytest
9
9
 
10
- from keepassxc_browser_api import Entry, Group, BrowserConfig, Association
10
+ from keepassxc_browser_api import Entry, BrowserConfig, Association
11
11
  from keepassxc_cli.config import CliConfig
12
12
  from keepassxc_cli.commands import (
13
- setup, status, show, search, ls, add, edit, rm, totp, clip, generate, lock, mkdir,
13
+ setup, status, show, add, edit, rm, totp, clip, lock, mkdir,
14
14
  )
15
15
 
16
16
 
@@ -33,16 +33,13 @@ def make_args(**kwargs) -> argparse.Namespace:
33
33
  defaults = {
34
34
  "show_password": False,
35
35
  "yes": False,
36
- "groups": False,
37
36
  "field": "password",
38
- "clip": False,
39
37
  "url": "https://example.com",
40
38
  "username": "user",
41
39
  "password": "pass",
42
40
  "title": "Example",
43
41
  "group_uuid": "",
44
42
  "uuid": "abcdef12-0000-0000-0000-000000000000",
45
- "query": "test",
46
43
  "name": "NewGroup",
47
44
  "parent_uuid": "",
48
45
  }
@@ -109,52 +106,26 @@ class TestShowCommand:
109
106
  assert rc == 0
110
107
  out = capsys.readouterr().out
111
108
  assert "Test Entry" in out
109
+ assert "Password" not in out
112
110
 
113
- def test_no_entries(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
114
- mock_client.get_logins.return_value = []
115
- args = make_args(url="https://notfound.com")
116
- rc = show.run(mock_client, args, cli_config, browser_config, browser_config_path)
117
- assert rc == 1
118
- assert "No entries" in capsys.readouterr().err
119
-
120
-
121
- # --- search ---
122
-
123
- class TestSearchCommand:
124
- def test_matches_found(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
125
- entry = mock_entry(name="GitHub", login="user@github.com")
111
+ def test_found_entries_show_password(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
112
+ entry = mock_entry()
126
113
  mock_client.get_logins.return_value = [entry]
127
- args = make_args(query="github.com")
128
- rc = search.run(mock_client, args, cli_config, browser_config, browser_config_path)
114
+ args = make_args(url="https://example.com", show_password=True)
115
+ rc = show.run(mock_client, args, cli_config, browser_config, browser_config_path)
129
116
  assert rc == 0
130
- assert "GitHub" in capsys.readouterr().out
117
+ out = capsys.readouterr().out
118
+ assert "Test Entry" in out
119
+ assert "Password:" in out
131
120
 
132
- def test_no_matches(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
121
+ def test_no_entries(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
133
122
  mock_client.get_logins.return_value = []
134
- args = make_args(query="zzznomatch.com")
135
- rc = search.run(mock_client, args, cli_config, browser_config, browser_config_path)
123
+ args = make_args(url="https://notfound.com")
124
+ rc = show.run(mock_client, args, cli_config, browser_config, browser_config_path)
136
125
  assert rc == 1
137
126
  assert "No entries" in capsys.readouterr().err
138
127
 
139
128
 
140
- # --- ls ---
141
-
142
- class TestLsCommand:
143
- def test_list_entries(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
144
- mock_client.get_database_entries.return_value = [mock_entry()]
145
- args = make_args(groups=False)
146
- rc = ls.run(mock_client, args, cli_config, browser_config, browser_config_path)
147
- assert rc == 0
148
- assert "Test Entry" in capsys.readouterr().out
149
-
150
- def test_list_groups(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_group):
151
- mock_client.get_database_groups.return_value = [mock_group()]
152
- args = make_args(groups=True)
153
- rc = ls.run(mock_client, args, cli_config, browser_config, browser_config_path)
154
- assert rc == 0
155
- assert "Root" in capsys.readouterr().out
156
-
157
-
158
129
  # --- add ---
159
130
 
160
131
  class TestAddCommand:
@@ -265,30 +236,6 @@ class TestClipCommand:
265
236
  assert "pyperclip" in capsys.readouterr().err
266
237
 
267
238
 
268
- # --- generate ---
269
-
270
- class TestGenerateCommand:
271
- def test_success(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
272
- mock_client.generate_password.return_value = "GenPass123!"
273
- args = make_args(clip=False)
274
- rc = generate.run(mock_client, args, cli_config, browser_config, browser_config_path)
275
- assert rc == 0
276
- assert "GenPass123!" in capsys.readouterr().out
277
-
278
- def test_failure(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
279
- mock_client.generate_password.return_value = None
280
- args = make_args(clip=False)
281
- rc = generate.run(mock_client, args, cli_config, browser_config, browser_config_path)
282
- assert rc == 1
283
-
284
- def test_clip(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
285
- mock_client.generate_password.return_value = "GenPass123!"
286
- args = make_args(clip=True)
287
- with patch.dict("sys.modules", {"pyperclip": MagicMock()}):
288
- rc = generate.run(mock_client, args, cli_config, browser_config, browser_config_path)
289
- assert rc == 0
290
-
291
-
292
239
  # --- lock ---
293
240
 
294
241
  class TestLockCommand:
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from keepassxc_browser_api import Entry
8
+ from keepassxc_cli.output import (
9
+ print_entry_detail,
10
+ print_totp,
11
+ print_status,
12
+ )
13
+
14
+
15
+ @pytest.fixture
16
+ def sample_entry():
17
+ return Entry(
18
+ uuid="abcdef12-0000-0000-0000-000000000000",
19
+ name="GitHub",
20
+ login="user@example.com",
21
+ password="s3cr3t",
22
+ totp="",
23
+ group="Root",
24
+ group_uuid="root-uuid",
25
+ string_fields=[{"KPH: url": "https://github.com"}],
26
+ )
27
+
28
+
29
+ class TestPrintEntryDetail:
30
+ def test_table_format_hidden(self, capsys, sample_entry):
31
+ print_entry_detail(sample_entry, fmt="table", show_password=False)
32
+ out = capsys.readouterr().out
33
+ assert "GitHub" in out
34
+ assert "user@example.com" in out
35
+ assert "Password" not in out
36
+ assert "s3cr3t" not in out
37
+
38
+ def test_table_format_show_password(self, capsys, sample_entry):
39
+ print_entry_detail(sample_entry, fmt="table", show_password=True)
40
+ out = capsys.readouterr().out
41
+ assert "s3cr3t" in out
42
+ assert "Password:" in out
43
+
44
+ def test_json_format_hidden(self, capsys, sample_entry):
45
+ print_entry_detail(sample_entry, fmt="json", show_password=False)
46
+ out = capsys.readouterr().out
47
+ data = json.loads(out)
48
+ assert data["name"] == "GitHub"
49
+ assert "password" not in data
50
+
51
+ def test_json_format_show_password(self, capsys, sample_entry):
52
+ print_entry_detail(sample_entry, fmt="json", show_password=True)
53
+ out = capsys.readouterr().out
54
+ data = json.loads(out)
55
+ assert data["name"] == "GitHub"
56
+ assert data["password"] == "s3cr3t"
57
+
58
+
59
+ class TestPrintTotp:
60
+ def test_table_format(self, capsys):
61
+ print_totp("123456", fmt="table")
62
+ assert capsys.readouterr().out.strip() == "123456"
63
+
64
+ def test_json_format(self, capsys):
65
+ print_totp("123456", fmt="json")
66
+ data = json.loads(capsys.readouterr().out)
67
+ assert data["totp"] == "123456"
68
+
69
+
70
+ class TestPrintStatus:
71
+ def test_table_format(self, capsys):
72
+ print_status({"Connected": "yes", "Associated": "no"}, fmt="table")
73
+ out = capsys.readouterr().out
74
+ assert "Connected: yes" in out
75
+ assert "Associated: no" in out
76
+
77
+ def test_json_format(self, capsys):
78
+ print_status({"Connected": "yes"}, fmt="json")
79
+ data = json.loads(capsys.readouterr().out)
80
+ assert data["Connected"] == "yes"
@@ -1,49 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import sys
5
- from pathlib import Path
6
-
7
- from keepassxc_browser_api import BrowserClient, BrowserConfig
8
-
9
- from keepassxc_cli.config import CliConfig
10
- from keepassxc_cli.output import print_password
11
-
12
-
13
- def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
14
- parents = [fmt_parent] if fmt_parent else []
15
- p = subparsers.add_parser(
16
- "generate",
17
- parents=parents,
18
- help="Generate a password using KeePassXC's configured password profile",
19
- )
20
- p.add_argument("--clip", action="store_true", help="Copy to clipboard instead of printing")
21
- p.set_defaults(func=run)
22
-
23
-
24
- def run(
25
- client: BrowserClient,
26
- args: argparse.Namespace,
27
- cli_config: CliConfig,
28
- browser_config: BrowserConfig,
29
- browser_config_path: Path,
30
- *,
31
- fmt: str = "table",
32
- ) -> int:
33
- password = client.generate_password()
34
- if password is None:
35
- print("Failed to generate password.", file=sys.stderr)
36
- return 1
37
-
38
- if args.clip:
39
- try:
40
- import pyperclip
41
- pyperclip.copy(password)
42
- print("Password copied to clipboard.")
43
- except ImportError:
44
- print("Error: pyperclip is required for clipboard support. Install it with: pip install pyperclip", file=sys.stderr)
45
- return 1
46
- else:
47
- print_password(password, fmt)
48
-
49
- return 0
@@ -1,43 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- from pathlib import Path
5
-
6
- from keepassxc_browser_api import BrowserClient, BrowserConfig
7
-
8
- from keepassxc_cli.config import CliConfig
9
- from keepassxc_cli.output import print_entries, print_groups
10
-
11
-
12
- def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
13
- parents = [fmt_parent] if fmt_parent else []
14
- p = subparsers.add_parser(
15
- "ls",
16
- parents=parents,
17
- help="List all database entries or groups",
18
- description=(
19
- "List all entries or groups in the open database.\n\n"
20
- "NOTE: requires 'Allow access to all entries' to be enabled in\n"
21
- "KeePassXC → Settings → Browser Integration."
22
- ),
23
- )
24
- p.add_argument("--groups", action="store_true", help="List groups instead of entries")
25
- p.set_defaults(func=run)
26
-
27
-
28
- def run(
29
- client: BrowserClient,
30
- args: argparse.Namespace,
31
- cli_config: CliConfig,
32
- browser_config: BrowserConfig,
33
- browser_config_path: Path,
34
- *,
35
- fmt: str = "table",
36
- ) -> int:
37
- if args.groups:
38
- groups = client.get_database_groups()
39
- print_groups(groups, fmt)
40
- else:
41
- entries = client.get_database_entries()
42
- print_entries(entries, fmt)
43
- return 0
@@ -1,39 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import sys
5
- from pathlib import Path
6
-
7
- from keepassxc_browser_api import BrowserClient, BrowserConfig
8
-
9
- from keepassxc_cli.config import CliConfig
10
- from keepassxc_cli.output import print_entries
11
-
12
-
13
- def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
14
- parents = [fmt_parent] if fmt_parent else []
15
- p = subparsers.add_parser(
16
- "search",
17
- parents=parents,
18
- help="Search database entries by URL or hostname",
19
- )
20
- p.add_argument("query", help="URL or hostname to search for")
21
- p.add_argument("-p", "--show-password", action="store_true", help="Reveal passwords")
22
- p.set_defaults(func=run)
23
-
24
-
25
- def run(
26
- client: BrowserClient,
27
- args: argparse.Namespace,
28
- cli_config: CliConfig,
29
- browser_config: BrowserConfig,
30
- browser_config_path: Path,
31
- *,
32
- fmt: str = "table",
33
- ) -> int:
34
- entries = client.get_logins(args.query)
35
- if not entries:
36
- print(f"No entries found matching: {args.query}", file=sys.stderr)
37
- return 1
38
- print_entries(entries, fmt, show_password=args.show_password)
39
- return 0
@@ -1,132 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
-
5
- from keepassxc_browser_api import Entry, Group
6
-
7
-
8
- def _truncate(s: str, n: int) -> str:
9
- if len(s) > n:
10
- return s[: n - 1] + "…"
11
- return s
12
-
13
-
14
- def _table_row(cols: list[str], widths: list[int]) -> str:
15
- return "| " + " | ".join(v.ljust(w) for v, w in zip(cols, widths)) + " |"
16
-
17
-
18
- def _table_sep(widths: list[int]) -> str:
19
- return "+-" + "-+-".join("-" * w for w in widths) + "-+"
20
-
21
-
22
- def _print_table(headers: list[str], rows: list[list[str]]) -> None:
23
- widths = [len(h) for h in headers]
24
- for row in rows:
25
- for i, cell in enumerate(row):
26
- widths[i] = max(widths[i], len(cell))
27
- sep = _table_sep(widths)
28
- print(sep)
29
- print(_table_row(headers, widths))
30
- print(sep)
31
- for row in rows:
32
- print(_table_row(row, widths))
33
- print(sep)
34
-
35
-
36
- def print_entries(entries: list[Entry], fmt: str = "table", show_password: bool = False) -> None:
37
- if fmt == "json":
38
- data = [
39
- {
40
- "uuid": e.uuid,
41
- "name": e.name,
42
- "login": e.login,
43
- "password": e.password if show_password else "***",
44
- "url": next((sf.get("KPH: url", "") for sf in e.string_fields if "KPH: url" in sf), ""),
45
- "group": e.group,
46
- }
47
- for e in entries
48
- ]
49
- print(json.dumps(data, indent=2))
50
- return
51
-
52
- # table
53
- headers = ["UUID", "Title", "Username", "URL", "Group"]
54
- rows = []
55
- for e in entries:
56
- url = next((sf.get("KPH: url", "") for sf in e.string_fields if "KPH: url" in sf), "")
57
- rows.append([_truncate(e.uuid, 9), e.name, e.login, url, e.group])
58
- _print_table(headers, rows)
59
-
60
-
61
- def _print_group_tree(group: Group, indent: int = 0) -> None:
62
- prefix = " " * indent
63
- print(f"{prefix}{group.name} [{_truncate(group.uuid, 9)}]")
64
- for child in group.children:
65
- _print_group_tree(child, indent + 1)
66
-
67
-
68
- def print_groups(groups: list[Group], fmt: str = "table") -> None:
69
- if fmt == "json":
70
- def _g2d(g: Group) -> dict:
71
- return {"uuid": g.uuid, "name": g.name, "children": [_g2d(c) for c in g.children]}
72
- print(json.dumps([_g2d(g) for g in groups], indent=2))
73
- return
74
-
75
- for g in groups:
76
- _print_group_tree(g)
77
-
78
-
79
- def print_entry_detail(entry: Entry, fmt: str = "table", show_password: bool = False) -> None:
80
- password = entry.password if show_password else "***"
81
- totp = entry.totp if show_password else None
82
- if fmt == "json":
83
- data = {
84
- "uuid": entry.uuid,
85
- "name": entry.name,
86
- "login": entry.login,
87
- "password": password,
88
- "group": entry.group,
89
- "group_uuid": entry.group_uuid,
90
- "string_fields": entry.string_fields,
91
- }
92
- if totp is not None:
93
- data["totp"] = totp
94
- print(json.dumps(data, indent=2))
95
- return
96
-
97
- print(f"UUID: {entry.uuid}")
98
- print(f"Title: {entry.name}")
99
- print(f"Username: {entry.login}")
100
- print(f"Password: {password}")
101
- if totp:
102
- print(f"TOTP: {totp}")
103
- if entry.group:
104
- print(f"Group: {entry.group}")
105
- if entry.group_uuid:
106
- print(f"Group UUID: {entry.group_uuid}")
107
- if entry.string_fields:
108
- for sf in entry.string_fields:
109
- for k, v in sf.items():
110
- print(f"{k}: {v}")
111
-
112
-
113
- def print_totp(totp: str, fmt: str = "table") -> None:
114
- if fmt == "json":
115
- print(json.dumps({"totp": totp}, indent=2))
116
- return
117
- print(totp)
118
-
119
-
120
- def print_password(password: str, fmt: str = "table") -> None:
121
- if fmt == "json":
122
- print(json.dumps({"password": password}, indent=2))
123
- return
124
- print(password)
125
-
126
-
127
- def print_status(info: dict, fmt: str = "table") -> None:
128
- if fmt == "json":
129
- print(json.dumps(info, indent=2))
130
- return
131
- for k, v in info.items():
132
- print(f"{k}: {v}")
@@ -1,137 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
-
5
- import pytest
6
-
7
- from keepassxc_browser_api import Entry, Group
8
- from keepassxc_cli.output import (
9
- print_entries,
10
- print_groups,
11
- print_entry_detail,
12
- print_totp,
13
- print_password,
14
- print_status,
15
- )
16
-
17
-
18
- @pytest.fixture
19
- def sample_entry():
20
- return Entry(
21
- uuid="abcdef12-0000-0000-0000-000000000000",
22
- name="GitHub",
23
- login="user@example.com",
24
- password="s3cr3t",
25
- totp="",
26
- group="Root",
27
- group_uuid="root-uuid",
28
- string_fields=[{"KPH: url": "https://github.com"}],
29
- )
30
-
31
-
32
- @pytest.fixture
33
- def sample_group():
34
- child = Group(uuid="child-uuid", name="Personal", children=[])
35
- return Group(uuid="root-uuid", name="Root", children=[child])
36
-
37
-
38
- class TestPrintEntries:
39
- def test_table_format(self, capsys, sample_entry):
40
- print_entries([sample_entry], fmt="table")
41
- out = capsys.readouterr().out
42
- assert "GitHub" in out
43
- assert "user@example.com" in out
44
- assert "Root" in out
45
- assert "abcdef12…" in out # truncated UUID (8 chars + ellipsis)
46
-
47
- def test_json_format(self, capsys, sample_entry):
48
- print_entries([sample_entry], fmt="json")
49
- out = capsys.readouterr().out
50
- data = json.loads(out)
51
- assert len(data) == 1
52
- assert data[0]["name"] == "GitHub"
53
- assert data[0]["login"] == "user@example.com"
54
- assert data[0]["password"] == "***"
55
-
56
- def test_json_format_show_password(self, capsys, sample_entry):
57
- print_entries([sample_entry], fmt="json", show_password=True)
58
- out = capsys.readouterr().out
59
- data = json.loads(out)
60
- assert data[0]["password"] == "s3cr3t"
61
-
62
- def test_table_format_multiple_entries(self, capsys, sample_entry):
63
- entries = [sample_entry, sample_entry]
64
- print_entries(entries, fmt="table")
65
- out = capsys.readouterr().out
66
- assert out.count("GitHub") == 2
67
-
68
-
69
- class TestPrintGroups:
70
- def test_table_format(self, capsys, sample_group):
71
- print_groups([sample_group], fmt="table")
72
- out = capsys.readouterr().out
73
- assert "Root" in out
74
- assert "Personal" in out
75
-
76
- def test_json_format(self, capsys, sample_group):
77
- print_groups([sample_group], fmt="json")
78
- out = capsys.readouterr().out
79
- data = json.loads(out)
80
- assert data[0]["name"] == "Root"
81
- assert data[0]["children"][0]["name"] == "Personal"
82
-
83
-
84
- class TestPrintEntryDetail:
85
- def test_table_format(self, capsys, sample_entry):
86
- print_entry_detail(sample_entry, fmt="table", show_password=False)
87
- out = capsys.readouterr().out
88
- assert "GitHub" in out
89
- assert "user@example.com" in out
90
- assert "***" in out
91
-
92
- def test_table_format_show_password(self, capsys, sample_entry):
93
- print_entry_detail(sample_entry, fmt="table", show_password=True)
94
- out = capsys.readouterr().out
95
- assert "s3cr3t" in out
96
-
97
- def test_json_format(self, capsys, sample_entry):
98
- print_entry_detail(sample_entry, fmt="json", show_password=True)
99
- out = capsys.readouterr().out
100
- data = json.loads(out)
101
- assert data["name"] == "GitHub"
102
- assert data["password"] == "s3cr3t"
103
-
104
-
105
- class TestPrintTotp:
106
- def test_table_format(self, capsys):
107
- print_totp("123456", fmt="table")
108
- assert capsys.readouterr().out.strip() == "123456"
109
-
110
- def test_json_format(self, capsys):
111
- print_totp("123456", fmt="json")
112
- data = json.loads(capsys.readouterr().out)
113
- assert data["totp"] == "123456"
114
-
115
-
116
- class TestPrintPassword:
117
- def test_table_format(self, capsys):
118
- print_password("mypass", fmt="table")
119
- assert capsys.readouterr().out.strip() == "mypass"
120
-
121
- def test_json_format(self, capsys):
122
- print_password("mypass", fmt="json")
123
- data = json.loads(capsys.readouterr().out)
124
- assert data["password"] == "mypass"
125
-
126
-
127
- class TestPrintStatus:
128
- def test_table_format(self, capsys):
129
- print_status({"Connected": "yes", "Associated": "no"}, fmt="table")
130
- out = capsys.readouterr().out
131
- assert "Connected: yes" in out
132
- assert "Associated: no" in out
133
-
134
- def test_json_format(self, capsys):
135
- print_status({"Connected": "yes"}, fmt="json")
136
- data = json.loads(capsys.readouterr().out)
137
- assert data["Connected"] == "yes"
File without changes
File without changes