keepassxc-cli 1.6.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.6.0 → keepassxc_cli-2.0.0}/CLAUDE.md +9 -6
- {keepassxc_cli-1.6.0/keepassxc_cli.egg-info → keepassxc_cli-2.0.0}/PKG-INFO +20 -16
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/README.md +19 -15
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/__main__.py +6 -6
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/add.py +9 -6
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/clip.py +6 -4
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/edit.py +11 -7
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/lock.py +5 -3
- {keepassxc_cli-1.6.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.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/show.py +4 -3
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/totp.py +2 -2
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/output.py +18 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0/keepassxc_cli.egg-info}/PKG-INFO +20 -16
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/tests/test_commands.py +42 -10
- keepassxc_cli-1.6.0/keepassxc_cli/commands/rm.py +0 -59
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/.github/workflows/auto-merge-dependabot.yml +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/.github/workflows/auto-release.yml +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/.github/workflows/lint_and_test.yml +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/.github/workflows/pypi.yml +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/.gitignore +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/LICENSE +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/assets/demo.gif +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/assets/settings-browser-integration.png +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/__init__.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/__init__.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/group_uuid.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/setup.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/status.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/version.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli/config.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/SOURCES.txt +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/dependency_links.txt +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/entry_points.txt +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/requires.txt +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/top_level.txt +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/pyproject.toml +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/setup.cfg +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/tests/conftest.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/tests/test_config.py +0 -0
- {keepassxc_cli-1.6.0 → keepassxc_cli-2.0.0}/tests/test_output.py +0 -0
|
@@ -13,19 +13,21 @@ 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; ensure_scheme() URL helper
|
|
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
|
-
├── 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
|
+
├── group_uuid.py # look up a group UUID by path
|
|
30
|
+
└── version.py # print installed package version
|
|
29
31
|
|
|
30
32
|
tests/
|
|
31
33
|
├── conftest.py # fixtures: mock_entry, mock_group, mock_browser_config, mock_client
|
|
@@ -109,6 +111,7 @@ ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_cli
|
|
|
109
111
|
| `4` | `ProtocolError(error_code=6 or 19)` — access denied by user |
|
|
110
112
|
|
|
111
113
|
- **Config permissions**: Config files are written with `0o600` (owner read/write only).
|
|
114
|
+
- **URL normalisation**: All URL-accepting command `run()` functions call `ensure_scheme(url)` (from `output.py`) before passing the URL to any `BrowserClient` method. This auto-prepends `https://` for bare hostnames (e.g. `example.com`) and emits a `logger.warning`. KeePassXC derives the entry title from `QUrl(url).host()`, which returns `""` for URLs without a scheme.
|
|
112
115
|
- **Venv**: Always use `.venv` for development.
|
|
113
116
|
- **Python ≥ 3.10** required.
|
|
114
117
|
- **Password visibility**: `show` omits password and TOTP entirely when `-p` is not passed (no masking).
|
|
@@ -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
|
|
@@ -86,6 +86,8 @@ keepassxc-cli status -j
|
|
|
86
86
|
|
|
87
87
|
### Commands
|
|
88
88
|
|
|
89
|
+
> **URL scheme**: All commands that accept a URL argument require a scheme (`https://` or `http://`). If you pass a bare hostname (e.g. `example.com`), the CLI will automatically prepend `https://` and emit a warning. KeePassXC derives the entry title from `QUrl(url).host()`, which returns an empty string for URLs without a scheme.
|
|
90
|
+
|
|
89
91
|
#### `setup` — Associate with KeePassXC
|
|
90
92
|
|
|
91
93
|
```bash
|
|
@@ -119,19 +121,20 @@ keepassxc-cli totp https://github.com -j
|
|
|
119
121
|
#### `clip` — Copy a field to clipboard
|
|
120
122
|
|
|
121
123
|
```bash
|
|
122
|
-
keepassxc-cli clip
|
|
123
|
-
keepassxc-cli clip
|
|
124
|
-
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
|
|
125
127
|
```
|
|
126
128
|
|
|
127
129
|
#### `add` — Add a new entry
|
|
128
130
|
|
|
129
131
|
```bash
|
|
130
132
|
# Password is prompted securely if --password is not given
|
|
131
|
-
keepassxc-cli add
|
|
132
|
-
keepassxc-cli add
|
|
133
|
-
# Place the entry in a specific group by UUID
|
|
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
|
+
# Place the entry in a specific group by UUID or by path
|
|
136
|
+
keepassxc-cli add https://example.com user --group-uuid <group-uuid>
|
|
137
|
+
keepassxc-cli add https://example.com user --group "Work/Projects"
|
|
135
138
|
```
|
|
136
139
|
|
|
137
140
|
> **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
|
|
@@ -139,19 +142,20 @@ keepassxc-cli add --url https://example.com --username user --group-uuid <group-
|
|
|
139
142
|
#### `edit` — Edit an entry
|
|
140
143
|
|
|
141
144
|
```bash
|
|
142
|
-
#
|
|
143
|
-
keepassxc-cli
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
keepassxc-cli edit
|
|
147
|
-
keepassxc-cli edit <uuid> --url https://github.com --password newpass --title "New Title"
|
|
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
|
+
# Specify --uuid explicitly when the URL matches multiple entries
|
|
149
|
+
keepassxc-cli edit https://github.com --uuid <uuid> --username newuser
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
#### `rm` — Delete an entry
|
|
151
153
|
|
|
152
154
|
```bash
|
|
153
|
-
keepassxc-cli rm
|
|
154
|
-
keepassxc-cli rm
|
|
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
|
|
155
159
|
```
|
|
156
160
|
|
|
157
161
|
#### `lock` — Lock the database
|
|
@@ -71,6 +71,8 @@ keepassxc-cli status -j
|
|
|
71
71
|
|
|
72
72
|
### Commands
|
|
73
73
|
|
|
74
|
+
> **URL scheme**: All commands that accept a URL argument require a scheme (`https://` or `http://`). If you pass a bare hostname (e.g. `example.com`), the CLI will automatically prepend `https://` and emit a warning. KeePassXC derives the entry title from `QUrl(url).host()`, which returns an empty string for URLs without a scheme.
|
|
75
|
+
|
|
74
76
|
#### `setup` — Associate with KeePassXC
|
|
75
77
|
|
|
76
78
|
```bash
|
|
@@ -104,19 +106,20 @@ keepassxc-cli totp https://github.com -j
|
|
|
104
106
|
#### `clip` — Copy a field to clipboard
|
|
105
107
|
|
|
106
108
|
```bash
|
|
107
|
-
keepassxc-cli clip
|
|
108
|
-
keepassxc-cli clip
|
|
109
|
-
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
|
|
110
112
|
```
|
|
111
113
|
|
|
112
114
|
#### `add` — Add a new entry
|
|
113
115
|
|
|
114
116
|
```bash
|
|
115
117
|
# Password is prompted securely if --password is not given
|
|
116
|
-
keepassxc-cli add
|
|
117
|
-
keepassxc-cli add
|
|
118
|
-
# Place the entry in a specific group by UUID
|
|
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
|
+
# Place the entry in a specific group by UUID or by path
|
|
121
|
+
keepassxc-cli add https://example.com user --group-uuid <group-uuid>
|
|
122
|
+
keepassxc-cli add https://example.com user --group "Work/Projects"
|
|
120
123
|
```
|
|
121
124
|
|
|
122
125
|
> **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
|
|
@@ -124,19 +127,20 @@ keepassxc-cli add --url https://example.com --username user --group-uuid <group-
|
|
|
124
127
|
#### `edit` — Edit an entry
|
|
125
128
|
|
|
126
129
|
```bash
|
|
127
|
-
#
|
|
128
|
-
keepassxc-cli
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
keepassxc-cli edit
|
|
132
|
-
keepassxc-cli edit <uuid> --url https://github.com --password newpass --title "New Title"
|
|
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
|
+
# Specify --uuid explicitly when the URL matches multiple entries
|
|
134
|
+
keepassxc-cli edit https://github.com --uuid <uuid> --username newuser
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
#### `rm` — Delete an entry
|
|
136
138
|
|
|
137
139
|
```bash
|
|
138
|
-
keepassxc-cli rm
|
|
139
|
-
keepassxc-cli rm
|
|
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
|
|
140
144
|
```
|
|
141
145
|
|
|
142
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,14 +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, print_result
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
-
|
|
17
|
-
p.
|
|
18
|
-
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")
|
|
19
21
|
p.add_argument("--password", default=None, help="Password (prompted if omitted)")
|
|
20
22
|
group = p.add_mutually_exclusive_group()
|
|
21
23
|
group.add_argument("--group-uuid", default="", help="Target group UUID")
|
|
@@ -32,6 +34,7 @@ def run(
|
|
|
32
34
|
*,
|
|
33
35
|
fmt: str = "table",
|
|
34
36
|
) -> int:
|
|
37
|
+
url = ensure_scheme(args.url)
|
|
35
38
|
password = args.password
|
|
36
39
|
if password is None:
|
|
37
40
|
password = getpass.getpass("Password: ")
|
|
@@ -53,10 +56,10 @@ def run(
|
|
|
53
56
|
group_uuid = matched.uuid
|
|
54
57
|
|
|
55
58
|
client.set_login(
|
|
56
|
-
url=
|
|
59
|
+
url=url,
|
|
57
60
|
username=args.username,
|
|
58
61
|
password=password,
|
|
59
62
|
group_uuid=group_uuid,
|
|
60
63
|
)
|
|
61
|
-
|
|
64
|
+
print_result("Entry added.", fmt)
|
|
62
65
|
return 0
|
|
@@ -7,18 +7,20 @@ 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
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("clip", parents=parents, help="Copy a field to clipboard")
|
|
18
|
+
p.add_argument("url", help="URL to look up")
|
|
16
19
|
p.add_argument(
|
|
17
20
|
"field",
|
|
18
21
|
choices=["password", "username", "totp"],
|
|
19
22
|
help="Field to copy: password, username, or totp",
|
|
20
23
|
)
|
|
21
|
-
p.add_argument("url", help="URL to look up")
|
|
22
24
|
p.set_defaults(func=run)
|
|
23
25
|
|
|
24
26
|
|
|
@@ -37,7 +39,7 @@ def run(
|
|
|
37
39
|
logger.error("pyperclip is required for clipboard support. Install it with: pip install pyperclip")
|
|
38
40
|
return 1
|
|
39
41
|
|
|
40
|
-
entries = client.get_logins(args.url)
|
|
42
|
+
entries = client.get_logins(ensure_scheme(args.url))
|
|
41
43
|
if not entries:
|
|
42
44
|
logger.warning("No entries found for: %s", args.url)
|
|
43
45
|
return 1
|
|
@@ -7,20 +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, print_result
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
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 []
|
|
15
17
|
p = subparsers.add_parser(
|
|
16
18
|
"edit",
|
|
19
|
+
parents=parents,
|
|
17
20
|
help="Edit an existing entry",
|
|
18
21
|
description=(
|
|
19
|
-
"Edit an existing entry
|
|
20
|
-
"
|
|
22
|
+
"Edit an existing entry by URL. Omitted fields are left unchanged.\n"
|
|
23
|
+
"If the URL matches multiple entries, specify --uuid to disambiguate."
|
|
21
24
|
),
|
|
22
25
|
)
|
|
23
|
-
p.add_argument("
|
|
26
|
+
p.add_argument("url", help="URL of the entry")
|
|
24
27
|
p.add_argument("--uuid", default=None, help="UUID of the entry (required when URL matches multiple entries)")
|
|
25
28
|
p.add_argument("--username", default=None, help="New username")
|
|
26
29
|
p.add_argument("--password", default=None, help="New password")
|
|
@@ -36,7 +39,8 @@ def run(
|
|
|
36
39
|
*,
|
|
37
40
|
fmt: str = "table",
|
|
38
41
|
) -> int:
|
|
39
|
-
|
|
42
|
+
url = ensure_scheme(args.url)
|
|
43
|
+
entries = client.get_logins(url)
|
|
40
44
|
|
|
41
45
|
if not entries:
|
|
42
46
|
logger.error("No entries found for: %s", args.url)
|
|
@@ -62,11 +66,11 @@ def run(
|
|
|
62
66
|
return 1
|
|
63
67
|
|
|
64
68
|
client.set_login(
|
|
65
|
-
url=
|
|
69
|
+
url=url,
|
|
66
70
|
username=args.username if args.username is not None else entry.login,
|
|
67
71
|
password=args.password if args.password is not None else entry.password,
|
|
68
72
|
uuid=entry.uuid,
|
|
69
73
|
group_uuid=entry.group_uuid,
|
|
70
74
|
)
|
|
71
|
-
|
|
75
|
+
print_result("Entry updated.", fmt)
|
|
72
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
|
|
@@ -7,7 +7,7 @@ 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_entry_detail
|
|
10
|
+
from keepassxc_cli.output import ensure_scheme, print_entry_detail
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -30,9 +30,10 @@ def run(
|
|
|
30
30
|
*,
|
|
31
31
|
fmt: str = "table",
|
|
32
32
|
) -> int:
|
|
33
|
-
|
|
33
|
+
url = ensure_scheme(args.url)
|
|
34
|
+
entries = client.get_logins(url)
|
|
34
35
|
if not entries:
|
|
35
|
-
logger.warning("No entries found for: %s",
|
|
36
|
+
logger.warning("No entries found for: %s", url)
|
|
36
37
|
return 1
|
|
37
38
|
for entry in entries:
|
|
38
39
|
print_entry_detail(entry, fmt, show_password=args.show_password, show_kph_prefix=getattr(args, "show_kph_prefix", False))
|
|
@@ -7,7 +7,7 @@ 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_totp
|
|
10
|
+
from keepassxc_cli.output import ensure_scheme, print_totp
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -28,7 +28,7 @@ def run(
|
|
|
28
28
|
*,
|
|
29
29
|
fmt: str = "table",
|
|
30
30
|
) -> int:
|
|
31
|
-
entries = client.get_logins(args.url)
|
|
31
|
+
entries = client.get_logins(ensure_scheme(args.url))
|
|
32
32
|
if not entries:
|
|
33
33
|
logger.warning("No entries found for: %s", args.url)
|
|
34
34
|
return 1
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
|
|
5
6
|
from keepassxc_browser_api import Entry
|
|
6
7
|
|
|
7
8
|
_KPH_PREFIX = "KPH: "
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_scheme(url: str) -> str:
|
|
14
|
+
"""Return url with a scheme. Prepends https:// with a warning if none is present."""
|
|
15
|
+
if url.startswith("http://") or url.startswith("https://"):
|
|
16
|
+
return url
|
|
17
|
+
logger.warning("URL %r has no scheme, assuming https://", url)
|
|
18
|
+
return "https://" + url
|
|
19
|
+
|
|
9
20
|
|
|
10
21
|
def _strip_kph(key: str) -> str:
|
|
11
22
|
return key[len(_KPH_PREFIX):] if key.startswith(_KPH_PREFIX) else key
|
|
@@ -64,6 +75,13 @@ def print_totp(totp: str, fmt: str = "table") -> None:
|
|
|
64
75
|
print(totp)
|
|
65
76
|
|
|
66
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
|
+
|
|
67
85
|
def print_status(info: dict, fmt: str = "table") -> None:
|
|
68
86
|
if fmt == "json":
|
|
69
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
|
|
@@ -86,6 +86,8 @@ keepassxc-cli status -j
|
|
|
86
86
|
|
|
87
87
|
### Commands
|
|
88
88
|
|
|
89
|
+
> **URL scheme**: All commands that accept a URL argument require a scheme (`https://` or `http://`). If you pass a bare hostname (e.g. `example.com`), the CLI will automatically prepend `https://` and emit a warning. KeePassXC derives the entry title from `QUrl(url).host()`, which returns an empty string for URLs without a scheme.
|
|
90
|
+
|
|
89
91
|
#### `setup` — Associate with KeePassXC
|
|
90
92
|
|
|
91
93
|
```bash
|
|
@@ -119,19 +121,20 @@ keepassxc-cli totp https://github.com -j
|
|
|
119
121
|
#### `clip` — Copy a field to clipboard
|
|
120
122
|
|
|
121
123
|
```bash
|
|
122
|
-
keepassxc-cli clip
|
|
123
|
-
keepassxc-cli clip
|
|
124
|
-
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
|
|
125
127
|
```
|
|
126
128
|
|
|
127
129
|
#### `add` — Add a new entry
|
|
128
130
|
|
|
129
131
|
```bash
|
|
130
132
|
# Password is prompted securely if --password is not given
|
|
131
|
-
keepassxc-cli add
|
|
132
|
-
keepassxc-cli add
|
|
133
|
-
# Place the entry in a specific group by UUID
|
|
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
|
+
# Place the entry in a specific group by UUID or by path
|
|
136
|
+
keepassxc-cli add https://example.com user --group-uuid <group-uuid>
|
|
137
|
+
keepassxc-cli add https://example.com user --group "Work/Projects"
|
|
135
138
|
```
|
|
136
139
|
|
|
137
140
|
> **Note**: The entry title is always derived from the URL hostname by KeePassXC. The protocol has no field to set a custom title.
|
|
@@ -139,19 +142,20 @@ keepassxc-cli add --url https://example.com --username user --group-uuid <group-
|
|
|
139
142
|
#### `edit` — Edit an entry
|
|
140
143
|
|
|
141
144
|
```bash
|
|
142
|
-
#
|
|
143
|
-
keepassxc-cli
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
keepassxc-cli edit
|
|
147
|
-
keepassxc-cli edit <uuid> --url https://github.com --password newpass --title "New Title"
|
|
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
|
+
# Specify --uuid explicitly when the URL matches multiple entries
|
|
149
|
+
keepassxc-cli edit https://github.com --uuid <uuid> --username newuser
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
#### `rm` — Delete an entry
|
|
151
153
|
|
|
152
154
|
```bash
|
|
153
|
-
keepassxc-cli rm
|
|
154
|
-
keepassxc-cli rm
|
|
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
|
|
155
159
|
```
|
|
156
160
|
|
|
157
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)
|
|
@@ -136,6 +137,15 @@ class TestShowCommand:
|
|
|
136
137
|
assert rc == 1
|
|
137
138
|
assert any("No entries" in r.message for r in caplog.records)
|
|
138
139
|
|
|
140
|
+
def test_url_no_scheme_prefixed(self, mock_client, cli_config, browser_config, browser_config_path, caplog, mock_entry):
|
|
141
|
+
entry = mock_entry()
|
|
142
|
+
mock_client.get_logins.return_value = [entry]
|
|
143
|
+
args = make_args(url="example.com", show_password=False)
|
|
144
|
+
rc = show.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
145
|
+
assert rc == 0
|
|
146
|
+
mock_client.get_logins.assert_called_once_with("https://example.com")
|
|
147
|
+
assert any("no scheme" in r.message.lower() for r in caplog.records)
|
|
148
|
+
|
|
139
149
|
|
|
140
150
|
# --- add ---
|
|
141
151
|
|
|
@@ -169,6 +179,15 @@ class TestAddCommand:
|
|
|
169
179
|
assert rc == 1
|
|
170
180
|
assert any("not found" in r.message.lower() for r in caplog.records)
|
|
171
181
|
|
|
182
|
+
def test_url_no_scheme_prefixed(self, mock_client, cli_config, browser_config, browser_config_path, caplog):
|
|
183
|
+
mock_client.set_login.return_value = True
|
|
184
|
+
args = make_args(url="example.com", username="u", password="p", group_uuid="", group=None)
|
|
185
|
+
rc = add.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
186
|
+
assert rc == 0
|
|
187
|
+
call_kwargs = mock_client.set_login.call_args.kwargs
|
|
188
|
+
assert call_kwargs["url"] == "https://example.com"
|
|
189
|
+
assert any("no scheme" in r.message.lower() for r in caplog.records)
|
|
190
|
+
|
|
172
191
|
def test_failure_propagates(self, mock_client, cli_config, browser_config, browser_config_path):
|
|
173
192
|
mock_client.set_login.side_effect = ProtocolError("access denied", error_code=6)
|
|
174
193
|
args = make_args(url="https://example.com", username="u", password="p", group_uuid="", group=None)
|
|
@@ -228,22 +247,25 @@ class TestEditCommand:
|
|
|
228
247
|
# --- rm ---
|
|
229
248
|
|
|
230
249
|
class TestRmCommand:
|
|
231
|
-
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]
|
|
232
253
|
mock_client.delete_entry.return_value = True
|
|
233
|
-
args = make_args(
|
|
254
|
+
args = make_args(url="https://example.com", uuid=None, yes=True)
|
|
234
255
|
rc = rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
235
256
|
assert rc == 0
|
|
236
|
-
mock_client.delete_entry.assert_called_once_with("
|
|
257
|
+
mock_client.delete_entry.assert_called_once_with("url-resolved-uuid")
|
|
237
258
|
assert "deleted" in capsys.readouterr().out.lower()
|
|
238
259
|
|
|
239
|
-
def
|
|
240
|
-
|
|
241
|
-
|
|
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]
|
|
242
264
|
mock_client.delete_entry.return_value = True
|
|
243
|
-
args = make_args(
|
|
265
|
+
args = make_args(url="https://example.com", uuid="uuid-2", yes=True)
|
|
244
266
|
rc = rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
245
267
|
assert rc == 0
|
|
246
|
-
mock_client.delete_entry.assert_called_once_with("
|
|
268
|
+
mock_client.delete_entry.assert_called_once_with("uuid-2")
|
|
247
269
|
|
|
248
270
|
def test_url_multiple_matches_error(self, mock_client, cli_config, browser_config, browser_config_path, caplog, mock_entry):
|
|
249
271
|
e1 = mock_entry(uuid="uuid-1", login="alice")
|
|
@@ -261,9 +283,19 @@ class TestRmCommand:
|
|
|
261
283
|
assert rc == 1
|
|
262
284
|
assert any("no entries" in r.message.lower() for r in caplog.records)
|
|
263
285
|
|
|
264
|
-
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]
|
|
265
297
|
mock_client.delete_entry.side_effect = ProtocolError("access denied", error_code=6)
|
|
266
|
-
args = make_args(
|
|
298
|
+
args = make_args(url="https://example.com", uuid=None, yes=True)
|
|
267
299
|
with pytest.raises(ProtocolError):
|
|
268
300
|
rm.run(mock_client, args, cli_config, browser_config, browser_config_path)
|
|
269
301
|
|
|
@@ -1,59 +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
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
15
|
-
p = subparsers.add_parser("rm", help="Delete an entry")
|
|
16
|
-
group = p.add_mutually_exclusive_group(required=True)
|
|
17
|
-
group.add_argument("--uuid", default=None, help="UUID of the entry to delete")
|
|
18
|
-
group.add_argument("--url", default=None, help="URL of the entry to delete (must match exactly one entry)")
|
|
19
|
-
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
|
|
20
|
-
p.set_defaults(func=run)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def run(
|
|
24
|
-
client: BrowserClient,
|
|
25
|
-
args: argparse.Namespace,
|
|
26
|
-
cli_config: CliConfig,
|
|
27
|
-
browser_config: BrowserConfig,
|
|
28
|
-
browser_config_path: Path,
|
|
29
|
-
*,
|
|
30
|
-
fmt: str = "table",
|
|
31
|
-
) -> int:
|
|
32
|
-
if args.uuid is not None:
|
|
33
|
-
target_uuid = args.uuid
|
|
34
|
-
label = args.uuid
|
|
35
|
-
else:
|
|
36
|
-
entries = client.get_logins(args.url)
|
|
37
|
-
if not entries:
|
|
38
|
-
logger.error("No entries found for: %s", args.url)
|
|
39
|
-
return 1
|
|
40
|
-
if len(entries) > 1:
|
|
41
|
-
logger.error(
|
|
42
|
-
"Multiple entries found for %s \u2014 specify --uuid to disambiguate:",
|
|
43
|
-
args.url,
|
|
44
|
-
)
|
|
45
|
-
for e in entries:
|
|
46
|
-
print(f" {e.uuid} {e.login} ({e.name})")
|
|
47
|
-
return 1
|
|
48
|
-
target_uuid = entries[0].uuid
|
|
49
|
-
label = f"{entries[0].name} ({args.url})"
|
|
50
|
-
|
|
51
|
-
if not args.yes:
|
|
52
|
-
answer = input(f"Delete entry {label}? [y/N] ").strip().lower()
|
|
53
|
-
if answer != "y":
|
|
54
|
-
print("Aborted.")
|
|
55
|
-
return 1
|
|
56
|
-
|
|
57
|
-
client.delete_entry(target_uuid)
|
|
58
|
-
print("Entry deleted.")
|
|
59
|
-
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
|