evm-cli 2.4.0__py3-none-any.whl

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.
evm/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ EVM - Environment Variable Manager
3
+ A command-line tool for managing environment variables.
4
+ """
5
+
6
+ __version__ = "2.4.0"
7
+ __author__ = "EVM Tool"
8
+
9
+ from .exceptions import (
10
+ BackupError,
11
+ CommandNotFoundError,
12
+ CorruptedStorageError,
13
+ DecryptionError,
14
+ EditorError,
15
+ EVMError,
16
+ ExportError,
17
+ GroupNotFoundError,
18
+ GroupOperationError,
19
+ ImportFailedError,
20
+ KeyAlreadyExistsError,
21
+ KeyNotFoundError,
22
+ LockTimeoutError,
23
+ OperationCancelledError,
24
+ SchemaError,
25
+ StorageError,
26
+ StoragePermissionError,
27
+ ValidationError,
28
+ )
29
+ from .manager import EnvironmentManager
30
+
31
+ __all__ = [
32
+ '__version__',
33
+ '__author__',
34
+ # Core API
35
+ 'EnvironmentManager',
36
+ # Exceptions
37
+ 'EVMError',
38
+ 'KeyNotFoundError',
39
+ 'KeyAlreadyExistsError',
40
+ 'StorageError',
41
+ 'CorruptedStorageError',
42
+ 'StoragePermissionError',
43
+ 'LockTimeoutError',
44
+ 'ExportError',
45
+ 'ImportFailedError',
46
+ 'CommandNotFoundError',
47
+ 'GroupNotFoundError',
48
+ 'GroupOperationError',
49
+ 'BackupError',
50
+ 'EditorError',
51
+ 'DecryptionError',
52
+ 'ValidationError',
53
+ 'SchemaError',
54
+ 'OperationCancelledError',
55
+ ]
evm/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Entry point for running EVM as a module: python -m evm
4
+ """
5
+
6
+ from evm.cli import main
7
+
8
+ if __name__ == '__main__':
9
+ raise SystemExit(main())
evm/_completion.py ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EVM Shell 补全脚本生成
4
+
5
+ 支持 bash, zsh, fish。
6
+ M3: 为 get/delete/edit/expand/validate/rename/copy 等命令
7
+ 提供动态变量名补全(通过 evm list --json --quiet 获取 key 列表)。
8
+ """
9
+
10
+
11
+ def generate_bash_completion(commands: list) -> str:
12
+ """生成 bash 补全脚本(含动态变量名补全)"""
13
+ cmds = ' '.join(commands)
14
+ # 需要变量名补全的命令
15
+ key_cmds = ' '.join([
16
+ 'get', 'delete', 'edit', 'expand', 'validate',
17
+ 'rename', 'copy', 'setg', 'getg', 'deleteg', 'listg',
18
+ ])
19
+ return f'''# EVM bash completion
20
+ # L3: 仅在 evm 命令可用时注册补全
21
+ if ! command -v evm &>/dev/null; then
22
+ return 2>/dev/null || exit
23
+ fi
24
+
25
+ _evm_completions() {{
26
+ local cur prev opts
27
+ COMPREPLY=()
28
+ cur="${{COMP_WORDS[COMP_CWORD]}}"
29
+ prev="${{COMP_WORDS[COMP_CWORD-1]}}"
30
+
31
+ # Top-level commands
32
+ local commands="{cmds}"
33
+ local global_opts="--help --version --verbose --env-file --json --quiet --dry-run --force"
34
+ local key_cmds="{key_cmds}"
35
+
36
+ # 动态获取变量名列表
37
+ _evm_keys() {{
38
+ evm list --json --quiet 2>/dev/null | \\
39
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('data',{{}}).keys()))" 2>/dev/null
40
+ }}
41
+
42
+ case "${{prev}}" in
43
+ evm)
44
+ COMPREPLY=( $(compgen -W "${{commands}} ${{global_opts}}" -- "${{cur}}") )
45
+ return 0
46
+ ;;
47
+ --env-file)
48
+ COMPREPLY=( $(compgen -f -- "${{cur}}") )
49
+ return 0
50
+ ;;
51
+ --format|-f)
52
+ COMPREPLY=( $(compgen -W "json env sh backup" -- "${{cur}}") )
53
+ return 0
54
+ ;;
55
+ set)
56
+ COMPREPLY=( $(compgen -W "--secret --help" -- "${{cur}}") )
57
+ return 0
58
+ ;;
59
+ get|delete|edit|expand|validate|rename|copy)
60
+ COMPREPLY=( $(compgen -W "$(_evm_keys) --secret --help" -- "${{cur}}") )
61
+ return 0
62
+ ;;
63
+ setg|getg|deleteg|listg)
64
+ # 分组名补全(从现有 key 中提取分组前缀)
65
+ local groups
66
+ groups=$(evm groups --json --quiet 2>/dev/null | \\
67
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('data',{{}}).get('groups',{{}}).keys()))" 2>/dev/null)
68
+ COMPREPLY=( $(compgen -W "${{groups}} --help" -- "${{cur}}") )
69
+ return 0
70
+ ;;
71
+ export)
72
+ COMPREPLY=( $(compgen -W "--format --output --group --help" -- "${{cur}}") )
73
+ return 0
74
+ ;;
75
+ load)
76
+ COMPREPLY=( $(compgen -W "--format --replace --group --nest --help" -- "${{cur}}") )
77
+ return 0
78
+ ;;
79
+ completion)
80
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "${{cur}}") )
81
+ return 0
82
+ ;;
83
+ esac
84
+
85
+ # 如果前一个词是需要 key 补全的命令,尝试补全变量名
86
+ for kc in ${{key_cmds}}; do
87
+ if [[ "${{COMP_WORDS[1]}}" == "$kc" ]]; then
88
+ COMPREPLY=( $(compgen -W "$(_evm_keys)" -- "${{cur}}") )
89
+ return 0
90
+ fi
91
+ done
92
+
93
+ COMPREPLY=( $(compgen -W "${{commands}} ${{global_opts}}" -- "${{cur}}") )
94
+ }}
95
+ complete -F _evm_completions evm
96
+ '''
97
+
98
+
99
+ def generate_zsh_completion(commands: list) -> str:
100
+ """生成 zsh 补全脚本(含动态变量名补全)"""
101
+ return f'''#compdef evm
102
+
103
+ # L3: 仅在 evm 命令可用时注册补全
104
+ if (( ! $+commands[evm] )); then
105
+ return
106
+ fi
107
+
108
+ _evm() {{
109
+ local -a commands
110
+ commands=(
111
+ {chr(10).join(f" '{c}:{c} command'" for c in commands)}
112
+ )
113
+
114
+ # 动态获取变量名列表
115
+ _evm_keys() {{
116
+ local keys
117
+ keys=(${{(f)"$(evm list --json --quiet 2>/dev/null | \\
118
+ python3 -c "import sys,json; d=json.load(sys.stdin); print('\\n'.join(d.get('data',{{}}).keys()))" 2>/dev/null)"}})
119
+ _describe 'variable' keys
120
+ }}
121
+
122
+ # 动态获取分组名列表
123
+ _evm_groups() {{
124
+ local groups
125
+ groups=(${{(f)"$(evm groups --json --quiet 2>/dev/null | \\
126
+ python3 -c "import sys,json; d=json.load(sys.stdin); print('\\n'.join(d.get('data',{{}}).get('groups',{{}}).keys()))" 2>/dev/null)"}})
127
+ _describe 'group' groups
128
+ }}
129
+
130
+ _arguments -C \\
131
+ '--help[Show help]' \\
132
+ '--version[Show version]' \\
133
+ '(-v --verbose)'{{-v,--verbose}}'[Show detailed info]' \\
134
+ '--env-file[Storage file path]:file:_files' \\
135
+ '--json[Output structured JSON]' \\
136
+ '--quiet[Suppress output]' \\
137
+ '--dry-run[Preview changes]' \\
138
+ '--force[Skip confirmation]' \\
139
+ '1: :->command' \\
140
+ '*:: :->args'
141
+
142
+ case $state in
143
+ command)
144
+ _describe 'command' commands
145
+ ;;
146
+ args)
147
+ case $words[1] in
148
+ get|delete|edit|expand|validate)
149
+ _evm_keys
150
+ ;;
151
+ rename|copy)
152
+ _evm_keys
153
+ ;;
154
+ setg|getg|deleteg|listg)
155
+ _evm_groups
156
+ ;;
157
+ completion)
158
+ _values 'shell' bash zsh fish
159
+ ;;
160
+ export)
161
+ _arguments \\
162
+ '(-f --format)'{{-f,--format}}'[Format]:format:(json env sh)' \\
163
+ '(-o --output)'{{-o,--output}}'[Output file]:file:_files' \\
164
+ '(-g --group)'{{-g,--group}}'[Group name]:group'
165
+ ;;
166
+ esac
167
+ ;;
168
+ esac
169
+ }}
170
+
171
+ _evm "$@"
172
+ '''
173
+
174
+
175
+ def generate_fish_completion(commands: list) -> str:
176
+ """生成 fish 补全脚本(含动态变量名补全)"""
177
+ lines = ['# EVM fish completion', '']
178
+
179
+ # L3: 仅在 evm 命令可用时注册补全
180
+ lines.append('# Guard: only register if evm is in PATH')
181
+ lines.append('if not command -q evm')
182
+ lines.append(' exit')
183
+ lines.append('end')
184
+ lines.append('')
185
+
186
+ # Disable file completion by default
187
+ lines.append('complete -c evm -f')
188
+ lines.append('')
189
+
190
+ # Global options
191
+ lines.append('# Global options')
192
+ lines.append("complete -c evm -l help -d 'Show help'")
193
+ lines.append("complete -c evm -l version -d 'Show version'")
194
+ lines.append("complete -c evm -s v -l verbose -d 'Show detailed info'")
195
+ lines.append("complete -c evm -l env-file -d 'Storage file path' -r -F")
196
+ lines.append("complete -c evm -l json -d 'Output structured JSON'")
197
+ lines.append("complete -c evm -l quiet -s q -d 'Suppress output'")
198
+ lines.append("complete -c evm -l dry-run -d 'Preview changes'")
199
+ lines.append("complete -c evm -l force -d 'Skip confirmation'")
200
+ lines.append('')
201
+
202
+ # Commands
203
+ lines.append('# Commands')
204
+ for cmd in commands:
205
+ lines.append(f"complete -c evm -n '__fish_use_subcommand' -a '{cmd}' -d '{cmd}'")
206
+ lines.append('')
207
+
208
+ # Dynamic variable name completion helper
209
+ lines.append('# Dynamic variable name completion')
210
+ lines.append('function __evm_keys')
211
+ lines.append(' evm list --json --quiet 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); [print(k) for k in d.get(\'data\',{})]" 2>/dev/null')
212
+ lines.append('end')
213
+ lines.append('')
214
+
215
+ # Dynamic group name completion helper
216
+ lines.append('function __evm_groups')
217
+ lines.append(' evm groups --json --quiet 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); [print(k) for k in d.get(\'data\',{}).get(\'groups\',{})]" 2>/dev/null')
218
+ lines.append('end')
219
+ lines.append('')
220
+
221
+ # Sub-options
222
+ lines.append('# Sub-options')
223
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from set' -l secret -d 'Encrypt value'")
224
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from get' -l secret -d 'Decrypt value'")
225
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from export' -s f -l format -d 'Format' -xa 'json env sh'")
226
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from export' -s o -l output -d 'Output file' -r -F")
227
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from load' -s f -l format -d 'Format' -xa 'json env backup'")
228
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from load' -s r -l replace -d 'Replace mode'")
229
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from load' -s n -l nest -d 'Nested JSON'")
230
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from completion' -xa 'bash zsh fish'")
231
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from history' -s n -l limit -d 'Number of entries' -x")
232
+ lines.append("complete -c evm -n '__fish_seen_subcommand_from schema' -xa 'set get delete validate list'")
233
+ lines.append('')
234
+
235
+ # Variable name completion for relevant commands
236
+ lines.append('# Variable name completion')
237
+ for cmd in ['get', 'delete', 'edit', 'expand', 'validate', 'rename', 'copy']:
238
+ lines.append(f"complete -c evm -n '__fish_seen_subcommand_from {cmd}' -a '(__evm_keys)'")
239
+ lines.append('')
240
+
241
+ # Group name completion
242
+ lines.append('# Group name completion')
243
+ for cmd in ['setg', 'getg', 'deleteg', 'listg']:
244
+ lines.append(f"complete -c evm -n '__fish_seen_subcommand_from {cmd}' -a '(__evm_groups)'")
245
+
246
+ return '\n'.join(lines) + '\n'
247
+
248
+
249
+ SHELL_GENERATORS = {
250
+ 'bash': generate_bash_completion,
251
+ 'zsh': generate_zsh_completion,
252
+ 'fish': generate_fish_completion,
253
+ }
evm/_crypto.py ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EVM 加密模块
4
+
5
+ 提供安全的加密/解密功能:
6
+ - HKDF-Expand: 密钥分离(加密密钥 + MAC 密钥)
7
+ - HMAC-CTR: 基于 HMAC 的 CTR 模式流密码
8
+ - HMAC-SHA256: 认证加密(Encrypt-then-MAC)
9
+
10
+ 格式: ENCv3:<salt_b64>:<iv_b64>:<mac_b64>:<ciphertext_b64>
11
+ """
12
+
13
+ import base64
14
+ import hashlib
15
+ import hmac
16
+ import os
17
+ import struct
18
+
19
+ from .exceptions import DecryptionError
20
+
21
+ # SHA-256 输出长度(字节),用于 HKDF-Expand 计算
22
+ HKDF_HASH_LEN = 32
23
+
24
+
25
+ def hkdf_expand(prk: bytes, info: bytes, length: int = 32) -> bytes:
26
+ """HKDF-Expand (RFC 5869)
27
+
28
+ 从伪随机密钥 (PRK) 派生指定长度的输出密钥材料。
29
+
30
+ Args:
31
+ prk: 伪随机密钥(至少 HKDF_HASH_LEN 字节)
32
+ info: 上下文和用途信息
33
+ length: 输出密钥材料长度(字节)
34
+
35
+ Returns:
36
+ 派生的密钥材料
37
+ """
38
+ n = (length + HKDF_HASH_LEN - 1) // HKDF_HASH_LEN
39
+ okm = b''
40
+ t = b''
41
+ for i in range(1, n + 1):
42
+ t = hmac.new(
43
+ prk, t + info + bytes([i]), hashlib.sha256
44
+ ).digest()
45
+ okm += t
46
+ return okm[:length]
47
+
48
+
49
+ def derive_subkeys(master_key: bytes, salt: bytes) -> tuple:
50
+ """从主密钥派生独立的加密和 MAC 子密钥
51
+
52
+ 使用 HKDF-Expand 和不同的 info 字符串确保密钥分离。
53
+
54
+ Args:
55
+ master_key: PBKDF2 输出的主密钥
56
+ salt: 用于 HKDF info 的随机盐
57
+
58
+ Returns:
59
+ (enc_key, mac_key) 各 32 字节
60
+ """
61
+ enc_key = hkdf_expand(master_key, salt + b'evm-encryption', 32)
62
+ mac_key = hkdf_expand(master_key, salt + b'evm-authentication', 32)
63
+ return enc_key, mac_key
64
+
65
+
66
+ def hmac_ctr_keystream(key: bytes, iv: bytes, length: int) -> bytes:
67
+ """生成 HMAC-CTR 模式密钥流
68
+
69
+ keystream = HMAC(key, IV || 0) || HMAC(key, IV || 1) || ...
70
+
71
+ Args:
72
+ key: 加密密钥
73
+ iv: 随机初始化向量(计数器起始值)
74
+ length: 需要的密钥流长度(字节)
75
+
76
+ Returns:
77
+ 指定长度的密钥流
78
+ """
79
+ stream = b''
80
+ counter = 0
81
+ while len(stream) < length:
82
+ block = hmac.new(
83
+ key, iv + struct.pack('>I', counter), hashlib.sha256
84
+ ).digest()
85
+ stream += block
86
+ counter += 1
87
+ return stream[:length]
88
+
89
+
90
+ def encrypt_v3(plaintext: str, derive_key_fn) -> str:
91
+ """v3 加密: HKDF 密钥分离 + HMAC-CTR + Encrypt-then-MAC
92
+
93
+ Args:
94
+ plaintext: 明文
95
+ derive_key_fn: 密钥派生函数 (salt) -> master_key
96
+
97
+ Returns:
98
+ ENCv3:<salt_b64>:<iv_b64>:<mac_b64>:<ciphertext_b64>
99
+ """
100
+ salt = os.urandom(16)
101
+ iv = os.urandom(16)
102
+ master_key = derive_key_fn(salt)
103
+ enc_key, mac_key = derive_subkeys(master_key, salt)
104
+
105
+ data_bytes = plaintext.encode('utf-8')
106
+
107
+ # HMAC-CTR 加密
108
+ keystream = hmac_ctr_keystream(enc_key, iv, len(data_bytes))
109
+ ciphertext = bytes(a ^ b for a, b in zip(data_bytes, keystream))
110
+
111
+ # Encrypt-then-MAC: HMAC 覆盖 salt + iv + ciphertext
112
+ mac = hmac.new(
113
+ mac_key, salt + iv + ciphertext, hashlib.sha256
114
+ ).digest()
115
+
116
+ salt_b64 = base64.b64encode(salt).decode('ascii')
117
+ iv_b64 = base64.b64encode(iv).decode('ascii')
118
+ mac_b64 = base64.b64encode(mac).decode('ascii')
119
+ ct_b64 = base64.b64encode(ciphertext).decode('ascii')
120
+
121
+ return f"ENCv3:{salt_b64}:{iv_b64}:{mac_b64}:{ct_b64}"
122
+
123
+
124
+ def decrypt_v3(encoded: str, derive_key_fn) -> str:
125
+ """v3 解密: 验证 MAC + HMAC-CTR 解密
126
+
127
+ Args:
128
+ encoded: salt_b64:iv_b64:mac_b64:ciphertext_b64
129
+ derive_key_fn: 密钥派生函数 (salt) -> master_key
130
+
131
+ Returns:
132
+ 解密后的明文
133
+
134
+ Raises:
135
+ DecryptionError: 格式错误或完整性校验失败
136
+ """
137
+ parts = encoded.split(':')
138
+ if len(parts) != 4:
139
+ raise DecryptionError("Invalid v3 encrypted data format")
140
+
141
+ try:
142
+ salt = base64.b64decode(parts[0])
143
+ iv = base64.b64decode(parts[1])
144
+ stored_mac = base64.b64decode(parts[2])
145
+ ciphertext = base64.b64decode(parts[3])
146
+ except Exception as e:
147
+ raise DecryptionError(f"Failed to decode v3 data: {e}")
148
+
149
+ master_key = derive_key_fn(salt)
150
+ enc_key, mac_key = derive_subkeys(master_key, salt)
151
+
152
+ # 验证 MAC(常量时间比较)
153
+ computed_mac = hmac.new(
154
+ mac_key, salt + iv + ciphertext, hashlib.sha256
155
+ ).digest()
156
+ if not hmac.compare_digest(stored_mac, computed_mac):
157
+ raise DecryptionError(
158
+ "Data integrity check failed — data may be corrupted or tampered"
159
+ )
160
+
161
+ # HMAC-CTR 解密
162
+ keystream = hmac_ctr_keystream(enc_key, iv, len(ciphertext))
163
+ plaintext = bytes(a ^ b for a, b in zip(ciphertext, keystream))
164
+
165
+ try:
166
+ return plaintext.decode('utf-8')
167
+ except UnicodeDecodeError as e:
168
+ raise DecryptionError(f"Decrypted data is not valid UTF-8: {e}")
169
+
170
+
171
+ __all__ = [
172
+ 'hkdf_expand',
173
+ 'derive_subkeys',
174
+ 'hmac_ctr_keystream',
175
+ 'encrypt_v3',
176
+ 'decrypt_v3',
177
+ ]
evm/_groups.py ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EVM 分组操作 Mixin
4
+
5
+ 从 manager.py 中提取的分组管理功能。
6
+ """
7
+
8
+
9
+ from ._typing import EnvironmentManagerProtocol
10
+ from .exceptions import (
11
+ GroupNotFoundError,
12
+ GroupOperationError,
13
+ KeyAlreadyExistsError,
14
+ KeyNotFoundError,
15
+ )
16
+
17
+
18
+ class GroupMixin(EnvironmentManagerProtocol):
19
+ """分组操作 mixin — 提供 setg/getg/deleteg/groups 等功能"""
20
+
21
+ def set_grouped(
22
+ self, group: str, key: str, value: str, dry_run: bool = False
23
+ ) -> str:
24
+ """设置分组变量"""
25
+ full_key = f"{group}:{key}" if group else key
26
+ if dry_run:
27
+ return f"[DRY-RUN] Would set: [{group}]{key} = {value}"
28
+ self._env_vars[full_key] = value
29
+ self._save_env_vars()
30
+ return f"Set: [{group}]{key} = {value}"
31
+
32
+ def get_grouped(self, group: str, key: str) -> str:
33
+ """获取分组变量
34
+
35
+ Raises:
36
+ KeyNotFoundError: 变量不存在
37
+ """
38
+ full_key = f"{group}:{key}" if group else key
39
+ value = self._env_vars.get(full_key)
40
+ if value is None and group:
41
+ value = self._env_vars.get(key)
42
+ if value is None:
43
+ raise KeyNotFoundError(full_key)
44
+ return value
45
+
46
+ def delete_grouped(
47
+ self, group: str, key: str, dry_run: bool = False
48
+ ) -> str:
49
+ """删除分组变量
50
+
51
+ Raises:
52
+ KeyNotFoundError: 变量不存在
53
+ """
54
+ full_key = f"{group}:{key}" if group else key
55
+ if full_key not in self._env_vars:
56
+ raise KeyNotFoundError(full_key)
57
+ if dry_run:
58
+ return f"[DRY-RUN] Would delete: [{group}]{key}"
59
+ del self._env_vars[full_key]
60
+ self._save_env_vars()
61
+ return f"Deleted: [{group}]{key}"
62
+
63
+ def list_groups(self) -> dict[str, int]:
64
+ """列出所有分组
65
+
66
+ Returns:
67
+ {group_name: variable_count} 字典
68
+ """
69
+ groups: dict[str, int] = {}
70
+ for key in self._env_vars:
71
+ if ':' in key:
72
+ group = key.split(':', 1)[0]
73
+ groups[group] = groups.get(group, 0) + 1
74
+ return groups
75
+
76
+ def delete_group(self, group: str, dry_run: bool = False) -> str:
77
+ """删除整个分组
78
+
79
+ Raises:
80
+ GroupOperationError: 尝试删除 default 组
81
+ GroupNotFoundError: 分组不存在
82
+ """
83
+ if group == 'default':
84
+ raise GroupOperationError(
85
+ "Cannot delete default namespace. Use 'clear' to remove all variables."
86
+ )
87
+
88
+ prefix = f"{group}:"
89
+ to_delete = [k for k in self._env_vars if k.startswith(prefix)]
90
+
91
+ if not to_delete:
92
+ raise GroupNotFoundError(group)
93
+
94
+ if dry_run:
95
+ return (
96
+ f"[DRY-RUN] Would delete group '{group}' "
97
+ f"and {len(to_delete)} variables"
98
+ )
99
+
100
+ for key in to_delete:
101
+ del self._env_vars[key]
102
+ self._save_env_vars()
103
+ return f"Deleted group '{group}' and all its variables ({len(to_delete)} total)"
104
+
105
+ def move_to_group(
106
+ self, key: str, new_group: str, dry_run: bool = False
107
+ ) -> str:
108
+ """移动变量到另一个分组
109
+
110
+ Raises:
111
+ KeyNotFoundError: 变量不存在
112
+ KeyAlreadyExistsError: 目标分组中已有同名 key
113
+ """
114
+ if key not in self._env_vars:
115
+ raise KeyNotFoundError(key)
116
+
117
+ new_key = f"{new_group}:{key}"
118
+ if new_key in self._env_vars and new_key != key:
119
+ raise KeyAlreadyExistsError(new_key)
120
+
121
+ if dry_run:
122
+ return f"[DRY-RUN] Would move: {key} -> {new_key}"
123
+
124
+ value = self._env_vars.pop(key)
125
+ self._env_vars[new_key] = value
126
+ self._save_env_vars()
127
+ return f"Moved: {key} -> {new_key}"