keepassxc-cli 1.7.0__tar.gz → 2.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-1.7.0 → keepassxc_cli-2.0.0}/CLAUDE.md +4 -4
- {keepassxc_cli-1.7.0/keepassxc_cli.egg-info → keepassxc_cli-2.0.0}/PKG-INFO +16 -15
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/README.md +15 -14
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/__main__.py +6 -6
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/add.py +7 -6
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/clip.py +4 -3
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/edit.py +8 -6
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/lock.py +5 -3
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/mkdir.py +9 -3
- keepassxc_cli-2.0.0/keepassxc_cli/commands/rm.py +63 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/output.py +7 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0/keepassxc_cli.egg-info}/PKG-INFO +16 -15
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/test_commands.py +24 -10
- keepassxc_cli-1.7.0/keepassxc_cli/commands/rm.py +0 -60
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/auto-merge-dependabot.yml +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/auto-release.yml +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/lint_and_test.yml +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/pypi.yml +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.gitignore +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/LICENSE +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/assets/demo.gif +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/assets/settings-browser-integration.png +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/__init__.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/__init__.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/group_uuid.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/setup.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/show.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/status.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/totp.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/version.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/config.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/SOURCES.txt +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/dependency_links.txt +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/entry_points.txt +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/requires.txt +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/top_level.txt +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/pyproject.toml +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/setup.cfg +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/conftest.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/test_config.py +0 -0
- {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/test_output.py +0 -0
|
@@ -19,11 +19,11 @@ keepassxc_cli/
|
|
|
19
19
|
├── setup.py # associate with KeePassXC
|
|
20
20
|
├── status.py # show connection/association status
|
|
21
21
|
├── show.py # show entries by URL
|
|
22
|
-
├── add.py # add new entry
|
|
23
|
-
├── edit.py # edit existing entry
|
|
24
|
-
├── rm.py # delete entry
|
|
22
|
+
├── add.py # add new entry: positional url/username, --group/--group-uuid optional
|
|
23
|
+
├── edit.py # edit existing entry: positional url, --uuid/--username/--password optional
|
|
24
|
+
├── rm.py # delete entry: positional url, --uuid optional for disambiguation
|
|
25
25
|
├── totp.py # get TOTP code
|
|
26
|
-
├── clip.py # copy field to clipboard
|
|
26
|
+
├── clip.py # copy field to clipboard: positional url then field
|
|
27
27
|
├── lock.py # lock database
|
|
28
28
|
├── mkdir.py # create group
|
|
29
29
|
├── group_uuid.py # look up a group UUID by path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keepassxc-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.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
|
|
@@ -121,20 +121,20 @@ keepassxc-cli totp https://github.com -j
|
|
|
121
121
|
#### `clip` — Copy a field to clipboard
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
|
-
keepassxc-cli clip
|
|
125
|
-
keepassxc-cli clip
|
|
126
|
-
keepassxc-cli clip
|
|
124
|
+
keepassxc-cli clip https://github.com password
|
|
125
|
+
keepassxc-cli clip https://github.com username
|
|
126
|
+
keepassxc-cli clip https://github.com totp
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
#### `add` — Add a new entry
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
# Password is prompted securely if --password is not given
|
|
133
|
-
keepassxc-cli add
|
|
134
|
-
keepassxc-cli add
|
|
133
|
+
keepassxc-cli add https://example.com user@example.com
|
|
134
|
+
keepassxc-cli add https://example.com user --password mypass
|
|
135
135
|
# Place the entry in a specific group by UUID or by path
|
|
136
|
-
keepassxc-cli add
|
|
137
|
-
keepassxc-cli add
|
|
136
|
+
keepassxc-cli add https://example.com user --group-uuid <group-uuid>
|
|
137
|
+
keepassxc-cli add https://example.com user --group "Work/Projects"
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
> **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
|
|
@@ -142,19 +142,20 @@ keepassxc-cli add --url https://example.com --username user --group "Work/Projec
|
|
|
142
142
|
#### `edit` — Edit an entry
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
|
-
#
|
|
146
|
-
keepassxc-cli edit
|
|
147
|
-
keepassxc-cli edit
|
|
145
|
+
# URL is positional; --uuid is optional when the URL matches exactly one entry
|
|
146
|
+
keepassxc-cli edit https://github.com --username newuser
|
|
147
|
+
keepassxc-cli edit https://github.com --password newpass
|
|
148
148
|
# Specify --uuid explicitly when the URL matches multiple entries
|
|
149
|
-
keepassxc-cli edit
|
|
149
|
+
keepassxc-cli edit https://github.com --uuid <uuid> --username newuser
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
#### `rm` — Delete an entry
|
|
153
153
|
|
|
154
154
|
```bash
|
|
155
|
-
keepassxc-cli rm
|
|
156
|
-
keepassxc-cli rm
|
|
157
|
-
|
|
155
|
+
keepassxc-cli rm https://example.com # prompts for confirmation
|
|
156
|
+
keepassxc-cli rm https://example.com --yes # skip confirmation
|
|
157
|
+
# Specify --uuid when URL matches multiple entries
|
|
158
|
+
keepassxc-cli rm https://example.com --uuid <uuid> --yes
|
|
158
159
|
```
|
|
159
160
|
|
|
160
161
|
#### `lock` — Lock the database
|
|
@@ -106,20 +106,20 @@ keepassxc-cli totp https://github.com -j
|
|
|
106
106
|
#### `clip` — Copy a field to clipboard
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
keepassxc-cli clip
|
|
110
|
-
keepassxc-cli clip
|
|
111
|
-
keepassxc-cli clip
|
|
109
|
+
keepassxc-cli clip https://github.com password
|
|
110
|
+
keepassxc-cli clip https://github.com username
|
|
111
|
+
keepassxc-cli clip https://github.com totp
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
#### `add` — Add a new entry
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
117
|
# Password is prompted securely if --password is not given
|
|
118
|
-
keepassxc-cli add
|
|
119
|
-
keepassxc-cli add
|
|
118
|
+
keepassxc-cli add https://example.com user@example.com
|
|
119
|
+
keepassxc-cli add https://example.com user --password mypass
|
|
120
120
|
# Place the entry in a specific group by UUID or by path
|
|
121
|
-
keepassxc-cli add
|
|
122
|
-
keepassxc-cli add
|
|
121
|
+
keepassxc-cli add https://example.com user --group-uuid <group-uuid>
|
|
122
|
+
keepassxc-cli add https://example.com user --group "Work/Projects"
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
> **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
|
|
@@ -127,19 +127,20 @@ keepassxc-cli add --url https://example.com --username user --group "Work/Projec
|
|
|
127
127
|
#### `edit` — Edit an entry
|
|
128
128
|
|
|
129
129
|
```bash
|
|
130
|
-
#
|
|
131
|
-
keepassxc-cli edit
|
|
132
|
-
keepassxc-cli edit
|
|
130
|
+
# URL is positional; --uuid is optional when the URL matches exactly one entry
|
|
131
|
+
keepassxc-cli edit https://github.com --username newuser
|
|
132
|
+
keepassxc-cli edit https://github.com --password newpass
|
|
133
133
|
# Specify --uuid explicitly when the URL matches multiple entries
|
|
134
|
-
keepassxc-cli edit
|
|
134
|
+
keepassxc-cli edit https://github.com --uuid <uuid> --username newuser
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
#### `rm` — Delete an entry
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
keepassxc-cli rm
|
|
141
|
-
keepassxc-cli rm
|
|
142
|
-
|
|
140
|
+
keepassxc-cli rm https://example.com # prompts for confirmation
|
|
141
|
+
keepassxc-cli rm https://example.com --yes # skip confirmation
|
|
142
|
+
# Specify --uuid when URL matches multiple entries
|
|
143
|
+
keepassxc-cli rm https://example.com --uuid <uuid> --yes
|
|
143
144
|
```
|
|
144
145
|
|
|
145
146
|
#### `lock` — Lock the database
|
|
@@ -48,13 +48,13 @@ 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
|
-
add.add_parser(subparsers)
|
|
52
|
-
edit.add_parser(subparsers)
|
|
53
|
-
rm.add_parser(subparsers)
|
|
51
|
+
add.add_parser(subparsers, fmt_parent)
|
|
52
|
+
edit.add_parser(subparsers, fmt_parent)
|
|
53
|
+
rm.add_parser(subparsers, fmt_parent)
|
|
54
54
|
totp.add_parser(subparsers, fmt_parent)
|
|
55
|
-
clip.add_parser(subparsers)
|
|
56
|
-
lock.add_parser(subparsers)
|
|
57
|
-
mkdir.add_parser(subparsers)
|
|
55
|
+
clip.add_parser(subparsers, fmt_parent)
|
|
56
|
+
lock.add_parser(subparsers, fmt_parent)
|
|
57
|
+
mkdir.add_parser(subparsers, fmt_parent)
|
|
58
58
|
group_uuid.add_parser(subparsers, fmt_parent)
|
|
59
59
|
version.add_parser(subparsers)
|
|
60
60
|
|
|
@@ -8,15 +8,16 @@ from pathlib import Path
|
|
|
8
8
|
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
9
9
|
|
|
10
10
|
from keepassxc_cli.config import CliConfig
|
|
11
|
-
from keepassxc_cli.output import ensure_scheme
|
|
11
|
+
from keepassxc_cli.output import ensure_scheme, print_result
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
17
|
-
|
|
18
|
-
p.
|
|
19
|
-
p.add_argument("
|
|
16
|
+
def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
|
|
17
|
+
parents = [fmt_parent] if fmt_parent else []
|
|
18
|
+
p = subparsers.add_parser("add", parents=parents, help="Add a new entry")
|
|
19
|
+
p.add_argument("url", help="Entry URL")
|
|
20
|
+
p.add_argument("username", help="Username")
|
|
20
21
|
p.add_argument("--password", default=None, help="Password (prompted if omitted)")
|
|
21
22
|
group = p.add_mutually_exclusive_group()
|
|
22
23
|
group.add_argument("--group-uuid", default="", help="Target group UUID")
|
|
@@ -60,5 +61,5 @@ def run(
|
|
|
60
61
|
password=password,
|
|
61
62
|
group_uuid=group_uuid,
|
|
62
63
|
)
|
|
63
|
-
|
|
64
|
+
print_result("Entry added.", fmt)
|
|
64
65
|
return 0
|
|
@@ -12,14 +12,15 @@ from keepassxc_cli.output import ensure_scheme
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
-
|
|
15
|
+
def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
|
|
16
|
+
parents = [fmt_parent] if fmt_parent else []
|
|
17
|
+
p = subparsers.add_parser("clip", parents=parents, help="Copy a field to clipboard")
|
|
18
|
+
p.add_argument("url", help="URL to look up")
|
|
17
19
|
p.add_argument(
|
|
18
20
|
"field",
|
|
19
21
|
choices=["password", "username", "totp"],
|
|
20
22
|
help="Field to copy: password, username, or totp",
|
|
21
23
|
)
|
|
22
|
-
p.add_argument("url", help="URL to look up")
|
|
23
24
|
p.set_defaults(func=run)
|
|
24
25
|
|
|
25
26
|
|
|
@@ -7,21 +7,23 @@ from pathlib import Path
|
|
|
7
7
|
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
8
|
|
|
9
9
|
from keepassxc_cli.config import CliConfig
|
|
10
|
-
from keepassxc_cli.output import ensure_scheme
|
|
10
|
+
from keepassxc_cli.output import ensure_scheme, print_result
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
15
|
+
def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
|
|
16
|
+
parents = [fmt_parent] if fmt_parent else []
|
|
16
17
|
p = subparsers.add_parser(
|
|
17
18
|
"edit",
|
|
19
|
+
parents=parents,
|
|
18
20
|
help="Edit an existing entry",
|
|
19
21
|
description=(
|
|
20
|
-
"Edit an existing entry
|
|
21
|
-
"
|
|
22
|
+
"Edit an existing entry by URL. Omitted fields are left unchanged.\n"
|
|
23
|
+
"If the URL matches multiple entries, specify --uuid to disambiguate."
|
|
22
24
|
),
|
|
23
25
|
)
|
|
24
|
-
p.add_argument("
|
|
26
|
+
p.add_argument("url", help="URL of the entry")
|
|
25
27
|
p.add_argument("--uuid", default=None, help="UUID of the entry (required when URL matches multiple entries)")
|
|
26
28
|
p.add_argument("--username", default=None, help="New username")
|
|
27
29
|
p.add_argument("--password", default=None, help="New password")
|
|
@@ -70,5 +72,5 @@ def run(
|
|
|
70
72
|
uuid=entry.uuid,
|
|
71
73
|
group_uuid=entry.group_uuid,
|
|
72
74
|
)
|
|
73
|
-
|
|
75
|
+
print_result("Entry updated.", fmt)
|
|
74
76
|
return 0
|
|
@@ -7,12 +7,14 @@ from pathlib import Path
|
|
|
7
7
|
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
8
|
|
|
9
9
|
from keepassxc_cli.config import CliConfig
|
|
10
|
+
from keepassxc_cli.output import print_result
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
15
|
-
|
|
15
|
+
def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
|
|
16
|
+
parents = [fmt_parent] if fmt_parent else []
|
|
17
|
+
p = subparsers.add_parser("lock", parents=parents, help="Lock the KeePassXC database")
|
|
16
18
|
p.set_defaults(func=run)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -27,7 +29,7 @@ def run(
|
|
|
27
29
|
) -> int:
|
|
28
30
|
success = client.lock_database()
|
|
29
31
|
if success:
|
|
30
|
-
|
|
32
|
+
print_result("Database locked.", fmt)
|
|
31
33
|
return 0
|
|
32
34
|
else:
|
|
33
35
|
logger.error("Failed to lock database.")
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from keepassxc_browser_api import BrowserClient, BrowserConfig
|
|
8
9
|
|
|
9
10
|
from keepassxc_cli.config import CliConfig
|
|
11
|
+
from keepassxc_cli.output import print_result
|
|
10
12
|
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
15
|
-
|
|
16
|
+
def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
|
|
17
|
+
parents = [fmt_parent] if fmt_parent else []
|
|
18
|
+
p = subparsers.add_parser("mkdir", parents=parents, help="Create a new group")
|
|
16
19
|
p.add_argument(
|
|
17
20
|
"name",
|
|
18
21
|
help="Group name or path. Use '/' to create nested groups (e.g. 'Work/Projects').",
|
|
@@ -30,5 +33,8 @@ def run(
|
|
|
30
33
|
fmt: str = "table",
|
|
31
34
|
) -> int:
|
|
32
35
|
group = client.create_group(args.name)
|
|
33
|
-
|
|
36
|
+
if fmt == "json":
|
|
37
|
+
print(json.dumps({"name": group.name, "uuid": group.uuid}, indent=2))
|
|
38
|
+
else:
|
|
39
|
+
print_result(f"Group created: {group.name} [{group.uuid}]", fmt)
|
|
34
40
|
return 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
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 ensure_scheme, print_result
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_parser(subparsers: argparse._SubParsersAction, fmt_parent: argparse.ArgumentParser | None = None) -> None:
|
|
16
|
+
parents = [fmt_parent] if fmt_parent else []
|
|
17
|
+
p = subparsers.add_parser("rm", parents=parents, help="Delete an entry")
|
|
18
|
+
p.add_argument("url", help="URL of the entry to delete")
|
|
19
|
+
p.add_argument("--uuid", default=None, help="UUID to disambiguate when URL matches multiple entries")
|
|
20
|
+
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
|
|
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
|
+
entries = client.get_logins(ensure_scheme(args.url))
|
|
34
|
+
if not entries:
|
|
35
|
+
logger.error("No entries found for: %s", args.url)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
if args.uuid is not None:
|
|
39
|
+
entry = next((e for e in entries if e.uuid == args.uuid), None)
|
|
40
|
+
if entry is None:
|
|
41
|
+
logger.error("Entry %s not found for URL: %s", args.uuid, args.url)
|
|
42
|
+
return 1
|
|
43
|
+
elif len(entries) == 1:
|
|
44
|
+
entry = entries[0]
|
|
45
|
+
else:
|
|
46
|
+
logger.error(
|
|
47
|
+
"Multiple entries found for %s \u2014 specify --uuid to disambiguate:",
|
|
48
|
+
args.url,
|
|
49
|
+
)
|
|
50
|
+
for e in entries:
|
|
51
|
+
print(f" {e.uuid} {e.login} ({e.name})")
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
label = f"{entry.name} ({args.url})"
|
|
55
|
+
if not args.yes:
|
|
56
|
+
answer = input(f"Delete entry {label}? [y/N] ").strip().lower()
|
|
57
|
+
if answer != "y":
|
|
58
|
+
print("Aborted.")
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
client.delete_entry(entry.uuid)
|
|
62
|
+
print_result("Entry deleted.", fmt)
|
|
63
|
+
return 0
|
|
@@ -75,6 +75,13 @@ def print_totp(totp: str, fmt: str = "table") -> None:
|
|
|
75
75
|
print(totp)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def print_result(message: str, fmt: str = "table") -> None:
|
|
79
|
+
if fmt == "json":
|
|
80
|
+
print(json.dumps({"status": "ok", "message": message}, indent=2))
|
|
81
|
+
return
|
|
82
|
+
print(message)
|
|
83
|
+
|
|
84
|
+
|
|
78
85
|
def print_status(info: dict, fmt: str = "table") -> None:
|
|
79
86
|
if fmt == "json":
|
|
80
87
|
print(json.dumps(info, indent=2))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keepassxc-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.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
|
|
@@ -121,20 +121,20 @@ keepassxc-cli totp https://github.com -j
|
|
|
121
121
|
#### `clip` — Copy a field to clipboard
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
|
-
keepassxc-cli clip
|
|
125
|
-
keepassxc-cli clip
|
|
126
|
-
keepassxc-cli clip
|
|
124
|
+
keepassxc-cli clip https://github.com password
|
|
125
|
+
keepassxc-cli clip https://github.com username
|
|
126
|
+
keepassxc-cli clip https://github.com totp
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
#### `add` — Add a new entry
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
# Password is prompted securely if --password is not given
|
|
133
|
-
keepassxc-cli add
|
|
134
|
-
keepassxc-cli add
|
|
133
|
+
keepassxc-cli add https://example.com user@example.com
|
|
134
|
+
keepassxc-cli add https://example.com user --password mypass
|
|
135
135
|
# Place the entry in a specific group by UUID or by path
|
|
136
|
-
keepassxc-cli add
|
|
137
|
-
keepassxc-cli add
|
|
136
|
+
keepassxc-cli add https://example.com user --group-uuid <group-uuid>
|
|
137
|
+
keepassxc-cli add https://example.com user --group "Work/Projects"
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
> **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
|
|
@@ -142,19 +142,20 @@ keepassxc-cli add --url https://example.com --username user --group "Work/Projec
|
|
|
142
142
|
#### `edit` — Edit an entry
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
|
-
#
|
|
146
|
-
keepassxc-cli edit
|
|
147
|
-
keepassxc-cli edit
|
|
145
|
+
# URL is positional; --uuid is optional when the URL matches exactly one entry
|
|
146
|
+
keepassxc-cli edit https://github.com --username newuser
|
|
147
|
+
keepassxc-cli edit https://github.com --password newpass
|
|
148
148
|
# Specify --uuid explicitly when the URL matches multiple entries
|
|
149
|
-
keepassxc-cli edit
|
|
149
|
+
keepassxc-cli edit https://github.com --uuid <uuid> --username newuser
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
#### `rm` — Delete an entry
|
|
153
153
|
|
|
154
154
|
```bash
|
|
155
|
-
keepassxc-cli rm
|
|
156
|
-
keepassxc-cli rm
|
|
157
|
-
|
|
155
|
+
keepassxc-cli rm https://example.com # prompts for confirmation
|
|
156
|
+
keepassxc-cli rm https://example.com --yes # skip confirmation
|
|
157
|
+
# Specify --uuid when URL matches multiple entries
|
|
158
|
+
keepassxc-cli rm https://example.com --uuid <uuid> --yes
|
|
158
159
|
```
|
|
159
160
|
|
|
160
161
|
#### `lock` — Lock the database
|
|
@@ -44,6 +44,7 @@ def make_args(**kwargs) -> argparse.Namespace:
|
|
|
44
44
|
"uuid": "abcdef12-0000-0000-0000-000000000000",
|
|
45
45
|
"name": "NewGroup",
|
|
46
46
|
"path": "Work",
|
|
47
|
+
"json_output": False,
|
|
47
48
|
}
|
|
48
49
|
defaults.update(kwargs)
|
|
49
50
|
return argparse.Namespace(**defaults)
|
|
@@ -246,22 +247,25 @@ class TestEditCommand:
|
|
|
246
247
|
# --- rm ---
|
|
247
248
|
|
|
248
249
|
class TestRmCommand:
|
|
249
|
-
def
|
|
250
|
+
def test_url_single_match_auto_deletes(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
|
|
251
|
+
entry = mock_entry(uuid="url-resolved-uuid")
|
|
252
|
+
mock_client.get_logins.return_value = [entry]
|
|
250
253
|
mock_client.delete_entry.return_value = True
|
|
251
|
-
args = make_args(
|
|
254
|
+
args = make_args(url="https://example.com", uuid=None, yes=True)
|
|
252
255
|
rc = rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
253
256
|
assert rc == 0
|
|
254
|
-
mock_client.delete_entry.assert_called_once_with("
|
|
257
|
+
mock_client.delete_entry.assert_called_once_with("url-resolved-uuid")
|
|
255
258
|
assert "deleted" in capsys.readouterr().out.lower()
|
|
256
259
|
|
|
257
|
-
def
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
def test_url_with_uuid_disambiguates(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
|
|
261
|
+
e1 = mock_entry(uuid="uuid-1", login="alice")
|
|
262
|
+
e2 = mock_entry(uuid="uuid-2", login="bob")
|
|
263
|
+
mock_client.get_logins.return_value = [e1, e2]
|
|
260
264
|
mock_client.delete_entry.return_value = True
|
|
261
|
-
args = make_args(
|
|
265
|
+
args = make_args(url="https://example.com", uuid="uuid-2", yes=True)
|
|
262
266
|
rc = rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
263
267
|
assert rc == 0
|
|
264
|
-
mock_client.delete_entry.assert_called_once_with("
|
|
268
|
+
mock_client.delete_entry.assert_called_once_with("uuid-2")
|
|
265
269
|
|
|
266
270
|
def test_url_multiple_matches_error(self, mock_client, cli_config, browser_config, browser_config_path, caplog, mock_entry):
|
|
267
271
|
e1 = mock_entry(uuid="uuid-1", login="alice")
|
|
@@ -279,9 +283,19 @@ class TestRmCommand:
|
|
|
279
283
|
assert rc == 1
|
|
280
284
|
assert any("no entries" in r.message.lower() for r in caplog.records)
|
|
281
285
|
|
|
282
|
-
def
|
|
286
|
+
def test_uuid_not_in_results_error(self, mock_client, cli_config, browser_config, browser_config_path, caplog, mock_entry):
|
|
287
|
+
entry = mock_entry(uuid="uuid-1")
|
|
288
|
+
mock_client.get_logins.return_value = [entry]
|
|
289
|
+
args = make_args(url="https://example.com", uuid="nonexistent-uuid", yes=True)
|
|
290
|
+
rc = rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
291
|
+
assert rc == 1
|
|
292
|
+
assert any("not found" in r.message.lower() for r in caplog.records)
|
|
293
|
+
|
|
294
|
+
def test_failure_propagates(self, mock_client, cli_config, browser_config, browser_config_path, mock_entry):
|
|
295
|
+
entry = mock_entry(uuid="some-uuid")
|
|
296
|
+
mock_client.get_logins.return_value = [entry]
|
|
283
297
|
mock_client.delete_entry.side_effect = ProtocolError("access denied", error_code=6)
|
|
284
|
-
args = make_args(
|
|
298
|
+
args = make_args(url="https://example.com", uuid=None, yes=True)
|
|
285
299
|
with pytest.raises(ProtocolError):
|
|
286
300
|
rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
287
301
|
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import logging
|
|
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 ensure_scheme
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
-
p = subparsers.add_parser("rm", help="Delete an entry")
|
|
17
|
-
group = p.add_mutually_exclusive_group(required=True)
|
|
18
|
-
group.add_argument("--uuid", default=None, help="UUID of the entry to delete")
|
|
19
|
-
group.add_argument("--url", default=None, help="URL of the entry to delete (must match exactly one entry)")
|
|
20
|
-
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
|
|
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
|
-
if args.uuid is not None:
|
|
34
|
-
target_uuid = args.uuid
|
|
35
|
-
label = args.uuid
|
|
36
|
-
else:
|
|
37
|
-
entries = client.get_logins(ensure_scheme(args.url))
|
|
38
|
-
if not entries:
|
|
39
|
-
logger.error("No entries found for: %s", args.url)
|
|
40
|
-
return 1
|
|
41
|
-
if len(entries) > 1:
|
|
42
|
-
logger.error(
|
|
43
|
-
"Multiple entries found for %s \u2014 specify --uuid to disambiguate:",
|
|
44
|
-
args.url,
|
|
45
|
-
)
|
|
46
|
-
for e in entries:
|
|
47
|
-
print(f" {e.uuid} {e.login} ({e.name})")
|
|
48
|
-
return 1
|
|
49
|
-
target_uuid = entries[0].uuid
|
|
50
|
-
label = f"{entries[0].name} ({args.url})"
|
|
51
|
-
|
|
52
|
-
if not args.yes:
|
|
53
|
-
answer = input(f"Delete entry {label}? [y/N] ").strip().lower()
|
|
54
|
-
if answer != "y":
|
|
55
|
-
print("Aborted.")
|
|
56
|
-
return 1
|
|
57
|
-
|
|
58
|
-
client.delete_entry(target_uuid)
|
|
59
|
-
print("Entry deleted.")
|
|
60
|
-
return 0
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|