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/cli.py ADDED
@@ -0,0 +1,961 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EVM 命令行接口
4
+
5
+ 负责 argparse 解析、命令调度和输出格式化。
6
+
7
+ 输出模式:
8
+ - 默认: 人类可读文本 (stdout)
9
+ - --json: 结构化 JSON (stdout=数据, stderr=日志)
10
+ - --quiet: 静默模式 (仅退出码)
11
+
12
+ 退出码:
13
+ 0 — 成功
14
+ 1 — 通用错误 / 操作取消
15
+ 2 — 变量不存在 (KeyNotFoundError)
16
+ 3 — 存储错误 (StorageError / CorruptedStorageError / LockTimeoutError)
17
+ 4 — 输入格式错误 (ImportFailedError / ExportError)
18
+ 5 — 解密失败 (DecryptionError)
19
+ 6 — 校验失败 (ValidationError / SchemaError)
20
+ 7 — 分组错误 (GroupNotFoundError / GroupOperationError)
21
+ 8 — 备份错误 (BackupError)
22
+ 9 — 编辑器错误 (EditorError)
23
+ 10 — 命令未找到 (CommandNotFoundError)
24
+ """
25
+
26
+ import argparse
27
+ import sys
28
+ from typing import Optional
29
+
30
+ from ._completion import SHELL_GENERATORS
31
+ from ._json import json_error, json_output
32
+ from .exceptions import (
33
+ BackupError,
34
+ CommandNotFoundError,
35
+ CorruptedStorageError,
36
+ DecryptionError,
37
+ EditorError,
38
+ EVMError,
39
+ ExportError,
40
+ GroupNotFoundError,
41
+ GroupOperationError,
42
+ ImportFailedError,
43
+ KeyAlreadyExistsError,
44
+ KeyNotFoundError,
45
+ LockTimeoutError,
46
+ OperationCancelledError,
47
+ SchemaError,
48
+ StorageError,
49
+ ValidationError,
50
+ )
51
+ from .formatters import (
52
+ print_diff,
53
+ print_groups,
54
+ print_history,
55
+ print_info,
56
+ print_load_memory_result,
57
+ print_schema,
58
+ print_search_results,
59
+ print_validate_all,
60
+ print_validate_result,
61
+ print_vars_by_group,
62
+ print_vars_table,
63
+ )
64
+ from .manager import EnvironmentManager
65
+
66
+ # 异常类型 → 退出码映射
67
+ EXIT_CODE_MAP = {
68
+ KeyNotFoundError: 2,
69
+ KeyAlreadyExistsError: 2,
70
+ StorageError: 3,
71
+ CorruptedStorageError: 3,
72
+ LockTimeoutError: 3,
73
+ ImportFailedError: 4,
74
+ ExportError: 4,
75
+ DecryptionError: 5,
76
+ ValidationError: 6,
77
+ SchemaError: 6,
78
+ GroupNotFoundError: 7,
79
+ GroupOperationError: 7,
80
+ BackupError: 8,
81
+ EditorError: 9,
82
+ CommandNotFoundError: 10,
83
+ OperationCancelledError: 1,
84
+ }
85
+
86
+ # 所有顶级命令(用于补全生成)
87
+ ALL_COMMANDS = [
88
+ 'set', 'get', 'delete', 'list', 'clear',
89
+ 'groups', 'setg', 'getg', 'deleteg', 'listg', 'delete-group', 'move-group',
90
+ 'export', 'load',
91
+ 'backup', 'restore',
92
+ 'search', 'rename', 'copy',
93
+ 'exec', 'loadmemory',
94
+ 'edit', 'info', 'diff', 'expand',
95
+ 'validate', 'history', 'schema', 'completion',
96
+ ]
97
+
98
+
99
+ def _exit_code_for(exc: EVMError) -> int:
100
+ """根据异常类型返回退出码"""
101
+ for exc_type, code in EXIT_CODE_MAP.items():
102
+ if isinstance(exc, exc_type):
103
+ return code
104
+ return 1
105
+
106
+
107
+ def create_parser() -> argparse.ArgumentParser:
108
+ """创建命令行参数解析器"""
109
+ parser = argparse.ArgumentParser(
110
+ prog='evm',
111
+ description='Environment Variable Manager - Manage environment variables easily',
112
+ formatter_class=argparse.RawDescriptionHelpFormatter,
113
+ epilog="""
114
+ Examples:
115
+ evm set API_KEY abc123 # Set a variable
116
+ evm set --secret DB_PASS mypass # Set an encrypted secret
117
+ evm get API_KEY # Get a variable
118
+ evm get API_KEY --json # Get as JSON: {"status":"ok","data":{"key":"API_KEY","value":"..."}}
119
+ evm list --json # List all as JSON
120
+ evm list --show-groups # List by namespace
121
+ evm export --format env # Export to .env
122
+ evm load config.json --nest # Import nested JSON
123
+ evm edit API_KEY # Edit in $EDITOR
124
+ evm info --json # Tool info as JSON
125
+ evm diff backup.json # Compare with backup
126
+ evm expand URL # Expand {{VAR}} templates
127
+ evm validate API_URL # Validate against schema
128
+ evm history --json # History as JSON
129
+ evm schema set API_URL --format url
130
+ evm completion bash # Generate shell completion
131
+
132
+ Agent-friendly usage:
133
+ evm get KEY --json # stdout = JSON, stderr = errors
134
+ evm list --json --quiet # stdout = JSON only, no decoration
135
+ evm --json info # All commands support --json
136
+
137
+ Group Management:
138
+ evm setg dev DB_URL localhost
139
+ evm getg dev DB_URL
140
+ evm listg dev
141
+ evm groups
142
+ evm delete-group dev
143
+
144
+ Options:
145
+ --json Output structured JSON to stdout (agent-friendly)
146
+ --quiet Suppress all human-readable output
147
+ --dry-run Preview changes (set, delete, clear, rename, copy, export,
148
+ load, setg, deleteg, delete-group, move-group, set --secret)
149
+ --force Skip confirmation for destructive operations (clear, delete-group)
150
+
151
+ Exit Codes:
152
+ 0 Success
153
+ 1 General error / cancelled
154
+ 2 Variable not found
155
+ 3 Storage error
156
+ 4 Import/export format error
157
+ 5 Decryption error
158
+ 6 Validation/schema error
159
+ 7 Group error
160
+ 8 Backup error
161
+ 9 Editor error
162
+ 10 Command not found
163
+ """,
164
+ )
165
+
166
+ parser.add_argument('--version', action='version', version='%(prog)s 2.3.0')
167
+ parser.add_argument('-v', '--verbose', action='store_true',
168
+ help='Show detailed version information')
169
+ parser.add_argument('--env-file',
170
+ help='Path to storage file (default: ~/.evm/env.json)')
171
+ parser.add_argument('--json', dest='json_mode', action='store_true',
172
+ help='Output structured JSON (agent-friendly)')
173
+ parser.add_argument('--quiet', '-q', action='store_true',
174
+ help='Suppress human-readable output')
175
+ parser.add_argument('--dry-run', action='store_true',
176
+ help='Preview changes without writing')
177
+ parser.add_argument('--force', action='store_true',
178
+ help='Skip confirmation for destructive operations')
179
+
180
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
181
+
182
+ def _sp(name, **kwargs):
183
+ """Create a subparser with global args (--json/--quiet/--dry-run/--force).
184
+
185
+ Uses default=argparse.SUPPRESS so subparser defaults don't overwrite
186
+ values set on the parent parser (e.g. `evm --json get KEY`).
187
+ """
188
+ p = subparsers.add_parser(name, **kwargs)
189
+ p.add_argument('--json', dest='json_mode', action='store_true',
190
+ default=argparse.SUPPRESS,
191
+ help='Output structured JSON (agent-friendly)')
192
+ p.add_argument('--quiet', '-q', action='store_true',
193
+ default=argparse.SUPPRESS,
194
+ help='Suppress human-readable output')
195
+ p.add_argument('--dry-run', action='store_true',
196
+ default=argparse.SUPPRESS,
197
+ help='Preview changes without writing')
198
+ p.add_argument('--force', action='store_true',
199
+ default=argparse.SUPPRESS,
200
+ help='Skip confirmation for destructive operations')
201
+ return p
202
+
203
+ # ── 基本命令 ──────────────────────────────────────────
204
+
205
+ set_p = _sp('set', help='Set a variable')
206
+ set_p.add_argument('key', help='Variable name')
207
+ set_p.add_argument('value', help='Variable value')
208
+ set_p.add_argument('--secret', '-s', action='store_true',
209
+ help='Encrypt the value')
210
+
211
+ get_p = _sp('get', help='Get a variable')
212
+ get_p.add_argument('key', help='Variable name')
213
+ get_p.add_argument('--secret', '-s', action='store_true',
214
+ help='Decrypt the value')
215
+
216
+ del_p = _sp('delete', help='Delete a variable')
217
+ del_p.add_argument('key', help='Variable name')
218
+
219
+ list_p = _sp('list', help='List all variables')
220
+ list_p.add_argument('pattern', nargs='?', help='Filter pattern')
221
+ list_p.add_argument('--group', '-g', help='Filter by group')
222
+ list_p.add_argument('--show-groups', action='store_true',
223
+ help='Group output by namespace')
224
+ list_p.add_argument('--no-prefix', action='store_true',
225
+ help='Remove group prefix from display')
226
+
227
+ _sp('clear', help='Clear all variables')
228
+
229
+ # ── 分组命令 ──────────────────────────────────────────
230
+
231
+ _sp('groups', help='List all groups')
232
+
233
+ setg_p = _sp('setg', help='Set a grouped variable')
234
+ setg_p.add_argument('group')
235
+ setg_p.add_argument('key')
236
+ setg_p.add_argument('value')
237
+
238
+ getg_p = _sp('getg', help='Get a grouped variable')
239
+ getg_p.add_argument('group')
240
+ getg_p.add_argument('key')
241
+
242
+ delg_p = _sp('deleteg', help='Delete a grouped variable')
243
+ delg_p.add_argument('group')
244
+ delg_p.add_argument('key')
245
+
246
+ listg_p = _sp('listg', help='List variables in a group')
247
+ listg_p.add_argument('group')
248
+ listg_p.add_argument('--no-prefix', action='store_true')
249
+
250
+ dg_p = _sp('delete-group', help='Delete an entire group')
251
+ dg_p.add_argument('group')
252
+
253
+ mg_p = _sp('move-group', help='Move variable to group')
254
+ mg_p.add_argument('key')
255
+ mg_p.add_argument('group')
256
+
257
+ # ── 导入导出 ──────────────────────────────────────────
258
+
259
+ exp_p = _sp('export', help='Export variables')
260
+ exp_p.add_argument('--format', '-f', choices=['json', 'env', 'sh'],
261
+ default='json')
262
+ exp_p.add_argument('--output', '-o', help='Output file path')
263
+ exp_p.add_argument('--group', '-g', help='Export from group')
264
+
265
+ ld_p = _sp('load', help='Load from file')
266
+ ld_p.add_argument('file', help='Input file')
267
+ ld_p.add_argument('--format', '-f', choices=['json', 'env', 'backup'])
268
+ ld_p.add_argument('--replace', '-r', action='store_true')
269
+ ld_p.add_argument('--group', '-g')
270
+ ld_p.add_argument('--nest', '-n', action='store_true')
271
+
272
+ # ── 备份恢复 ──────────────────────────────────────────
273
+
274
+ bk_p = _sp('backup', help='Backup variables')
275
+ bk_p.add_argument('--file', '-f', help='Backup file path')
276
+
277
+ rs_p = _sp('restore', help='Restore from backup')
278
+ rs_p.add_argument('file')
279
+ rs_p.add_argument('--merge', '-m', action='store_true')
280
+
281
+ # ── 搜索/重命名/复制 ──────────────────────────────────
282
+
283
+ sr_p = _sp('search', help='Search variables')
284
+ sr_p.add_argument('pattern')
285
+ sr_p.add_argument('--value', '-v', action='store_true')
286
+
287
+ rn_p = _sp('rename', help='Rename a variable')
288
+ rn_p.add_argument('old_key')
289
+ rn_p.add_argument('new_key')
290
+
291
+ cp_p = _sp('copy', help='Copy a variable')
292
+ cp_p.add_argument('src_key')
293
+ cp_p.add_argument('dst_key')
294
+
295
+ # ── 执行/内存 ─────────────────────────────────────────
296
+
297
+ ex_p = _sp('exec', help='Execute with env vars')
298
+ ex_p.add_argument('exec_args', nargs='+')
299
+
300
+ lm_p = _sp('loadmemory', help='Load to os.environ')
301
+ lm_p.add_argument('--prefix', '-p')
302
+ lm_p.add_argument('--no-prefix', action='store_true')
303
+
304
+ # ── 编辑/信息/Diff/展开 ───────────────────────────────
305
+
306
+ ed_p = _sp('edit', help='Edit value in $EDITOR')
307
+ ed_p.add_argument('key')
308
+
309
+ _sp('info', help='Show tool information')
310
+
311
+ df_p = _sp('diff', help='Compare with backup')
312
+ df_p.add_argument('file')
313
+
314
+ xp_p = _sp('expand', help='Expand {{VAR}} templates')
315
+ xp_p.add_argument('key')
316
+
317
+ # ── P2 新功能 ─────────────────────────────────────────
318
+
319
+ # validate
320
+ vl_p = _sp('validate', help='Validate against schema')
321
+ vl_p.add_argument('key', nargs='?', help='Variable (omit for all)')
322
+
323
+ # history
324
+ hi_p = _sp('history', help='Show operation history')
325
+ hi_p.add_argument('--limit', '-n', type=int, default=20,
326
+ help='Number of entries to show')
327
+ hi_p.add_argument('--clear', action='store_true',
328
+ help='Clear all history')
329
+
330
+ # schema
331
+ sc_p = _sp('schema', help='Manage variable schemas')
332
+ sc_sub = sc_p.add_subparsers(dest='schema_command', help='Schema subcommand')
333
+
334
+ sc_set = sc_sub.add_parser('set', help='Set schema for a variable')
335
+ sc_set.add_argument('key')
336
+ sc_set.add_argument('--format', '-f',
337
+ choices=['url', 'email', 'port', 'integer',
338
+ 'boolean', 'path', 'ipv4', 'ipv6'],
339
+ help='Built-in format')
340
+ sc_set.add_argument('--required', '-r', action='store_true',
341
+ help='Mark as required')
342
+ sc_set.add_argument('--pattern', '-p', help='Custom regex pattern')
343
+ sc_set.add_argument('--description', '-d', help='Description')
344
+
345
+ sc_get = sc_sub.add_parser('get', help='Get schema definition')
346
+ sc_get.add_argument('key', nargs='?', help='Variable (omit for all)')
347
+
348
+ sc_del = sc_sub.add_parser('delete', help='Remove schema definition')
349
+ sc_del.add_argument('key')
350
+
351
+ sc_sub.add_parser('list', help='List all schema definitions')
352
+
353
+ sc_val = sc_sub.add_parser('validate', help='Validate against schema')
354
+ sc_val.add_argument('key', nargs='?', help='Variable (omit for all)')
355
+
356
+ # completion
357
+ co_p = _sp('completion', help='Generate shell completion')
358
+ co_p.add_argument('shell', choices=['bash', 'zsh', 'fish'],
359
+ help='Shell type')
360
+
361
+ return parser
362
+
363
+
364
+ def _confirm(message: str) -> bool:
365
+ """交互式确认(非交互模式或 stdin 非终端时返回 False)"""
366
+ if not sys.stdin.isatty():
367
+ return False
368
+ try:
369
+ response = input(f"{message} [y/N] ").strip().lower()
370
+ return response in ('y', 'yes')
371
+ except (EOFError, KeyboardInterrupt):
372
+ print()
373
+ return False
374
+
375
+
376
+ def main(argv: Optional[list[str]] = None) -> int:
377
+ """主入口
378
+
379
+ Returns:
380
+ 退出码 (0=成功, 1-10=按异常类型细分)
381
+ """
382
+ parser = create_parser()
383
+ args = parser.parse_args(argv)
384
+
385
+ json_mode = getattr(args, 'json_mode', False)
386
+ quiet = getattr(args, 'quiet', False)
387
+
388
+ if args.verbose:
389
+ mgr = EnvironmentManager(args.env_file)
390
+ info = mgr.info()
391
+ if json_mode:
392
+ json_output(info, quiet)
393
+ else:
394
+ print_info(info)
395
+ return 0
396
+
397
+ if not args.command:
398
+ parser.print_help()
399
+ return 0
400
+
401
+ dry_run = getattr(args, 'dry_run', False)
402
+ force = getattr(args, 'force', False)
403
+
404
+ try:
405
+ mgr = EnvironmentManager(args.env_file)
406
+ return _dispatch(mgr, args, dry_run, force, json_mode, quiet)
407
+ except OperationCancelledError:
408
+ if json_mode:
409
+ json_error("Operation cancelled.", 1, quiet)
410
+ else:
411
+ print("Operation cancelled.", file=sys.stderr)
412
+ return 1
413
+ except EVMError as e:
414
+ code = _exit_code_for(e)
415
+ if json_mode:
416
+ json_error(str(e), code, quiet)
417
+ else:
418
+ print(f"Error: {e}", file=sys.stderr)
419
+ return code
420
+ except KeyboardInterrupt:
421
+ if not quiet:
422
+ print("\nOperation cancelled", file=sys.stderr)
423
+ return 1
424
+ except Exception as e:
425
+ if json_mode:
426
+ json_error(f"Unexpected error: {e}", 1, quiet)
427
+ else:
428
+ print(f"Unexpected error: {e}", file=sys.stderr)
429
+ return 1
430
+
431
+
432
+ # ── 命令处理器函数 ──────────────────────────────────────────
433
+
434
+ def _cmd_set(mgr, args, dry_run, force, json_mode, quiet):
435
+ """处理 set 命令"""
436
+ if getattr(args, 'secret', False):
437
+ msg = mgr.set_secret(args.key, args.value, dry_run=dry_run)
438
+ if json_mode:
439
+ json_output({
440
+ "key": args.key, "encrypted": True, "message": msg,
441
+ }, quiet)
442
+ elif not quiet:
443
+ print(msg)
444
+ else:
445
+ msg = mgr.set(args.key, args.value, dry_run=dry_run)
446
+ if json_mode:
447
+ json_output({
448
+ "key": args.key, "value": args.value, "message": msg,
449
+ }, quiet)
450
+ elif not quiet:
451
+ print(msg)
452
+ return 0
453
+
454
+
455
+ def _cmd_get(mgr, args, dry_run, force, json_mode, quiet):
456
+ """处理 get 命令"""
457
+ is_secret = getattr(args, 'secret', False)
458
+ if is_secret:
459
+ value = mgr.get_secret(args.key)
460
+ else:
461
+ value = mgr.get(args.key)
462
+ if json_mode:
463
+ json_output({"key": args.key, "value": value}, quiet)
464
+ elif not quiet:
465
+ if is_secret and sys.stdout.isatty():
466
+ print(
467
+ "[WARNING] Decrypted secret displayed on terminal "
468
+ "(visible in scrollback).",
469
+ file=sys.stderr,
470
+ )
471
+ print(value)
472
+ return 0
473
+
474
+
475
+ def _cmd_delete(mgr, args, dry_run, force, json_mode, quiet):
476
+ """处理 delete 命令"""
477
+ msg = mgr.delete(args.key, dry_run=dry_run)
478
+ if json_mode:
479
+ json_output({"key": args.key, "deleted": True, "message": msg}, quiet)
480
+ elif not quiet:
481
+ print(msg)
482
+ return 0
483
+
484
+
485
+ def _cmd_list(mgr, args, dry_run, force, json_mode, quiet):
486
+ """处理 list 命令"""
487
+ no_prefix = getattr(args, 'no_prefix', False)
488
+ if getattr(args, 'show_groups', False):
489
+ if args.group:
490
+ filtered = mgr.list_vars(group=args.group)
491
+ elif args.pattern:
492
+ filtered = mgr.list_vars(pattern=args.pattern)
493
+ else:
494
+ filtered = mgr.list_vars()
495
+ if json_mode:
496
+ json_output(filtered, quiet)
497
+ elif not quiet:
498
+ print_vars_by_group(filtered)
499
+ else:
500
+ result = mgr.list_vars(
501
+ pattern=args.pattern,
502
+ group=args.group,
503
+ no_prefix=no_prefix,
504
+ )
505
+ if json_mode:
506
+ json_output(result, quiet)
507
+ elif not quiet:
508
+ print_vars_table(result)
509
+ return 0
510
+
511
+
512
+ def _cmd_clear(mgr, args, dry_run, force, json_mode, quiet):
513
+ """处理 clear 命令"""
514
+ if not dry_run and not force:
515
+ count = len(mgr._env_vars)
516
+ if count > 0:
517
+ if not sys.stdin.isatty():
518
+ raise EVMError(
519
+ "Cannot confirm 'clear' in non-interactive mode. "
520
+ "Use --force to skip confirmation."
521
+ )
522
+ if not _confirm(f"This will clear all {count} variables. Continue?"):
523
+ raise OperationCancelledError("clear")
524
+ count = len(mgr._env_vars)
525
+ msg = mgr.clear(dry_run=dry_run)
526
+ if json_mode:
527
+ json_output({"cleared": count, "message": msg}, quiet)
528
+ elif not quiet:
529
+ print(msg)
530
+ return 0
531
+
532
+
533
+ def _cmd_groups(mgr, args, dry_run, force, json_mode, quiet):
534
+ """处理 groups 命令"""
535
+ groups = mgr.list_groups()
536
+ if json_mode:
537
+ json_output({"groups": groups}, quiet)
538
+ elif not quiet:
539
+ print_groups(groups)
540
+ return 0
541
+
542
+
543
+ def _cmd_setg(mgr, args, dry_run, force, json_mode, quiet):
544
+ """处理 setg 命令"""
545
+ msg = mgr.set_grouped(args.group, args.key, args.value, dry_run=dry_run)
546
+ if json_mode:
547
+ json_output({
548
+ "group": args.group, "key": args.key,
549
+ "value": args.value, "message": msg,
550
+ }, quiet)
551
+ elif not quiet:
552
+ print(msg)
553
+ return 0
554
+
555
+
556
+ def _cmd_getg(mgr, args, dry_run, force, json_mode, quiet):
557
+ """处理 getg 命令"""
558
+ value = mgr.get_grouped(args.group, args.key)
559
+ if json_mode:
560
+ json_output({
561
+ "group": args.group, "key": args.key, "value": value,
562
+ }, quiet)
563
+ elif not quiet:
564
+ print(value)
565
+ return 0
566
+
567
+
568
+ def _cmd_deleteg(mgr, args, dry_run, force, json_mode, quiet):
569
+ """处理 deleteg 命令"""
570
+ msg = mgr.delete_grouped(args.group, args.key, dry_run=dry_run)
571
+ if json_mode:
572
+ json_output({
573
+ "group": args.group, "key": args.key,
574
+ "deleted": True, "message": msg,
575
+ }, quiet)
576
+ elif not quiet:
577
+ print(msg)
578
+ return 0
579
+
580
+
581
+ def _cmd_listg(mgr, args, dry_run, force, json_mode, quiet):
582
+ """处理 listg 命令"""
583
+ no_prefix = getattr(args, 'no_prefix', False)
584
+ result = mgr.list_vars(group=args.group, no_prefix=no_prefix)
585
+ if json_mode:
586
+ json_output(result, quiet)
587
+ elif not quiet:
588
+ print_vars_table(result)
589
+ return 0
590
+
591
+
592
+ def _cmd_delete_group(mgr, args, dry_run, force, json_mode, quiet):
593
+ """处理 delete-group 命令"""
594
+ if not dry_run and not force:
595
+ if not sys.stdin.isatty():
596
+ raise EVMError(
597
+ "Cannot confirm 'delete-group' in non-interactive mode. "
598
+ "Use --force to skip confirmation."
599
+ )
600
+ if not _confirm(
601
+ f"This will delete group '{args.group}' and all its variables. Continue?"
602
+ ):
603
+ raise OperationCancelledError("delete-group")
604
+ msg = mgr.delete_group(args.group, dry_run=dry_run)
605
+ if json_mode:
606
+ json_output({
607
+ "group": args.group, "deleted": True, "message": msg,
608
+ }, quiet)
609
+ elif not quiet:
610
+ print(msg)
611
+ return 0
612
+
613
+
614
+ def _cmd_move_group(mgr, args, dry_run, force, json_mode, quiet):
615
+ """处理 move-group 命令"""
616
+ msg = mgr.move_to_group(args.key, args.group, dry_run=dry_run)
617
+ if json_mode:
618
+ json_output({
619
+ "key": args.key, "target_group": args.group, "message": msg,
620
+ }, quiet)
621
+ elif not quiet:
622
+ print(msg)
623
+ return 0
624
+
625
+
626
+ def _cmd_export(mgr, args, dry_run, force, json_mode, quiet):
627
+ """处理 export 命令"""
628
+ msg = mgr.export(
629
+ format_type=args.format,
630
+ output_file=args.output,
631
+ group=args.group,
632
+ dry_run=dry_run,
633
+ )
634
+ if json_mode:
635
+ json_output({"message": msg, "format": args.format}, quiet)
636
+ elif not quiet:
637
+ print(msg)
638
+ return 0
639
+
640
+
641
+ def _cmd_load(mgr, args, dry_run, force, json_mode, quiet):
642
+ """处理 load 命令"""
643
+ msg = mgr.load(
644
+ input_file=args.file,
645
+ format_type=getattr(args, 'format', None),
646
+ replace=getattr(args, 'replace', False),
647
+ group=getattr(args, 'group', None),
648
+ nest=getattr(args, 'nest', False),
649
+ dry_run=dry_run,
650
+ )
651
+ if json_mode:
652
+ json_output({"message": msg, "file": args.file}, quiet)
653
+ elif not quiet:
654
+ print(msg)
655
+ return 0
656
+
657
+
658
+ def _cmd_backup(mgr, args, dry_run, force, json_mode, quiet):
659
+ """处理 backup 命令"""
660
+ msg = mgr.backup(args.file)
661
+ if json_mode:
662
+ json_output({"message": msg}, quiet)
663
+ elif not quiet:
664
+ print(msg)
665
+ return 0
666
+
667
+
668
+ def _cmd_restore(mgr, args, dry_run, force, json_mode, quiet):
669
+ """处理 restore 命令"""
670
+ msg = mgr.restore(args.file, merge=getattr(args, 'merge', False))
671
+ if json_mode:
672
+ json_output({"message": msg, "file": args.file}, quiet)
673
+ elif not quiet:
674
+ print(msg)
675
+ return 0
676
+
677
+
678
+ def _cmd_search(mgr, args, dry_run, force, json_mode, quiet):
679
+ """处理 search 命令"""
680
+ results = mgr.search(
681
+ args.pattern, search_value=getattr(args, 'value', False)
682
+ )
683
+ if json_mode:
684
+ json_output(results, quiet)
685
+ elif not quiet:
686
+ print_search_results(
687
+ results, args.pattern, getattr(args, 'value', False)
688
+ )
689
+ return 0
690
+
691
+
692
+ def _cmd_rename(mgr, args, dry_run, force, json_mode, quiet):
693
+ """处理 rename 命令"""
694
+ msg = mgr.rename(args.old_key, args.new_key, dry_run=dry_run)
695
+ if json_mode:
696
+ json_output({
697
+ "old_key": args.old_key, "new_key": args.new_key,
698
+ "message": msg,
699
+ }, quiet)
700
+ elif not quiet:
701
+ print(msg)
702
+ return 0
703
+
704
+
705
+ def _cmd_copy(mgr, args, dry_run, force, json_mode, quiet):
706
+ """处理 copy 命令"""
707
+ msg = mgr.copy(args.src_key, args.dst_key, dry_run=dry_run)
708
+ if json_mode:
709
+ json_output({
710
+ "src_key": args.src_key, "dst_key": args.dst_key,
711
+ "message": msg,
712
+ }, quiet)
713
+ elif not quiet:
714
+ print(msg)
715
+ return 0
716
+
717
+
718
+ def _cmd_exec(mgr, args, dry_run, force, json_mode, quiet):
719
+ """处理 exec 命令 - 返回子进程退出码"""
720
+ return mgr.execute(args.exec_args)
721
+
722
+
723
+ def _cmd_loadmemory(mgr, args, dry_run, force, json_mode, quiet):
724
+ """处理 loadmemory 命令"""
725
+ filter_prefix = getattr(args, 'prefix', None)
726
+ add_evm_prefix = not getattr(args, 'no_prefix', False)
727
+ loaded, prefix_used, filter_used = mgr.load_to_memory(
728
+ filter_prefix=filter_prefix,
729
+ add_evm_prefix=add_evm_prefix,
730
+ )
731
+ if json_mode:
732
+ json_output({
733
+ "loaded": loaded,
734
+ "evm_prefix": prefix_used,
735
+ "filter_prefix": filter_used,
736
+ }, quiet)
737
+ elif not quiet:
738
+ print_load_memory_result(loaded, prefix_used, filter_used)
739
+ return 0
740
+
741
+
742
+ def _cmd_edit(mgr, args, dry_run, force, json_mode, quiet):
743
+ """处理 edit 命令"""
744
+ msg = mgr.edit(args.key)
745
+ changed = "Updated" in msg
746
+ if json_mode:
747
+ json_output({"key": args.key, "changed": changed, "message": msg}, quiet)
748
+ elif not quiet:
749
+ print(msg)
750
+ return 0
751
+
752
+
753
+ def _cmd_info(mgr, args, dry_run, force, json_mode, quiet):
754
+ """处理 info 命令"""
755
+ info = mgr.info()
756
+ if json_mode:
757
+ json_output(info, quiet)
758
+ elif not quiet:
759
+ print_info(info)
760
+ return 0
761
+
762
+
763
+ def _cmd_diff(mgr, args, dry_run, force, json_mode, quiet):
764
+ """处理 diff 命令"""
765
+ result = mgr.diff(args.file)
766
+ if json_mode:
767
+ json_output(result, quiet)
768
+ elif not quiet:
769
+ print_diff(result)
770
+ return 0
771
+
772
+
773
+ def _cmd_expand(mgr, args, dry_run, force, json_mode, quiet):
774
+ """处理 expand 命令"""
775
+ expanded = mgr.expand(args.key)
776
+ if json_mode:
777
+ json_output({"key": args.key, "expanded": expanded}, quiet)
778
+ elif not quiet:
779
+ print(expanded)
780
+ return 0
781
+
782
+
783
+ def _cmd_validate(mgr, args, dry_run, force, json_mode, quiet):
784
+ """处理 validate 命令"""
785
+ key = getattr(args, 'key', None)
786
+ if key:
787
+ result = mgr.validate(key)
788
+ if json_mode:
789
+ json_output({"key": key, **result}, quiet)
790
+ elif not quiet:
791
+ print_validate_result(key, result)
792
+ else:
793
+ results = mgr.validate_all()
794
+ if json_mode:
795
+ json_output(results, quiet)
796
+ elif not quiet:
797
+ print_validate_all(results)
798
+ return 0
799
+
800
+
801
+ def _cmd_history(mgr, args, dry_run, force, json_mode, quiet):
802
+ """处理 history 命令"""
803
+ if getattr(args, 'clear', False):
804
+ msg = mgr.clear_history()
805
+ if json_mode:
806
+ json_output({"message": msg}, quiet)
807
+ elif not quiet:
808
+ print(msg)
809
+ else:
810
+ entries = mgr.get_history(limit=args.limit)
811
+ if json_mode:
812
+ json_output(entries, quiet)
813
+ elif not quiet:
814
+ print_history(entries)
815
+ return 0
816
+
817
+
818
+ def _cmd_schema(mgr, args, dry_run, force, json_mode, quiet):
819
+ """处理 schema 命令"""
820
+ return _dispatch_schema(mgr, args, json_mode, quiet)
821
+
822
+
823
+ def _cmd_completion(mgr, args, dry_run, force, json_mode, quiet):
824
+ """处理 completion 命令"""
825
+ generator = SHELL_GENERATORS.get(args.shell)
826
+ if generator:
827
+ script = generator(ALL_COMMANDS)
828
+ print(script, end='')
829
+ else:
830
+ raise EVMError(f"Unsupported shell: {args.shell}")
831
+ return 0
832
+
833
+
834
+ # ── 命令注册表 ──────────────────────────────────────────────
835
+
836
+ COMMAND_HANDLERS = {
837
+ 'set': _cmd_set,
838
+ 'get': _cmd_get,
839
+ 'delete': _cmd_delete,
840
+ 'list': _cmd_list,
841
+ 'clear': _cmd_clear,
842
+ 'groups': _cmd_groups,
843
+ 'setg': _cmd_setg,
844
+ 'getg': _cmd_getg,
845
+ 'deleteg': _cmd_deleteg,
846
+ 'listg': _cmd_listg,
847
+ 'delete-group': _cmd_delete_group,
848
+ 'move-group': _cmd_move_group,
849
+ 'export': _cmd_export,
850
+ 'load': _cmd_load,
851
+ 'backup': _cmd_backup,
852
+ 'restore': _cmd_restore,
853
+ 'search': _cmd_search,
854
+ 'rename': _cmd_rename,
855
+ 'copy': _cmd_copy,
856
+ 'exec': _cmd_exec,
857
+ 'loadmemory': _cmd_loadmemory,
858
+ 'edit': _cmd_edit,
859
+ 'info': _cmd_info,
860
+ 'diff': _cmd_diff,
861
+ 'expand': _cmd_expand,
862
+ 'validate': _cmd_validate,
863
+ 'history': _cmd_history,
864
+ 'schema': _cmd_schema,
865
+ 'completion': _cmd_completion,
866
+ }
867
+
868
+
869
+ def _dispatch(
870
+ mgr: EnvironmentManager,
871
+ args,
872
+ dry_run: bool,
873
+ force: bool,
874
+ json_mode: bool,
875
+ quiet: bool,
876
+ ) -> int:
877
+ """命令调度(注册表模式)
878
+
879
+ Returns:
880
+ 退出码(通常为 0,exec 命令透传子进程退出码)
881
+ """
882
+ cmd = args.command
883
+ handler = COMMAND_HANDLERS.get(cmd)
884
+
885
+ if handler is None:
886
+ raise EVMError(f"Unknown command: {cmd}")
887
+
888
+ return handler(mgr, args, dry_run, force, json_mode, quiet) # type: ignore[no-any-return]
889
+
890
+
891
+ def _dispatch_schema(
892
+ mgr: EnvironmentManager, args, json_mode: bool, quiet: bool
893
+ ) -> int:
894
+ """Schema 子命令调度"""
895
+ sc_cmd = getattr(args, 'schema_command', None)
896
+
897
+ if sc_cmd == 'set':
898
+ required = None
899
+ if getattr(args, 'required', False):
900
+ required = True
901
+ msg = mgr.set_schema(
902
+ args.key,
903
+ format=getattr(args, 'format', None),
904
+ required=required,
905
+ pattern=getattr(args, 'pattern', None),
906
+ description=getattr(args, 'description', None),
907
+ )
908
+ if json_mode:
909
+ json_output({"key": args.key, "message": msg}, quiet)
910
+ elif not quiet:
911
+ print(msg)
912
+
913
+ elif sc_cmd == 'get':
914
+ key = getattr(args, 'key', None)
915
+ schema = mgr.get_schema(key)
916
+ if json_mode:
917
+ json_output(schema, quiet)
918
+ elif not quiet:
919
+ print_schema(schema)
920
+
921
+ elif sc_cmd == 'delete':
922
+ msg = mgr.delete_schema(args.key)
923
+ if json_mode:
924
+ json_output({"key": args.key, "message": msg}, quiet)
925
+ elif not quiet:
926
+ print(msg)
927
+
928
+ elif sc_cmd == 'list':
929
+ schema = mgr.get_schema()
930
+ if json_mode:
931
+ json_output(schema, quiet)
932
+ elif not quiet:
933
+ print_schema(schema)
934
+
935
+ elif sc_cmd == 'validate':
936
+ key = getattr(args, 'key', None)
937
+ if key:
938
+ result = mgr.validate(key)
939
+ if json_mode:
940
+ json_output({"key": key, **result}, quiet)
941
+ elif not quiet:
942
+ print_validate_result(key, result)
943
+ else:
944
+ results = mgr.validate_all()
945
+ if json_mode:
946
+ json_output(results, quiet)
947
+ elif not quiet:
948
+ print_validate_all(results)
949
+
950
+ else:
951
+ # 无子命令时显示 schema 列表
952
+ schema = mgr.get_schema()
953
+ if json_mode:
954
+ json_output(schema, quiet)
955
+ elif not quiet:
956
+ print_schema(schema)
957
+
958
+ return 0
959
+
960
+
961
+ __all__ = ['create_parser', 'main', 'ALL_COMMANDS', 'EXIT_CODE_MAP', 'COMMAND_HANDLERS']