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.
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.gitignore +1 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/CLAUDE.md +4 -6
- {keepassxc_cli-0.2.0/keepassxc_cli.egg-info → keepassxc_cli-1.0.0}/PKG-INFO +11 -42
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/README.md +9 -40
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/__main__.py +1 -4
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/edit.py +1 -1
- keepassxc_cli-1.0.0/keepassxc_cli/output.py +55 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0/keepassxc_cli.egg-info}/PKG-INFO +11 -42
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/SOURCES.txt +0 -3
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/requires.txt +1 -1
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/pyproject.toml +1 -1
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/tests/conftest.py +0 -3
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/tests/test_commands.py +13 -66
- keepassxc_cli-1.0.0/tests/test_output.py +80 -0
- keepassxc_cli-0.2.0/keepassxc_cli/commands/generate.py +0 -49
- keepassxc_cli-0.2.0/keepassxc_cli/commands/ls.py +0 -43
- keepassxc_cli-0.2.0/keepassxc_cli/commands/search.py +0 -39
- keepassxc_cli-0.2.0/keepassxc_cli/output.py +0 -132
- keepassxc_cli-0.2.0/tests/test_output.py +0 -137
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/auto-merge-dependabot.yml +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/auto-release.yml +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/lint_and_test.yml +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/.github/workflows/pypi.yml +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/LICENSE +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/__init__.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/__init__.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/add.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/clip.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/lock.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/mkdir.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/rm.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/setup.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/show.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/status.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/commands/totp.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli/config.py +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/dependency_links.txt +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/entry_points.txt +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/keepassxc_cli.egg-info/top_level.txt +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/setup.cfg +0 -0
- {keepassxc_cli-0.2.0 → keepassxc_cli-1.0.0}/tests/test_config.py +0 -0
|
@@ -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 —
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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,
|
|
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 '
|
|
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.
|
|
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.
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
@@ -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,
|
|
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,
|
|
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
|
|
114
|
-
|
|
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(
|
|
128
|
-
rc =
|
|
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
|
-
|
|
117
|
+
out = capsys.readouterr().out
|
|
118
|
+
assert "Test Entry" in out
|
|
119
|
+
assert "Password:" in out
|
|
131
120
|
|
|
132
|
-
def
|
|
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(
|
|
135
|
-
rc =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|