From 00c608a3bde30f0127ba618dbed56f048d55ebab Mon Sep 17 00:00:00 2001 From: Zhengshou Lai Date: Mon, 13 Apr 2026 11:53:27 +0800 Subject: [PATCH] feat: add interactive config wizard - Add mycli wizard command for guided configuration - Interactive menus for paths, SSH, API keys, Feishu apps, git proxies - User-friendly prompts with numbered choices - Support add/edit/delete operations via wizard - Hide sensitive input (API keys, secrets) - Confirm before destructive operations --- README.md | 3 + mycli/__pycache__/cli.cpython-310.pyc | Bin 927 -> 960 bytes mycli/__pycache__/cli.cpython-314.pyc | Bin 1602 -> 1669 bytes mycli/cli.py | 3 +- mycli/commands/__init__.py | 3 +- .../__pycache__/__init__.cpython-310.pyc | Bin 553 -> 606 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 569 -> 626 bytes .../__pycache__/config_cmd.cpython-314.pyc | Bin 3142 -> 9887 bytes .../commands/__pycache__/env.cpython-314.pyc | Bin 5003 -> 10341 bytes mycli/commands/config_wizard.py | 324 ++++++++++++++++++ 10 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 mycli/commands/config_wizard.py diff --git a/README.md b/README.md index 757bc23..d851dce 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ mycli self uninstall # Uninstall mycli mycli self info # Show installation info # Configuration (config.json) - 增删改查 +# 方式1: 交互式向导(推荐,无需记忆命令) +mycli wizard # 启动交互式配置向导 +# 方式2: 命令行直接操作 mycli config show # Show full config mycli config list # List top-level sections mycli config get paths # Get paths section diff --git a/mycli/__pycache__/cli.cpython-310.pyc b/mycli/__pycache__/cli.cpython-310.pyc index a4b307694a5181692965b66af217c8233b59723f..a540f6861f46d0885d2230839d47192e37e854a7 100644 GIT binary patch delta 466 zcmYMvO-sWt7zgk^Y1(G#i+fS zasL_dfRmXsg(;*h(OR^HE8QjNNnN z;B@As9kI>B8>F5@r{0A_i0U5n$%;PYkD222D-6P>{!6uqIzl~Y*O%z%r2XGowX{~a z$ntVuYlL)AP#x0}#!S!H!A|{w`V$qK)*Gm5JWP{&p+wC#rQStGuI0xO2x;n5!&@uAe2=wc((+DWg`o#?>1>zD+0ss60 Dg7sy| delta 414 zcmYMvKTE?v7zXfrFPG$+m^P`dT?>i|f|H22`3a1mRZTEWSoT^-6zolL)l zqhG_~=Wx}FE$0o-{dl;)AO4#kRZ$cWGrat3+$^I3{U*!f>lYvrn(o@9>LEiHy;q7p z@&%d09MV|EO(GrTtih~o%@nhCm22%O&)QeMHCNoaV-@UllE~1Odsa~^>|n4UX>~MZtzx4ME3$DpFq%>m0o47rQi$C{%&M*auH=_R bg|i2F1*V@KnP7y|bG>o~oI9KazV}RVm0(nM-i4)9O1!DyR zR41M>W(HD|nHV9o1(eQ%(o3N9EhsI(B<~JX53&Fi#B!N1LPUVpU{}G7s$y~(lRVh& z8BB5(IJEE~v;?t>Gn5F&iUe_3L@+R<0R1S#5Gy?S43i-v&*Xnh;T*TP$}_7Hi&7>B zGN&>MP2SGzC?*PY5eE<#vjd3+hL0Q!tei!{llfR|?LksSLLfpIM2G;1TP(SWnR!Lx zK&BiJLp9xDVQ+H=rVUwGmQks)$SEK;c2@17hP9XV#nURt4CWGu_2A<~(HP^xDGDFSe PaMrVvCD_zhK<)$pmMdMm delta 460 zcmZqWJ;cMS&Bx2d00eBwcQO}D2PVgb;`j1`@YeauYN2io}3SSs;dLy2HZW<_cDH ziz7ZBq#{0EQ)KcUmhk#ptm#Ghr3JV6%2JDpGxPJ}^9xFVR1wJRTil5$De=kqxw(mX zDPS`}+Vc`~Q{&^&?d7n^%}*)KNwq7I2kHccMDbrB@qw9|+L==L`kc!RR7G a!3Q=5CiZseCh5u3S&vLkXOm(9`2Yaep<>_w diff --git a/mycli/cli.py b/mycli/cli.py index 8b3c61f..c95bf5e 100644 --- a/mycli/cli.py +++ b/mycli/cli.py @@ -2,7 +2,7 @@ import click -from mycli.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd, self_cmd, config_cmd +from mycli.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd, self_cmd, config_cmd, wizard_cmd @click.group() @@ -24,6 +24,7 @@ cli.add_command(ssh_cmd) cli.add_command(git_cmd) cli.add_command(self_cmd) cli.add_command(config_cmd) +cli.add_command(wizard_cmd) def main(): diff --git a/mycli/commands/__init__.py b/mycli/commands/__init__.py index 79bfce1..39e9eb7 100644 --- a/mycli/commands/__init__.py +++ b/mycli/commands/__init__.py @@ -6,5 +6,6 @@ from .ssh import ssh_cmd from .git import git_cmd from .self_mgmt import self_cmd from .config_cmd import config_cmd +from .config_wizard import wizard_cmd -__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "config_cmd"] +__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "config_cmd", "wizard_cmd"] diff --git a/mycli/commands/__pycache__/__init__.cpython-310.pyc b/mycli/commands/__pycache__/__init__.cpython-310.pyc index d08acca4bbb02d4f0359dbb3009ed53b40df3fa7..4e0088d3bc1e42296207015a6028241ed26923ff 100644 GIT binary patch delta 159 zcmZ3M}GW%%?O)h4P5xT`5AD@_$6Cb~lp-2#DLXqI)uZ+PW0zf7Ug8~x|BOEdT0K>o+ A>;M1& diff --git a/mycli/commands/__pycache__/__init__.cpython-314.pyc b/mycli/commands/__pycache__/__init__.cpython-314.pyc index d6306df937a8dbca9108fbcc61acd64c627ddbba..704ff36189a876a650486c9e22f66cd63a054978 100644 GIT binary patch delta 147 zcmdnV@`;64n~#@^0SKmM+{x^i$ScWMHBmiPDvmLT-BZv*s6zFnN^8JDe=j4I@GT delta 116 zcmeywvXg~Zn~#@^0SMkC-N_7@$ScX1H&H#6Pmdvp!=JH4Fo<25L6dXhvT|lWP43Bh zjOI+5LX&eCV<+Ebj5ZMmYGVZAVkIE)ftit!@g{@Ba|Wa93`TbuBw$GXE`!n|uJR`K IB2l0+09g1O9RL6T diff --git a/mycli/commands/__pycache__/config_cmd.cpython-314.pyc b/mycli/commands/__pycache__/config_cmd.cpython-314.pyc index 4b39f8c3cdc20b3f37c9c8311ef9dcc119eb0fe3..3f5e73999e134c1b5123d69d065a42d73aeb97c8 100644 GIT binary patch literal 9887 zcmcgyYit`=cD^&5;qWmOB~cb-NtQ-0%d{=omfy1D#J1$O746ue(v2J~LyjcI6v@s| zvBhq;s*BdLldhS~2GOQLBNU55MvGQx_eXxF3uoJ+e<;hg)TtI&G+Pw=Pp%EP&HiZ5 zxx*ocG-Y{Fv{%rb$36GlbMM^qoyX;l3Xh9G`A20n`cE~4e2ougp)K(6q}4{qG?7V) zyiH{0ER$mBl})j5WzX8qa4D|YXya2nytAFPpAk|5eaD@3oN=a{&EyI{2qTo179#Vq z{UBq!mjyrBmm^8Jueb*{m(mj=JNB8>P7AeX9cq_J4dXXH&ke?Rn-sEG;Fp)eyAEHE z*=u9I>rzx$ct~Ys^U9TSm0T_R4su4jT=Tq)rmW^QRke=Hn=IpPuofV5d5LOeKS)$( zk!bV!5~Zrwk)+;~L~bbbPWjfMZp>3}u~65nL*108ZnjWwT8CQ7Q@2>CYuBM}%~Q8o zsQoh2Mgr~W){}|&Xmo5+4W*)qxHJ)phsKl%C7zPPi3xa$Xx#$=M&|}Ysd2d4!;~EE zJmb(9Q$`e3O{kg*`bLm09-2_X)_s8+9~!wQcDMnR#9`TV!$mrfG%*#9MPVq54BZ2F zi7Ei`6J(0)P3{(jq`tW(U8|RrlfS+Rb?ui z!xkVT1gAn}-U@H0$wo2lgAV`PB9M@Q_;XI!pQ_@ml&=WFPY2FpCfQ4L;h@^+5n4FWb znG6?tRTrquyB6|xew1-0Af*51w33oS#rsftFBF?pq<5wySYv5hB$1Nhi4;|nK)1`) ze?2sjj44{LOM+tE#}a%PDGB(=63rC~hc+Z2tAgeT*n2bkyrOS62 z-3EFZa2Z<;*G6|mwP;*Rh2mjF=OWQ?O6QYm6nJs(YKgecMtfG3ihoR`}1^pG+gnm65Y1C6DB@FU#k+lMnkBDp@Pj@Lp;7a=Wd_NXz#y1-M=D; z_l3Gu_oj^U+2v(->wS0Ys#x{I_D{C|VEgPF^LxJ7|DgK#Bk{y9#i~rl_b>d?;ko0w z?aGYIU7G99`nE4{4;}kgD|~m}z5VWN)%z1!M?+2}G$o}r!-#2nR~Maz5Y-FjJ^4?Vml^!qmy@cQ1lhd$!Rj`W24dlsSHJ>GRJ%Mk z`$hILle7oP;HCnFc_;IEI>(!Q(iwEX$Q2B!B&UEn=^DCw7yu;#(9lSBk)cnwLdT&` zch;NzfyQ~>kBinshN95B!rF$^*7cF?CY)@rLbV0{=3NUJx(rYv$PUxNfOX-@x?N6l zP6#r^D`^1;%Nz=0$S#Bx*&Vb4xNx#4dk*Ho1#Or_b`bMhj6q8yiL%XP3u&f*7akt5 zZ6Q%cvVNa3uxKZdgV;AISmY@WbL?e-!kkU!3X)iQZ3uHRlN4SRqD+*Svb}Hy9~x8v zZ~DQd^}zAPFbK++gTc6R4e?O=p2d2@yXT<}yX2t`C=hu=?}YdC>@81gXt`7XQs{$- zDs}!&D_&cBTPMMLc#YogY(;!+?d|FL-A>m)y5ejcuqqm%NB|(IqE_fkx6WKUrB=~< zSDM4Qr)|kyPR;?kSy%B|;IM$6C|9 z2snL8I1y3$(PdK6n^m`9)r7>nmW)LKeGzwc{+b$1Dew+)H{hm-OMy*oMOwFYKxPy= zg;F71Od`TX3h;~3x;+Kp``sDtnT@y_o;irayT)=(X|H-r4WY#yb$W&JxAY95LQR;&HgI(_S-uB_Pn z>opz-zj@XUgMcvKXuDw5e(dja@P8-P_BD{7?%&ZL;O{xyeRa-z4V(J6+V1V%)8AtI zs)dL9uLAD=!%p2EQDRC;N%LJ@Q;L>myQb1rm##*Wh8vV!)@To+%rHl}KsA_m>&!QVfGfr3T&ZG=DfB99udJ?AkjY~r?Chcu%UUOyrzmUh zvB@@Bu;5x=ta8p|k{w1Dgq9#{Vtg4(VjIq5>0nuW4CXC$k}I_k>)6SBD4*otP0z`P@8c=IsLl`zM?Z}DH znPxj*g&>{}y#t0)MW;L^yPK&2rfnFh@;XQDCR zoJ9kDpfRvEYFM;d;;6WpzLCzf{lV|u7n)W#jx7G&U4c}d+n!m+oNtbwle4wkXaoZ0 z=%vl$nqy=8XWE_SdEY#r^>;7yJroZY+a1$3It|tM})TID}=g#T*Cj?-TLXGh$p zMd#mp-KU$K;1fl)R2V4o1)UNwXWWw?gQ?rds3omoMBcoWfqqNoVx2In5E!wg(7#bG z1M?U~Zg}1ty~z%u>sV6EET;)b5LDd*V__R=vw}2)hM4c6r4R&`%RndjVF%&l}L8Ql=qZaN%9C?@9u%L|D-jwDSS%u9XPIMx9GdB^BK}+wlXk3`jFed>fMxyd=mXI`q9-#-qso0N@dN*&KZ8y=bz<2Ie-WX?Chbrp1JY4Z_ld>9Si<1gslIyN8<5S572&=TK2Tx_q4A=|C6=_ z>~n^GNJ@;v}7!$K3s1CnVJ{ z5m*FxXtOBbyrsUX2h(Tx4V$z!^FNw3JWX4z-Aoi-!a*2Wkz<5BUwNYQC0J zO*@YroPz<;op^%oLMvorrpaY;3gQ5mKWtE=6tZI1$utiz z0MP+vu4K|i_S>RNfPq5~^&${MijeKztzt^NIM;b}Oo=Pklj<>j3)Ka~zd9OAghMgy zSa-hp&#)&tvcCdJvQ_)A+Nc=cP@E&%xmLIBQ1 z=a7f`ZMfC&nJTy8K)DLjQLjMLpCU~XDtZRt@hyweeNkExTc`V1E2C>yVjk9lj{??D)`jabl%KJEH&)BGDVcGK^Q z%pT5e>X^GUUpwzuur2g09LV~5A32UZ;fa4|?pQ{NM2j5;*pG((?508uLv56oJeFyF z9Wpc}a6KB0DN-~p;a~|Bj=XZGGj9Xu($g9G4y$==M6EC+<*$t^YVqM0ep9OI18}pE zZ_~0tfNrW~{(Py$IA1EVFmxig3DM5LJh;L}u$U4A4k|SLQo?+lo4UXatZND!)zJ*>JN&`P` z5Y7roHdxj*9jY{)H8?aYa0NUpE#MM5a#68Ub`_#xORw@X$p2UYHm(3zE~a;$RFzN) z6)p-VU#9CMjjD+WNiojVByfR}F(B$r`wYPE#&ewRT(jv|fe!|(B2&-}QO``{rrY7r zSn4b~l|$9;1bvM;AIWVxEnGK|y3?U zLy?Fym6%kekZ4wNzpvnpO;pWgf(#Tc_J0Z3}036pJm&+C?6y}P`d*p7@C%`OS=hWNY+e=`?h6e$sLpQMIsexDMS$lo;q>z|mWL z6!#0af7#u%=x$neOZVN9Nx5m)f;j)Ztnb*DeGeUHi@SWyRjqK~2>$;0jQD`tzREc< zX`8Lyz0k1W$yOfw^58@6JiY}u%6gAK;!aQ`3e@Ss$Vem+1~-=`+}VVt3uxG4(RWld z6LdXwk?wsO{5k!}0X@~!?TI8} zt9lIi_hE$|Fg2AbH4WdvM_lp}!=XEl!q%INDaTY5TG1KN{sHPW8^bXFOtyVZ-2XzH zKPPox6Y<|j^Up~KWX)OOX2Xq!%(e$i!(*0ZoKI|oZCt}U$gH#CGUHohe3?^AMssz( zx!T&?zr@t9F!q~GH<~j15>xw_tz;UJ)Q?re8V8SSTMO(;>CL{v5>towjW-%+#%G=L z`?3elKVU9AW;>Z9$mt@3^=1oa2ehnRW(us&EHPV5b|{L;ZY-0UyPVy9@&VKLnBB&7 zBaeQ{j>_1yW`~z+K?^5i&XvqwB!wk`JL?=yFENdeSvzwSo42gt-CA`)jty4K=*=?i Yx!!PRT~MQU6?2kG*!UkT-qJn!KNVReV*mgE delta 1231 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 delta 572 zcmaDF(5}Ga7F`&(+JsE(uf> zQY1AwnKwWHuDeJ9C<;_}m{*exq_IeG@+V#`M*GRKd>5JSF>e0Am&?c~H#ttgiaAJ7 zd2*tF(&XI&{A@rrgQn2r2w^jBEs!p45TP@9rf_||pC(6<5Qq!1;}&aiYH~(?kse4u zA4C{{2#~vrK>mX|9_;2KBbfXx-u!~pytMqp+<0(MumU9%KmZaL4j^?P!VQRvHvowS zhA$ipyyD$H4X%?NMW?u3=dgRi!Q0Q>#XZ4)1?zPVy&^uKDoxQ_?D6p_`N{F|w>Z;E z^O8&Q^K*)eEPzr)) diff --git a/mycli/commands/config_wizard.py b/mycli/commands/config_wizard.py new file mode 100644 index 0000000..b3b2738 --- /dev/null +++ b/mycli/commands/config_wizard.py @@ -0,0 +1,324 @@ +"""Interactive configuration wizard for mycli.""" + +import json +from pathlib import Path + +import click + +from mycli.config import config + + +def _prompt_choice(options: list[str], prompt: str = "Select") -> str | None: + """Show numbered options and get user choice.""" + click.echo(f"\n{prompt}:") + for i, opt in enumerate(options, 1): + click.echo(f" {i}. {opt}") + click.echo(" 0. Cancel") + + while True: + choice = click.prompt("Enter number", type=int, default=0) + if choice == 0: + return None + if 1 <= choice <= len(options): + return options[choice - 1] + click.echo("Invalid choice, try again.") + + +def _prompt_confirm(message: str) -> bool: + """Prompt for yes/no confirmation.""" + return click.confirm(message, default=False) + + +@click.group(name="wizard", invoke_without_command=True) +@click.pass_context +def wizard_cmd(ctx): + """Interactive configuration wizard.""" + if ctx.invoked_subcommand is None: + # Show main wizard menu + show_main_menu() + + +def show_main_menu(): + """Show the main wizard menu.""" + click.clear() + click.secho("╔══════════════════════════════════════╗", fg="cyan") + click.secho("║ MyCLI Configuration Wizard ║", fg="cyan") + click.secho("╚══════════════════════════════════════╝", fg="cyan") + + options = { + "1": ("Manage Paths", manage_paths), + "2": ("Manage SSH Hosts", manage_ssh_hosts), + "3": ("Manage API Keys", manage_api_keys), + "4": ("Manage Feishu Apps", manage_feishu_apps), + "5": ("Manage Git Proxies", manage_git_proxies), + "0": ("Exit", lambda: click.echo("Goodbye!")), + } + + while True: + click.echo("\nMain Menu:") + for key, (label, _) in options.items(): + click.echo(f" [{key}] {label}") + + choice = click.prompt("\nSelect option", type=str, default="0") + + if choice in options: + _, func = options[choice] + func() + if choice == "0": + break + else: + click.echo("Invalid choice.") + + +def manage_paths(): + """Interactive path management.""" + click.secho("\n--- Path Management ---", fg="cyan") + + paths = config.get("paths", {}) + + # Show current paths + click.echo("\nCurrent paths:") + for name, path in sorted(paths.items()): + exists = "✓" if Path(path).expanduser().exists() else "✗" + click.echo(f" [{exists}] {name}: {path}") + + action = _prompt_choice(["Add new path", "Edit path", "Delete path", "Back"]) + + if action == "Add new path": + name = click.prompt("Path name (e.g., 'mynewproject')") + path = click.prompt("Path value (e.g., '~/Documents/project')") + _config_set(f"paths.{name}", path) + click.secho(f"Added path: {name} = {path}", fg="green") + + elif action == "Edit path": + if not paths: + click.echo("No paths to edit.") + return + name = _prompt_choice(list(paths.keys()), "Select path to edit") + if name: + new_path = click.prompt("New path value", default=paths[name]) + _config_set(f"paths.{name}", new_path) + click.secho(f"Updated path: {name}", fg="green") + + elif action == "Delete path": + if not paths: + click.echo("No paths to delete.") + return + name = _prompt_choice(list(paths.keys()), "Select path to delete") + if name and _prompt_confirm(f"Delete path '{name}'?"): + _config_delete(f"paths.{name}") + click.secho(f"Deleted path: {name}", fg="green") + + +def manage_ssh_hosts(): + """Interactive SSH host management.""" + click.secho("\n--- SSH Host Management ---", fg="cyan") + + hosts = config.get("ssh_hosts", {}) + + click.echo("\nCurrent SSH hosts:") + for name, host_config in sorted(hosts.items()): + click.echo(f" {name}: {host_config['host']}:{host_config['port']}") + + action = _prompt_choice(["Add new host", "Edit host", "Delete host", "Back"]) + + if action == "Add new host": + name = click.prompt("Host name (e.g., 'myserver')") + host = click.prompt("SSH connection (e.g., 'user@host')") + port = click.prompt("Port", type=int, default=22) + has_key = click.confirm("Use SSH key?", default=False) + + host_config = {"host": host, "port": port} + if has_key: + key_path = click.prompt("SSH key path (e.g., '~/.ssh/id_rsa')") + host_config["key"] = key_path + + _config_set(f"ssh_hosts.{name}", host_config) + click.secho(f"Added SSH host: {name}", fg="green") + + elif action == "Edit host": + if not hosts: + click.echo("No hosts to edit.") + return + name = _prompt_choice(list(hosts.keys()), "Select host to edit") + if name: + host_config = hosts[name] + host = click.prompt("Host", default=host_config["host"]) + port = click.prompt("Port", type=int, default=host_config["port"]) + + host_config["host"] = host + host_config["port"] = port + _config_set(f"ssh_hosts.{name}", host_config) + click.secho(f"Updated SSH host: {name}", fg="green") + + elif action == "Delete host": + if not hosts: + click.echo("No hosts to delete.") + return + name = _prompt_choice(list(hosts.keys()), "Select host to delete") + if name and _prompt_confirm(f"Delete host '{name}'?"): + _config_delete(f"ssh_hosts.{name}") + click.secho(f"Deleted SSH host: {name}", fg="green") + + +def manage_api_keys(): + """Interactive API key management.""" + click.secho("\n--- API Key Management ---", fg="cyan") + + api_keys = config.get("api_keys", {}) + + click.echo("\nCurrent API keys:") + for name in sorted(api_keys.keys()): + value = api_keys[name] + masked = value[:8] + "..." + value[-4:] if len(value) > 12 else "***" + click.echo(f" {name}: {masked}") + + action = _prompt_choice(["Add new API key", "Edit API key", "Delete API key", "Back"]) + + if action == "Add new API key": + name = click.prompt("API name (e.g., 'openai', 'anthropic')") + key = click.prompt("API key", hide_input=True) + _config_set(f"api_keys.{name}", key) + click.secho(f"Added API key: {name}", fg="green") + + elif action == "Edit API key": + if not api_keys: + click.echo("No API keys to edit.") + return + name = _prompt_choice(list(api_keys.keys()), "Select API key to edit") + if name: + key = click.prompt("New API key", hide_input=True) + _config_set(f"api_keys.{name}", key) + click.secho(f"Updated API key: {name}", fg="green") + + elif action == "Delete API key": + if not api_keys: + click.echo("No API keys to delete.") + return + name = _prompt_choice(list(api_keys.keys()), "Select API key to delete") + if name and _prompt_confirm(f"Delete API key '{name}'?"): + _config_delete(f"api_keys.{name}") + click.secho(f"Deleted API key: {name}", fg="green") + + +def manage_feishu_apps(): + """Interactive Feishu app management.""" + click.secho("\n--- Feishu App Management ---", fg="cyan") + + apps = config.get("feishu_apps", {}) + + click.echo("\nCurrent Feishu apps:") + for name in sorted(apps.keys()): + app = apps[name] + click.echo(f" {name}: {app['app_id']}") + + action = _prompt_choice(["Add new app", "Edit app", "Delete app", "Back"]) + + if action == "Add new app": + name = click.prompt("App name (e.g., 'mybot')") + app_id = click.prompt("App ID") + app_secret = click.prompt("App Secret", hide_input=True) + + app_config = {"app_id": app_id, "app_secret": app_secret} + _config_set(f"feishu_apps.{name}", app_config) + click.secho(f"Added Feishu app: {name}", fg="green") + + elif action == "Edit app": + if not apps: + click.echo("No apps to edit.") + return + name = _prompt_choice(list(apps.keys()), "Select app to edit") + if name: + app = apps[name] + app_id = click.prompt("App ID", default=app["app_id"]) + change_secret = click.confirm("Change app secret?", default=False) + + app["app_id"] = app_id + if change_secret: + app["app_secret"] = click.prompt("App Secret", hide_input=True) + + _config_set(f"feishu_apps.{name}", app) + click.secho(f"Updated Feishu app: {name}", fg="green") + + elif action == "Delete app": + if not apps: + click.echo("No apps to delete.") + return + name = _prompt_choice(list(apps.keys()), "Select app to delete") + if name and _prompt_confirm(f"Delete app '{name}'?"): + _config_delete(f"feishu_apps.{name}") + click.secho(f"Deleted Feishu app: {name}", fg="green") + + +def manage_git_proxies(): + """Interactive Git proxy management.""" + click.secho("\n--- Git Proxy Management ---", fg="cyan") + + proxies = config.get("git_proxies", {}) + + click.echo("\nCurrent Git proxies:") + for name, url in sorted(proxies.items()): + click.echo(f" {name}: {url}") + + action = _prompt_choice(["Add new proxy", "Edit proxy", "Delete proxy", "Back"]) + + if action == "Add new proxy": + name = click.prompt("Proxy name (e.g., 'fastgithub')") + url = click.prompt("Proxy URL (e.g., 'http://127.0.0.1:38457')") + _config_set(f"git_proxies.{name}", url) + click.secho(f"Added Git proxy: {name}", fg="green") + + elif action == "Edit proxy": + if not proxies: + click.echo("No proxies to edit.") + return + name = _prompt_choice(list(proxies.keys()), "Select proxy to edit") + if name: + url = click.prompt("Proxy URL", default=proxies[name]) + _config_set(f"git_proxies.{name}", url) + click.secho(f"Updated Git proxy: {name}", fg="green") + + elif action == "Delete proxy": + if not proxies: + click.echo("No proxies to delete.") + return + name = _prompt_choice(list(proxies.keys()), "Select proxy to delete") + if name and _prompt_confirm(f"Delete proxy '{name}'?"): + _config_delete(f"git_proxies.{name}") + click.secho(f"Deleted Git proxy: {name}", fg="green") + + +def _config_set(key: str, value): + """Set a config value directly in the JSON file.""" + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + keys = key.split(".") + target = data + for k in keys[:-1]: + if k not in target: + target[k] = {} + target = target[k] + target[keys[-1]] = value + + with open(config_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + +def _config_delete(key: str): + """Delete a config key directly from the JSON file.""" + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + keys = key.split(".") + target = data + for k in keys[:-1]: + target = target[k] + del target[keys[-1]] + + with open(config_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n")