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.

Files changed (41) hide show
  1. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/CLAUDE.md +4 -4
  2. {keepassxc_cli-1.7.0/keepassxc_cli.egg-info → keepassxc_cli-2.0.0}/PKG-INFO +16 -15
  3. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/README.md +15 -14
  4. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/__main__.py +6 -6
  5. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/add.py +7 -6
  6. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/clip.py +4 -3
  7. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/edit.py +8 -6
  8. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/lock.py +5 -3
  9. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/mkdir.py +9 -3
  10. keepassxc_cli-2.0.0/keepassxc_cli/commands/rm.py +63 -0
  11. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/output.py +7 -0
  12. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0/keepassxc_cli.egg-info}/PKG-INFO +16 -15
  13. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/test_commands.py +24 -10
  14. keepassxc_cli-1.7.0/keepassxc_cli/commands/rm.py +0 -60
  15. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/auto-merge-dependabot.yml +0 -0
  16. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/auto-release.yml +0 -0
  17. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/lint_and_test.yml +0 -0
  18. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.github/workflows/pypi.yml +0 -0
  19. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/.gitignore +0 -0
  20. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/LICENSE +0 -0
  21. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/assets/demo.gif +0 -0
  22. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/assets/settings-browser-integration.png +0 -0
  23. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/__init__.py +0 -0
  24. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/__init__.py +0 -0
  25. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/group_uuid.py +0 -0
  26. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/setup.py +0 -0
  27. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/show.py +0 -0
  28. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/status.py +0 -0
  29. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/totp.py +0 -0
  30. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/commands/version.py +0 -0
  31. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli/config.py +0 -0
  32. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/SOURCES.txt +0 -0
  33. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/dependency_links.txt +0 -0
  34. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/entry_points.txt +0 -0
  35. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/requires.txt +0 -0
  36. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/keepassxc_cli.egg-info/top_level.txt +0 -0
  37. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/pyproject.toml +0 -0
  38. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/setup.cfg +0 -0
  39. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/conftest.py +0 -0
  40. {keepassxc_cli-1.7.0 → keepassxc_cli-2.0.0}/tests/test_config.py +0 -0
  41. {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 (--group path or --group-uuid)
23
- ├── edit.py # edit existing entry by URL (--uuid optional)
24
- ├── rm.py # delete entry by UUID or URL
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: 1.7.0
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 password https://github.com
125
- keepassxc-cli clip username https://github.com
126
- keepassxc-cli clip totp https://github.com
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 --url https://example.com --username user@example.com
134
- keepassxc-cli add --url https://example.com --username user --password mypass
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 --url https://example.com --username user --group-uuid <group-uuid>
137
- keepassxc-cli add --url https://example.com --username user --group "Work/Projects"
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
- # --url is required; --uuid is optional when the URL matches exactly one entry
146
- keepassxc-cli edit --url https://github.com --username newuser
147
- keepassxc-cli edit --url https://github.com --password newpass
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 --url https://github.com --uuid <uuid> --username newuser
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 --uuid <uuid> # prompts for confirmation
156
- keepassxc-cli rm --uuid <uuid> --yes # skip confirmation
157
- keepassxc-cli rm --url https://example.com # resolve by URL (errors if multiple matches)
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 password https://github.com
110
- keepassxc-cli clip username https://github.com
111
- keepassxc-cli clip totp https://github.com
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 --url https://example.com --username user@example.com
119
- keepassxc-cli add --url https://example.com --username user --password mypass
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 --url https://example.com --username user --group-uuid <group-uuid>
122
- keepassxc-cli add --url https://example.com --username user --group "Work/Projects"
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
- # --url is required; --uuid is optional when the URL matches exactly one entry
131
- keepassxc-cli edit --url https://github.com --username newuser
132
- keepassxc-cli edit --url https://github.com --password newpass
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 --url https://github.com --uuid <uuid> --username newuser
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 --uuid <uuid> # prompts for confirmation
141
- keepassxc-cli rm --uuid <uuid> --yes # skip confirmation
142
- keepassxc-cli rm --url https://example.com # resolve by URL (errors if multiple matches)
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
- p = subparsers.add_parser("add", help="Add a new entry")
18
- p.add_argument("--url", required=True, help="Entry URL")
19
- p.add_argument("--username", required=True, help="Username")
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
- print("Entry added successfully.")
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
- p = subparsers.add_parser("clip", help="Copy a field to clipboard")
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. Provide --url to look up the entry; omitted fields are\n"
21
- "left unchanged. If the URL matches multiple entries, specify --uuid to disambiguate."
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("--url", required=True, help="URL of the entry")
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
- print("Entry updated successfully.")
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
- p = subparsers.add_parser("lock", help="Lock the KeePassXC database")
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
- print("Database locked.")
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
- p = subparsers.add_parser("mkdir", help="Create a new group")
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
- print(f"Group created: {group.name} [{group.uuid}]")
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: 1.7.0
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 password https://github.com
125
- keepassxc-cli clip username https://github.com
126
- keepassxc-cli clip totp https://github.com
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 --url https://example.com --username user@example.com
134
- keepassxc-cli add --url https://example.com --username user --password mypass
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 --url https://example.com --username user --group-uuid <group-uuid>
137
- keepassxc-cli add --url https://example.com --username user --group "Work/Projects"
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
- # --url is required; --uuid is optional when the URL matches exactly one entry
146
- keepassxc-cli edit --url https://github.com --username newuser
147
- keepassxc-cli edit --url https://github.com --password newpass
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 --url https://github.com --uuid <uuid> --username newuser
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 --uuid <uuid> # prompts for confirmation
156
- keepassxc-cli rm --uuid <uuid> --yes # skip confirmation
157
- keepassxc-cli rm --url https://example.com # resolve by URL (errors if multiple matches)
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 test_uuid_with_yes_flag(self, mock_client, cli_config, browser_config, browser_config_path, capsys):
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(uuid="some-uuid", url=None, yes=True)
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("some-uuid")
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 test_url_single_match_deletes(self, mock_client, cli_config, browser_config, browser_config_path, capsys, mock_entry):
258
- entry = mock_entry(uuid="url-resolved-uuid")
259
- mock_client.get_logins.return_value = [entry]
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(uuid=None, url="https://example.com", yes=True)
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("url-resolved-uuid")
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 test_failure_propagates(self, mock_client, cli_config, browser_config, browser_config_path):
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(uuid="some-uuid", url=None, yes=True)
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