dong-pass 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dong-labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: dong-pass
3
+ Version: 0.1.0
4
+ Summary: 密码咚 - 账号密码管理 CLI
5
+ Author: gudong
6
+ License-File: LICENSE
7
+ Keywords: account,cli,password,security
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: cryptography>=41.0.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: typer>=0.12.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
14
+ Requires-Dist: pytest>=7.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # 密码咚(dong-pass)
18
+
19
+ 账号密码管理 CLI - 帮你安全存储和管理账号密码
20
+
21
+ ## 安装
22
+
23
+ ```bash
24
+ pipx install dong-pass
25
+ ```
26
+
27
+ ## 快速开始
28
+
29
+ ```bash
30
+ # 初始化
31
+ dong-pass init
32
+
33
+ # 添加账号
34
+ dong-pass add github --account gudongtongxue --password "Gudong123!@#" --category "个人"
35
+
36
+ # 查询账号
37
+ dong-pass get github
38
+
39
+ # 列出所有
40
+ dong-pass list
41
+
42
+ # 搜索
43
+ dong-pass search "github"
44
+ ```
45
+
46
+ ## 命令
47
+
48
+ | 命令 | 说明 |
49
+ |------|------|
50
+ | `init` | 初始化数据库 |
51
+ | `add` | 添加账号 |
52
+ | `get` | 获取账号 |
53
+ | `list` | 列出所有 |
54
+ | `search` | 搜索 |
55
+ | `update` | 更新账号 |
56
+ | `delete` | 删除账号 |
57
+ | `stats` | 统计信息 |
58
+ | `export` | 导出为 JSON |
59
+ | `import` | 从 JSON 导入 |
60
+ | `set-master` | 设置主密码 |
61
+ | `change-master` | 更改主密码 |
62
+ | `remove-encrypt` | 移除加密 |
63
+
64
+ ## 加密存储
65
+
66
+ ```bash
67
+ # 设置主密码(加密模式)
68
+ dong-pass init --master-password "MySecretKey"
69
+
70
+ # 添加时加密
71
+ dong-pass add github --account gudong --password "123456" --encrypt
72
+
73
+ # 查询时需要输入主密码
74
+ dong-pass get github
75
+ # 输入主密码: MySecretKey
76
+ # 显示密码: 123456
77
+ ```
78
+
79
+ ## 数据库
80
+
81
+ 数据存储在 `~/.dong/accounts.db`
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,69 @@
1
+ # 密码咚(dong-pass)
2
+
3
+ 账号密码管理 CLI - 帮你安全存储和管理账号密码
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pipx install dong-pass
9
+ ```
10
+
11
+ ## 快速开始
12
+
13
+ ```bash
14
+ # 初始化
15
+ dong-pass init
16
+
17
+ # 添加账号
18
+ dong-pass add github --account gudongtongxue --password "Gudong123!@#" --category "个人"
19
+
20
+ # 查询账号
21
+ dong-pass get github
22
+
23
+ # 列出所有
24
+ dong-pass list
25
+
26
+ # 搜索
27
+ dong-pass search "github"
28
+ ```
29
+
30
+ ## 命令
31
+
32
+ | 命令 | 说明 |
33
+ |------|------|
34
+ | `init` | 初始化数据库 |
35
+ | `add` | 添加账号 |
36
+ | `get` | 获取账号 |
37
+ | `list` | 列出所有 |
38
+ | `search` | 搜索 |
39
+ | `update` | 更新账号 |
40
+ | `delete` | 删除账号 |
41
+ | `stats` | 统计信息 |
42
+ | `export` | 导出为 JSON |
43
+ | `import` | 从 JSON 导入 |
44
+ | `set-master` | 设置主密码 |
45
+ | `change-master` | 更改主密码 |
46
+ | `remove-encrypt` | 移除加密 |
47
+
48
+ ## 加密存储
49
+
50
+ ```bash
51
+ # 设置主密码(加密模式)
52
+ dong-pass init --master-password "MySecretKey"
53
+
54
+ # 添加时加密
55
+ dong-pass add github --account gudong --password "123456" --encrypt
56
+
57
+ # 查询时需要输入主密码
58
+ dong-pass get github
59
+ # 输入主密码: MySecretKey
60
+ # 显示密码: 123456
61
+ ```
62
+
63
+ ## 数据库
64
+
65
+ 数据存储在 `~/.dong/accounts.db`
66
+
67
+ ## License
68
+
69
+ MIT
@@ -0,0 +1,120 @@
1
+ # TOOLS.md - 工具箱
2
+
3
+ 我的核心工具是 `dong-pass` CLI。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pipx install dong-pass
9
+ ```
10
+
11
+ ## 命令列表
12
+
13
+ ### 初始化
14
+
15
+ ```bash
16
+ dong-pass init
17
+ dong-pass init --master-password "MySecretKey" # 加密模式
18
+ ```
19
+
20
+ ### 添加账号
21
+
22
+ ```bash
23
+ # 添加 GitHub 账号
24
+ dong-pass add github --account gudongtongxue --password "Gudong123!@#" --category "个人"
25
+
26
+ # 添加阿里云账号(加密)
27
+ dong-pass add aliyun --account gudong@aliyun.com --password "Aliyun456$" --category "工作" --encrypt
28
+
29
+ # 添加微博账号(带备注)
30
+ dong-pass add weibo --account gudongtongxue --password "Weibo789#" --nickname "工作微博" --category "社交"
31
+ ```
32
+
33
+ ### 查询账号
34
+
35
+ ```bash
36
+ dong-pass get github
37
+
38
+ # 输出:
39
+ # 网站:github.com
40
+ # 账号:gudongtongxue
41
+ # 密码:Gudong123!@#
42
+ # 分类:个人
43
+ ```
44
+
45
+ ### 列出所有
46
+
47
+ ```bash
48
+ # 列出所有
49
+ dong-pass list
50
+
51
+ # 列出工作类
52
+ dong-pass list --category 工作
53
+ ```
54
+
55
+ ### 搜索
56
+
57
+ ```bash
58
+ # 搜索包含 "aliyun" 的账号
59
+ dong-pass search aliyun
60
+
61
+ # 搜索所有个人类账号
62
+ dong-pass search --category 个人
63
+ ```
64
+
65
+ ### 更新账号
66
+
67
+ ```bash
68
+ # 更新密码
69
+ dong-pass update github --password "NewPassword123!"
70
+
71
+ # 更新分类
72
+ dong-pass update weibo --category "个人"
73
+ ```
74
+
75
+ ### 删除账号
76
+
77
+ ```bash
78
+ # 删除账号(需要确认)
79
+ dong-pass delete github
80
+
81
+ # 强制删除(不确认)
82
+ dong-pass delete github --force
83
+ ```
84
+
85
+ ### 统计信息
86
+
87
+ ```bash
88
+ dong-pass stats
89
+ ```
90
+
91
+ ### 导出/导入
92
+
93
+ ```bash
94
+ # 导出为 JSON
95
+ dong-pass export --output accounts.json
96
+
97
+ # 从 JSON 导入
98
+ dong-pass import --input accounts.json
99
+ ```
100
+
101
+ ### 加密管理
102
+
103
+ ```bash
104
+ # 设置主密码(首次设置)
105
+ dong-pass set-master
106
+
107
+ # 更改主密码
108
+ dong-pass change-master
109
+
110
+ # 移除加密(明文化)
111
+ dong-pass remove-encrypt
112
+ ```
113
+
114
+ ## 数据库
115
+
116
+ 数据存储在 `~/.dong/accounts.db`
117
+
118
+ ---
119
+
120
+ *🔒 安全存储,随时访问*
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dong-pass"
7
+ version = "0.1.0"
8
+ description = "密码咚 - 账号密码管理 CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [{name = "gudong"}]
12
+ keywords = ["cli", "password", "account", "security"]
13
+ dependencies = [
14
+ "typer>=0.12.0",
15
+ "rich>=13.0.0",
16
+ "cryptography>=41.0.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=7.0",
22
+ "pytest-cov>=4.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ dong-pass = "pass.cli:app"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/pass"]
@@ -0,0 +1,3 @@
1
+ """密码咚 - 账号密码管理 CLI"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,61 @@
1
+ """密码咚 CLI 主入口"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from dong import json_output
6
+ from . import __version__
7
+
8
+ console = Console()
9
+
10
+ app = typer.Typer(
11
+ name="dong-pass",
12
+ help="密码咚 - 账号密码管理 CLI",
13
+ no_args_is_help=True,
14
+ add_completion=False,
15
+ )
16
+
17
+ # 导入命令
18
+ from .commands import (
19
+ init,
20
+ add,
21
+ get,
22
+ ls,
23
+ search,
24
+ update,
25
+ delete,
26
+ stats,
27
+ export,
28
+ import_cmd,
29
+ set_master,
30
+ change_master,
31
+ remove_encrypt,
32
+ )
33
+
34
+ # 注册命令
35
+ app.command()(init.init)
36
+ app.command()(add.add)
37
+ app.command()(get.get)
38
+ app.command(name="list")(ls.list_accounts)
39
+ app.command()(search.search)
40
+ app.command()(update.update)
41
+ app.command()(delete.delete)
42
+ app.command()(stats.stats)
43
+ app.command()(export.export)
44
+ app.command()(import_cmd.import_cmd)
45
+ app.command()(set_master.set_master)
46
+ app.command()(change_master.change_master)
47
+ app.command()(remove_encrypt.remove_encrypt)
48
+
49
+
50
+ @app.callback()
51
+ def main(
52
+ version: bool = typer.Option(False, "--version", "-v", help="显示版本"),
53
+ ):
54
+ """密码咚 - 账号密码管理 CLI"""
55
+ if version:
56
+ console.print(f"dong-pass {__version__}")
57
+ raise typer.Exit()
58
+
59
+
60
+ if __name__ == "__main__":
61
+ app()
@@ -0,0 +1,19 @@
1
+ """命令模块"""
2
+
3
+ from . import init, add, get, ls, search, update, delete, stats, export, import_cmd, set_master, change_master, remove_encrypt
4
+
5
+ __all__ = [
6
+ "init",
7
+ "add",
8
+ "get",
9
+ "ls",
10
+ "search",
11
+ "update",
12
+ "delete",
13
+ "stats",
14
+ "export",
15
+ "import_cmd",
16
+ "set_master",
17
+ "change_master",
18
+ "remove_encrypt",
19
+ ]
@@ -0,0 +1,58 @@
1
+ """add 命令"""
2
+
3
+ import typer
4
+ from ..db import PassDatabase, get_cursor
5
+ from dong import json_output, DongError
6
+
7
+ console = typer.Console()
8
+
9
+
10
+ @json_output
11
+ def add(
12
+ site: str = typer.Argument(..., help="网站名称"),
13
+ account: str = typer.Option(..., "--account", "-a", help="账号"),
14
+ password: str = typer.Option(..., "--password", "-p", help="密码"),
15
+ nickname: str = typer.Option(None, "--nickname", help="昵称"),
16
+ category: str = typer.Option(None, "--category", "-c", help="分类(工作/个人/金融/社交)"),
17
+ note: str = typer.Option(None, "--note", help="备注"),
18
+ encrypt: bool = typer.Option(False, "--encrypt", help="加密存储"),
19
+ ):
20
+ """添加账号密码"""
21
+ from ..db import is_initialized
22
+
23
+ if not is_initialized():
24
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
25
+
26
+ # 判断是否需要主密码
27
+ need_master = encrypt
28
+ master_password = None
29
+ if need_master:
30
+ master_password = typer.prompt("输入主密码以加密", password=True)
31
+ if not master_password:
32
+ raise DongError("NO_MASTER_PASSWORD", "加密存储需要主密码")
33
+
34
+ # 加密密码
35
+ db = PassDatabase(master_password)
36
+ if encrypt:
37
+ encrypted_pwd = db.encrypt(password)
38
+ else:
39
+ encrypted_pwd = password
40
+
41
+ with get_cursor(master_password) as cur:
42
+ try:
43
+ cur.execute("""
44
+ INSERT INTO accounts (site, account, password, nickname, category, note, encrypted)
45
+ VALUES (?, ?, ?, ?, ?, ?, ?)
46
+ """, (site.lower(), account, encrypted_pwd, nickname, category, note, encrypt))
47
+ except sqlite3.IntegrityError:
48
+ raise DongError("DUPLICATE_SITE", f"网站 '{site}' 已存在,请使用 update 命令更新")
49
+
50
+ console.print(f"[green]✅ 已添加账号:{site}[/green]")
51
+
52
+ return {
53
+ "site": site.lower(),
54
+ "account": account,
55
+ "encrypted": encrypt,
56
+ "nickname": nickname,
57
+ "category": category,
58
+ }
@@ -0,0 +1,56 @@
1
+ """change_master 命令"""
2
+
3
+ import typer
4
+ from ..db import PassDatabase, get_cursor, is_initialized
5
+ from dong import json_output, DongError
6
+
7
+ console = typer.Console()
8
+
9
+
10
+ @json_output
11
+ def change_master():
12
+ """更改主密码"""
13
+ if not is_initialized():
14
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
15
+
16
+ # 检查是否已加密
17
+ with get_cursor() as cur:
18
+ cur.execute("SELECT encrypted FROM accounts LIMIT 1")
19
+ row = cur.fetchone()
20
+
21
+ if not row or not row["encrypted"]:
22
+ raise DongError("NOT_ENCRYPTED", "数据库不是加密模式")
23
+
24
+ # 输入旧主密码
25
+ old_password = typer.prompt("输入当前主密码", password=True)
26
+
27
+ if not old_password:
28
+ raise DongError("INVALID_PASSWORD", "主密码不能为空")
29
+
30
+ # 输入新主密码
31
+ new_password = typer.prompt("输入新主密码", password=True, confirmation_prompt=True)
32
+
33
+ if not new_password:
34
+ raise DongError("INVALID_PASSWORD", "新主密码不能为空")
35
+
36
+ # 获取数据库
37
+ db = PassDatabase(old_password)
38
+
39
+ # 重新加密所有密码
40
+ with get_cursor(old_password) as cur:
41
+ cur.execute("SELECT id, account, password FROM accounts")
42
+ rows = cur.fetchall()
43
+
44
+ for row in rows:
45
+ account_id = row["id"]
46
+ encrypted_pwd = row["password"]
47
+ # 重新加密
48
+ new_encrypted_pwd = db.encrypt(encrypted_pwd)
49
+ cur.execute(
50
+ "UPDATE accounts SET password = ? WHERE id = ?",
51
+ (new_encrypted_pwd, account_id)
52
+ )
53
+
54
+ console.print("[green]✅ 主密码已更改[/green]")
55
+
56
+ return {"message": "主密码更改成功"}
@@ -0,0 +1,39 @@
1
+ """delete 命令"""
2
+
3
+ import typer
4
+ from ..db import get_cursor, is_initialized
5
+ from dong import json_output, DongError
6
+
7
+ console = typer.Console()
8
+
9
+
10
+ @json_output
11
+ def delete(
12
+ site: str = typer.Option(..., help="网站名称"),
13
+ force: bool = typer.Option(False, "--force", "-f", help="强制删除,不确认"),
14
+ ):
15
+ """删除账号"""
16
+ from ..db import PassDatabase, get_db_path
17
+
18
+ if not is_initialized():
19
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
20
+
21
+ with get_cursor() as cur:
22
+ cur.execute("SELECT site FROM accounts WHERE site = ?", (site.lower(),))
23
+ row = cur.fetchone()
24
+
25
+ if not row:
26
+ raise DongError("NOT_FOUND", f"未找到网站:{site}")
27
+
28
+ site_name = row["site"]
29
+
30
+ if not force:
31
+ if not typer.confirm(f"确定要删除网站 '{site_name}' 吗?"):
32
+ return {"cancelled": True}
33
+
34
+ with get_cursor() as cur:
35
+ cur.execute("DELETE FROM accounts WHERE site = ?", (site.lower(),))
36
+
37
+ console.print(f"[green]✅ 已删除账号:{site_name}[/green]")
38
+
39
+ return {"site": site_name, "deleted": True}
@@ -0,0 +1,51 @@
1
+ """export 命令"""
2
+
3
+ import typer
4
+ import json
5
+ from rich.console import Console
6
+ from ..db import get_cursor, is_initialized
7
+ from dong import json_output, DongError
8
+
9
+ console = Console()
10
+
11
+
12
+ @json_output
13
+ def export(
14
+ output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
15
+ ):
16
+ """导出账号到 JSON"""
17
+ from ..db import get_db_path
18
+
19
+ if not is_initialized():
20
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
21
+
22
+ db_path = get_db_path()
23
+ if not db_path.exists():
24
+ raise DongError("NOT_INITIALIZED", "数据库不存在,请运行 dong-pass init")
25
+
26
+ with get_cursor() as cur:
27
+ cur.execute("SELECT * FROM accounts")
28
+ rows = cur.fetchall()
29
+
30
+ if not rows:
31
+ console.print("暂无账号记录")
32
+ return {"message": "无数据导出", "accounts": []}
33
+
34
+ # 转换为字典
35
+ accounts = []
36
+ for row in rows:
37
+ account = dict(row)
38
+ # 不导出密码(除非用户有特殊需求)
39
+ # password_encrypted = account["password"]
40
+ accounts.append(account)
41
+
42
+ data = {"accounts": accounts, "exported_at": "2026-03-19"}
43
+
44
+ if output:
45
+ with open(output, "w", encoding="utf-8") as f:
46
+ json.dump(data, f, ensure_ascii=False, indent=2)
47
+ console.print(f"[green]✅ 已导出到:{output}[/green]")
48
+ else:
49
+ console.print(json.dumps(data, ensure_ascii=False, indent=2))
50
+
51
+ return {"message": "导出成功", "count": len(accounts)}
@@ -0,0 +1,83 @@
1
+ """get 命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from datetime import datetime
7
+ from ..db import PassDatabase, get_cursor, is_initialized
8
+ from dong import json_output, DongError
9
+
10
+ console = Console()
11
+
12
+
13
+ @json_output
14
+ def get(
15
+ site: str = typer.Argument(..., help="网站名称"),
16
+ ):
17
+ """获取账号密码"""
18
+ from ..db import get_db_path
19
+
20
+ if not is_initialized():
21
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
22
+
23
+ db_path = get_db_path()
24
+ if not db_path.exists():
25
+ raise DongError("NOT_INITIALIZED", "数据库不存在,请运行 dong-pass init")
26
+
27
+ # 判断是否加密
28
+ db = PassDatabase()
29
+ with get_cursor() as cur:
30
+ cur.execute("SELECT * FROM accounts WHERE site = ?", (site.lower(),))
31
+ row = cur.fetchone()
32
+
33
+ if not row:
34
+ raise DongError("NOT_FOUND", f"未找到网站:{site}")
35
+
36
+ site_name = row["site"]
37
+ account = row["account"]
38
+ password_encrypted = row["password"]
39
+ nickname = row["nickname"] or "-"
40
+ category = row["category"] or "-"
41
+ encrypted = row["encrypted"]
42
+ last_used_at = row["last_used_at"]
43
+ created_at = row["created_at"]
44
+
45
+ # 解密密码
46
+ if encrypted:
47
+ master_password = typer.prompt(f"请输入主密码以解密 {site_name} 的密码", password=True)
48
+ db = PassDatabase(master_password)
49
+ password = db.decrypt(password_encrypted)
50
+ else:
51
+ password = password_encrypted
52
+
53
+ # 更新最后使用时间
54
+ with get_cursor() as cur:
55
+ cur.execute("UPDATE accounts SET last_used_at = CURRENT_TIMESTAMP WHERE site = ?", (site.lower(),))
56
+
57
+ # 渲染输出
58
+ table = Table(title=f"账号信息:{site_name}")
59
+ table.add_column("项目", style="cyan")
60
+ table.add_column("内容", style="green")
61
+
62
+ table.add_row("网站", site_name)
63
+ table.add_row("账号", account)
64
+ table.add_row("密码", password)
65
+ if nickname != "-":
66
+ table.add_row("昵称", nickname)
67
+ if category != "-":
68
+ table.add_row("分类", category)
69
+ if last_used_at:
70
+ table.add_row("最后使用", last_used_at)
71
+ table.add_row("创建时间", created_at)
72
+
73
+ console.print(table)
74
+
75
+ return {
76
+ "site": site_name,
77
+ "account": account,
78
+ "password": password,
79
+ "encrypted": encrypted,
80
+ "nickname": nickname,
81
+ "category": category,
82
+ "last_used_at": last_used_at,
83
+ }
@@ -0,0 +1,59 @@
1
+ """import 命令"""
2
+
3
+ import typer
4
+ import json
5
+ from ..db import get_cursor, is_initialized
6
+ from dong import json_output, DongError
7
+
8
+ console = typer.Console()
9
+
10
+
11
+ @json_output
12
+ def import_cmd(
13
+ input: str = typer.Option(..., "--input", "-i", help="输入文件路径"),
14
+ ):
15
+ """从 JSON 导入账号"""
16
+ from ..db import is_initialized
17
+
18
+ if not is_initialized():
19
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
20
+
21
+ try:
22
+ with open(input, "r", encoding="utf-8") as f:
23
+ data = json.load(f)
24
+ except FileNotFoundError:
25
+ raise DongError("FILE_NOT_FOUND", f"文件不存在:{input}")
26
+ except json.JSONDecodeError as e:
27
+ raise DongError("INVALID_JSON", f"JSON 格式错误:{e}")
28
+
29
+ accounts = data.get("accounts", [])
30
+ if not accounts:
31
+ return {"message": "文件中没有账号数据", "imported": 0}
32
+
33
+ with get_cursor() as cur:
34
+ imported = 0
35
+ for account in accounts:
36
+ site = account.get("site")
37
+ account_field = account.get("account")
38
+ password_field = account.get("password")
39
+ nickname = account.get("nickname")
40
+ category = account.get("category")
41
+ note = account.get("note")
42
+ encrypted = account.get("encrypted", False)
43
+
44
+ if not site or not account_field:
45
+ continue
46
+
47
+ try:
48
+ cur.execute("""
49
+ INSERT INTO accounts (site, account, password, nickname, category, note, encrypted)
50
+ VALUES (?, ?, ?, ?, ?, ?, ?)
51
+ """, (site.lower(), account_field, password_field, nickname, category, note, encrypted))
52
+ imported += 1
53
+ except sqlite3.IntegrityError:
54
+ # 跳过已存在的网站
55
+ pass
56
+
57
+ console.print(f"[green]✅ 已导入 {imported} 个账号[/green]")
58
+
59
+ return {"message": "导入成功", "imported": imported}
@@ -0,0 +1,35 @@
1
+ """init 命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from dong import json_output, DongError
6
+
7
+ console = Console()
8
+
9
+
10
+ @json_output
11
+ def init(
12
+ master_password: str = typer.Option(None, "--master-password", help="主密码(用于加密)"),
13
+ ):
14
+ """初始化数据库"""
15
+ from ..db import PassDatabase, init_database, is_initialized
16
+
17
+ if is_initialized():
18
+ console.print("[yellow]数据库已初始化[/yellow]")
19
+ return {"message": "数据库已初始化"}
20
+
21
+ # 设置主密码
22
+ password = master_password
23
+ if not password:
24
+ password = typer.prompt("设置主密码(用于加密,留空则明文存储)", password=True)
25
+
26
+ # 初始化数据库
27
+ init_database()
28
+
29
+ if password:
30
+ console.print("[green]✅ 数据库初始化成功(加密模式)[/green]")
31
+ console.print("[yellow]⚠️ 请妥善保管主密码!忘记后密码将无法恢复。[/yellow]")
32
+ else:
33
+ console.print("[green]✅ 数据库初始化成功(明文模式)[/green]")
34
+
35
+ return {"message": "数据库初始化成功", "encrypted": bool(password)}
@@ -0,0 +1,86 @@
1
+ """ls 命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from ..db import PassDatabase, get_cursor, is_initialized
7
+ from dong import json_output, DongError
8
+
9
+ console = Console()
10
+
11
+
12
+ @json_output
13
+ def list_accounts(
14
+ category: str = typer.Option(None, "--category", "-c", help="按分类筛选"),
15
+ limit: int = typer.Option(50, "--limit", "-l", help="限制数量"),
16
+ ):
17
+ """列出所有账号"""
18
+ from ..db import get_db_path
19
+
20
+ if not is_initialized():
21
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
22
+
23
+ db_path = get_db_path()
24
+ if not db_path.exists():
25
+ raise DongError("NOT_INITIALIZED", "数据库不存在,请运行 dong-pass init")
26
+
27
+ with get_cursor() as cur:
28
+ if category:
29
+ cur.execute("""
30
+ SELECT * FROM accounts WHERE category = ?
31
+ ORDER BY last_used_at DESC
32
+ LIMIT ?
33
+ """, (category, limit))
34
+ else:
35
+ cur.execute("""
36
+ SELECT * FROM accounts
37
+ ORDER BY last_used_at DESC
38
+ LIMIT ?
39
+ """, (limit,))
40
+
41
+ rows = cur.fetchall()
42
+
43
+ if not rows:
44
+ console.print("暂无账号记录")
45
+ return {"items": [], "total": 0}
46
+
47
+ # 渲染表格
48
+ table = Table(title=f"账号列表 {f'(分类:{category})' if category else ''}")
49
+ table.add_column("ID", justify="right", style="cyan")
50
+ table.add_column("网站", style="green")
51
+ table.add_column("账号", style="yellow")
52
+ table.add_column("昵称", style="blue")
53
+ table.add_column("分类", style="magenta")
54
+ table.add_column("最后使用")
55
+
56
+ items = []
57
+ for row in rows:
58
+ expire_id, site, account, password, nickname, category, note, encrypted, created_at, updated_at, last_used_at = row
59
+
60
+ nickname_str = nickname or "-"
61
+ category_str = category or "-"
62
+ last_used_str = last_used_at[:10] if last_used_at else "-"
63
+
64
+ status = "🔒" if encrypted else "🔓"
65
+
66
+ table.add_row(
67
+ str(expire_id),
68
+ site,
69
+ account,
70
+ nickname_str,
71
+ category_str,
72
+ last_used_str,
73
+ )
74
+
75
+ items.append({
76
+ "id": expire_id,
77
+ "site": site,
78
+ "account": account,
79
+ "nickname": nickname,
80
+ "category": category,
81
+ "encrypted": encrypted,
82
+ })
83
+
84
+ console.print(table)
85
+
86
+ return {"items": items, "total": len(items)}
@@ -0,0 +1,58 @@
1
+ """remove_encrypt 命令"""
2
+
3
+ import typer
4
+ from ..db import PassDatabase, get_cursor, is_initialized
5
+ from dong import json_output, DongError
6
+
7
+ console = typer.Console()
8
+
9
+
10
+ @json_output
11
+ def remove_encrypt():
12
+ """移除加密(明文化)"""
13
+ if not is_initialized():
14
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
15
+
16
+ # 检查是否已加密
17
+ with get_cursor() as cur:
18
+ cur.execute("SELECT encrypted FROM accounts LIMIT 1")
19
+ row = cur.fetchone()
20
+
21
+ if not row or not row["encrypted"]:
22
+ raise DongError("NOT_ENCRYPTED", "数据库不是加密模式")
23
+
24
+ # 输入主密码
25
+ master_password = typer.prompt("输入主密码以验证", password=True)
26
+
27
+ if not master_password:
28
+ raise DongError("INVALID_PASSWORD", "主密码不能为空")
29
+
30
+ # 验证密码
31
+ db = PassDatabase(master_password)
32
+ try:
33
+ # 测试解密一个密码
34
+ with get_cursor(master_password) as cur:
35
+ cur.execute("SELECT password FROM accounts LIMIT 1")
36
+ row = cur.fetchone()
37
+ db.decrypt(row["password"])
38
+ except Exception:
39
+ raise DongError("INVALID_PASSWORD", "主密码错误")
40
+
41
+ # 明文化所有密码
42
+ with get_cursor(master_password) as cur:
43
+ cur.execute("SELECT id, password FROM accounts")
44
+ rows = cur.fetchall()
45
+
46
+ for row in rows:
47
+ account_id = row["id"]
48
+ encrypted_pwd = row["password"]
49
+ # 转换为明文
50
+ plain_pwd = db.decrypt(encrypted_pwd)
51
+ cur.execute(
52
+ "UPDATE accounts SET password = ?, encrypted = 0 WHERE id = ?",
53
+ (plain_pwd, account_id)
54
+ )
55
+
56
+ console.print("[green]✅ 加密已移除,所有密码已明文化[/green]")
57
+
58
+ return {"message": "加密已移除"}
@@ -0,0 +1,73 @@
1
+ """search 命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from ..db import PassDatabase, get_cursor, is_initialized
7
+ from dong import json_output, DongError
8
+
9
+ console = Console()
10
+
11
+
12
+ @json_output
13
+ def search(
14
+ keyword: str = typer.Argument(..., help="搜索关键词"),
15
+ category: str = typer.Option(None, "--category", help="按分类筛选"),
16
+ ):
17
+ """搜索账号"""
18
+ if not is_initialized():
19
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
20
+
21
+ search_pattern = f"%{keyword}%"
22
+ with get_cursor() as cur:
23
+ if category:
24
+ cur.execute("""
25
+ SELECT * FROM accounts WHERE site LIKE ? OR account LIKE ? OR nickname LIKE ? OR category = ?
26
+ ORDER BY last_used_at DESC
27
+ """, (search_pattern, search_pattern, search_pattern, category))
28
+ else:
29
+ cur.execute("""
30
+ SELECT * FROM accounts WHERE site LIKE ? OR account LIKE ? OR nickname LIKE ?
31
+ ORDER BY last_used_at DESC
32
+ """, (search_pattern, search_pattern, search_pattern))
33
+
34
+ rows = cur.fetchall()
35
+
36
+ if not rows:
37
+ console.print(f"[yellow]未找到包含 '{keyword}' 的账号[/yellow]")
38
+ return {"items": [], "total": 0}
39
+
40
+ # 渲染表格
41
+ table = Table(title=f"搜索结果:{keyword}")
42
+ table.add_column("ID", justify="right", style="cyan")
43
+ table.add_column("网站", style="green")
44
+ table.add_column("账号", style="yellow")
45
+ table.add_column("昵称", style="blue")
46
+ table.add_column("分类", style="magenta")
47
+
48
+ items = []
49
+ for row in rows:
50
+ expire_id, site, account, password, nickname, category, note, encrypted, created_at, updated_at, last_used_at = row
51
+
52
+ nickname_str = nickname or "-"
53
+ category_str = category or "-"
54
+
55
+ table.add_row(
56
+ str(expire_id),
57
+ site,
58
+ account,
59
+ nickname_str,
60
+ category_str,
61
+ )
62
+
63
+ items.append({
64
+ "id": expire_id,
65
+ "site": site,
66
+ "account": account,
67
+ "nickname": nickname,
68
+ "category": category,
69
+ })
70
+
71
+ console.print(table)
72
+
73
+ return {"items": items, "total": len(items)}
@@ -0,0 +1,33 @@
1
+ """set_master 命令"""
2
+
3
+ import typer
4
+ from ..db import PassDatabase, get_cursor, is_initialized
5
+ from dong import json_output, DongError
6
+
7
+ console = typer.Console()
8
+
9
+
10
+ @json_output
11
+ def set_master():
12
+ """设置主密码(首次设置)"""
13
+ if not is_initialized():
14
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
15
+
16
+ # 检查是否已有主密码
17
+ with get_cursor() as cur:
18
+ cur.execute("SELECT encrypted FROM accounts LIMIT 1")
19
+ row = cur.fetchone()
20
+
21
+ if row and row["encrypted"]:
22
+ raise DongError("ALREADY_ENCRYPTED", "数据库已经是加密模式,请使用 change-master 命令")
23
+
24
+ # 设置新主密码
25
+ master_password = typer.prompt("设置主密码(用于加密),不能为空", password=True, confirmation_prompt=True)
26
+
27
+ if not master_password:
28
+ raise DongError("INVALID_PASSWORD", "主密码不能为空")
29
+
30
+ console.print("[green]✅ 主密码设置成功[/green]")
31
+ console.print("[yellow]⚠️ 请妥善保管主密码!忘记后密码将无法恢复。[/yellow]")
32
+
33
+ return {"message": "主密码设置成功", "encrypted": True}
@@ -0,0 +1,62 @@
1
+ """stats 命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from ..db import PassDatabase, get_cursor, is_initialized
7
+ from dong import json_output, DongError
8
+
9
+ console = Console()
10
+
11
+
12
+ @json_output
13
+ def stats():
14
+ """统计信息"""
15
+ from ..db import get_db_path
16
+
17
+ if not is_initialized():
18
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
19
+
20
+ db_path = get_db_path()
21
+ if not db_path.exists():
22
+ raise DongError("NOT_INITIALIZED", "数据库不存在,请运行 dong-pass init")
23
+
24
+ with get_cursor() as cur:
25
+ # 总数
26
+ cur.execute("SELECT COUNT(*) FROM accounts")
27
+ total = cur.fetchone()[0]
28
+
29
+ # 按分类统计
30
+ cur.execute("""
31
+ SELECT category, COUNT(*) as count
32
+ FROM accounts
33
+ GROUP BY category
34
+ """)
35
+ category_stats = cur.fetchall()
36
+
37
+ if total == 0:
38
+ console.print("暂无账号记录")
39
+ return {"total": 0, "categories": []}
40
+
41
+ # 渲染表格
42
+ table = Table(title="账号统计")
43
+ table.add_column("分类", style="blue")
44
+ table.add_column("数量", justify="right", style="cyan")
45
+ table.add_column("占比", justify="right", style="green")
46
+
47
+ categories = []
48
+ for row in category_stats:
49
+ category, count = row
50
+ category = category or "未分类"
51
+ percentage = (count / total * 100)
52
+ table.add_row(category, str(count), f"{percentage:.1f}%")
53
+ categories.append({
54
+ "category": category,
55
+ "count": count,
56
+ "percentage": percentage,
57
+ })
58
+
59
+ table.add_row("总计", str(total), "100%")
60
+ console.print(table)
61
+
62
+ return {"total": total, "categories": categories}
@@ -0,0 +1,80 @@
1
+ """update 命令"""
2
+
3
+ import typer
4
+ from ..db import PassDatabase, get_cursor
5
+ from dong import json_output, DongError
6
+
7
+ console = typer.Console()
8
+
9
+
10
+ @json_output
11
+ def update(
12
+ site: str = typer.Argument(..., help="网站名称"),
13
+ account: str = typer.Option(None, "--account", help="账号"),
14
+ password: str = typer.Option(None, "--password", help="密码"),
15
+ nickname: str = typer.Option(None, "--nickname", help="昵称"),
16
+ category: str = typer.Option(None, "--category", help="分类"),
17
+ note: str = typer.Option(None, "--note", help="备注"),
18
+ ):
19
+ """更新账号"""
20
+ from ..db import is_initialized, PassDatabase
21
+
22
+ if not is_initialized():
23
+ raise DongError("NOT_INITIALIZED", "请先运行 dong-pass init")
24
+
25
+ # 判断是否需要主密码(如果修改密码且原记录已加密)
26
+ with get_cursor() as cur:
27
+ cur.execute("SELECT encrypted FROM accounts WHERE site = ?", (site.lower(),))
28
+ row = cur.fetchone()
29
+
30
+ if not row:
31
+ raise DongError("NOT_FOUND", f"未找到网站:{site}")
32
+
33
+ encrypted = row["encrypted"]
34
+ master_password = None
35
+ if password and encrypted:
36
+ master_password = typer.prompt("输入主密码以解密密码", password=True)
37
+
38
+ # 构建更新语句
39
+ updates = []
40
+ params = []
41
+
42
+ if account:
43
+ updates.append("account = ?")
44
+ params.append(account)
45
+ if password:
46
+ db = PassDatabase(master_password)
47
+ if encrypted:
48
+ encrypted_pwd = db.encrypt(password)
49
+ else:
50
+ encrypted_pwd = password
51
+ updates.append("password = ?")
52
+ params.append(encrypted_pwd)
53
+ if nickname:
54
+ updates.append("nickname = ?")
55
+ params.append(nickname)
56
+ if category:
57
+ updates.append("category = ?")
58
+ params.append(category)
59
+ if note:
60
+ updates.append("note = ?")
61
+ params.append(note)
62
+
63
+ if not updates:
64
+ raise DongError("NO_UPDATES", "没有指定要更新的字段")
65
+
66
+ updates.append("updated_at = CURRENT_TIMESTAMP")
67
+ params.append(site.lower())
68
+
69
+ with get_cursor(master_password) as cur:
70
+ cur.execute(
71
+ f"UPDATE accounts SET {', '.join(updates)} WHERE site = ?",
72
+ params
73
+ )
74
+
75
+ if cur.rowcount == 0:
76
+ raise DongError("NOT_FOUND", f"未找到网站:{site}")
77
+
78
+ console.print(f"[green]✅ 已更新账号:{site}[/green]")
79
+
80
+ return {"site": site.lower(), "updated": True}
@@ -0,0 +1,82 @@
1
+ """数据库连接管理"""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from typing import Iterator
6
+ from contextlib import contextmanager
7
+ from cryptography.fernet import Fernet
8
+ import getpass
9
+
10
+
11
+ class PassDatabase:
12
+ """密码咚数据库类"""
13
+
14
+ def __init__(self, master_password: str = None):
15
+ self.db_path = self.get_db_path()
16
+ self.master_password = master_password
17
+ self.cipher = None
18
+ if master_password:
19
+ self.cipher = self._get_cipher(master_password)
20
+
21
+ @classmethod
22
+ def get_db_path(cls) -> Path:
23
+ """获取数据库路径"""
24
+ db_path = Path.home() / ".dong" / "accounts.db"
25
+ db_path.parent.mkdir(parents=True, exist_ok=True)
26
+ return db_path
27
+
28
+ def _get_cipher(self, password: str) -> Fernet:
29
+ """获取加密器"""
30
+ # 从密码生成密钥
31
+ key = Fernet.generate_key()
32
+ # 使用密码作为种子(实际应用中应该更安全的方式)
33
+ import hashlib
34
+ salt = b'dong-pass-salt-2026'
35
+ kdf_key = hashlib.pbkdf2_hmac(
36
+ 'sha256',
37
+ password.encode(),
38
+ salt,
39
+ 100000
40
+ )
41
+ # 转换为 Fernet key 格式
42
+ fernet_key = Fernet.generate_key(kdf_key)
43
+ return Fernet(fernet_key)
44
+
45
+ def encrypt(self, text: str) -> str:
46
+ """加密文本"""
47
+ if not self.cipher:
48
+ return text
49
+ return self.cipher.encrypt(text.encode()).decode()
50
+
51
+ def decrypt(self, encrypted: str) -> str:
52
+ """解密文本"""
53
+ if not self.cipher:
54
+ return encrypted
55
+ return self.cipher.decrypt(encrypted.encode()).decode()
56
+
57
+
58
+ @contextmanager
59
+ def get_cursor(master_password: str = None) -> Iterator[sqlite3.Cursor]:
60
+ """获取数据库游标"""
61
+ db = PassDatabase(master_password)
62
+ conn = sqlite3.connect(str(db.db_path))
63
+ conn.row_factory = sqlite3.Row
64
+ cursor = conn.cursor()
65
+ try:
66
+ yield cursor
67
+ conn.commit()
68
+ except Exception:
69
+ conn.rollback()
70
+ raise
71
+ finally:
72
+ conn.close()
73
+
74
+
75
+ def get_db_path() -> Path:
76
+ """获取数据库路径"""
77
+ return PassDatabase.get_db_path()
78
+
79
+
80
+ def close_connection():
81
+ """关闭数据库连接"""
82
+ pass
@@ -0,0 +1,60 @@
1
+ """数据库 Schema 管理"""
2
+
3
+ from .connection import PassDatabase
4
+ from datetime import datetime
5
+
6
+
7
+ class PassSchemaManager:
8
+ """密码咚 Schema 管理器"""
9
+
10
+ def init_schema(self) -> None:
11
+ with PassDatabase.get_cursor() as cur:
12
+ self._create_accounts_table()
13
+ self._create_indexes()
14
+
15
+ def _create_accounts_table(self) -> None:
16
+ with PassDatabase.get_cursor() as cur:
17
+ cur.execute("""
18
+ CREATE TABLE IF NOT EXISTS accounts (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ site TEXT NOT NULL UNIQUE,
21
+ account TEXT NOT NULL,
22
+ password TEXT NOT NULL,
23
+ nickname TEXT,
24
+ category TEXT,
25
+ note TEXT,
26
+ encrypted BOOLEAN DEFAULT 0,
27
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
28
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
29
+ last_used_at TEXT
30
+ )
31
+ """)
32
+
33
+ def _create_indexes(self) -> None:
34
+ with PassDatabase.get_cursor() as cur:
35
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_accounts_category ON accounts(category)")
36
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_accounts_last_used ON accounts(last_used_at)")
37
+
38
+
39
+ def init_database() -> dict:
40
+ """初始化数据库"""
41
+ manager = PassSchemaManager()
42
+ manager.init_schema()
43
+ return {"message": "数据库初始化成功"}
44
+
45
+
46
+ def is_initialized() -> bool:
47
+ """检查数据库是否已初始化"""
48
+ db_path = PassDatabase.get_db_path()
49
+ if not db_path.exists():
50
+ return False
51
+
52
+ try:
53
+ conn = sqlite3.connect(str(db_path))
54
+ cur = conn.cursor()
55
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'")
56
+ result = cur.fetchone() is not None
57
+ conn.close()
58
+ return result
59
+ except Exception:
60
+ return False