-
Notifications
You must be signed in to change notification settings - Fork 128
Expand file tree
/
Copy pathsetup.py
More file actions
2208 lines (1974 loc) · 107 KB
/
setup.py
File metadata and controls
2208 lines (1974 loc) · 107 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
EvoNexus — Setup Wizard
Generates workspace configuration, CLAUDE.md, .env, and folder structure.
Usage: python setup.py (or: make setup)
This file also doubles as a setuptools build backend when invoked by pip
(e.g. via `pip install -e .` or `npx @evoapi/evo-nexus`). In that case the
interactive wizard is skipped and only package metadata is produced — see
`_IS_BUILD_BACKEND` below.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
WORKSPACE = Path(__file__).parent
# ── Non-interactive / build-backend detection ──────────────────────────
#
# We use TWO signals (narrow and explicit) to avoid the false-positive
# problem Sourcery flagged on upstream PR #11:
#
# 1. _IS_TTY — true when stdin is an interactive terminal.
# Fixes the EOFError from `input()` under pip/npx.
#
# 2. _IS_BUILD_BACKEND — true ONLY when setuptools/pip is the caller.
# Detected via the explicit env var EVO_NEXUS_INSTALL=1
# (set by cli/bin/cli.mjs) or narrow argv markers that
# setuptools always injects (egg_info / dist_info /
# bdist_wheel / --editable). We deliberately do NOT use
# generic args like --version because a direct call
# `python setup.py --version` should remain interactive-ish.
#
_IS_TTY = sys.stdin.isatty() if sys.stdin else False
_BUILD_ARGV_MARKERS = {"egg_info", "dist_info", "bdist_wheel", "sdist", "--editable"}
_IS_BUILD_BACKEND = (
os.environ.get("EVO_NEXUS_INSTALL") == "1"
or any(a in _BUILD_ARGV_MARKERS for a in sys.argv[1:])
)
def _read_version_from_pyproject() -> str:
"""Single source of truth for the package version.
Reads [project].version from pyproject.toml. Avoids the drift risk
Sourcery flagged on PR #11 (hardcoded "0.23.2" string).
"""
try:
import re
pyproject = (WORKSPACE / "pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.MULTILINE)
if match:
return match.group(1)
except (OSError, ImportError):
pass
return "0.0.0" # fallback; never expected in practice
# ANSI colors
GREEN = "\033[92m"
CYAN = "\033[96m"
YELLOW = "\033[93m"
RED = "\033[91m"
DIM = "\033[2m"
BOLD = "\033[1m"
RESET = "\033[0m"
# ─────────────────────────────────────────────────────────────────────────────
# Setup wizard i18n (pt-BR / en-US / es)
# ─────────────────────────────────────────────────────────────────────────────
# Matches the BCP-47 tags the dashboard UI uses. The selected language also
# becomes the default `workspace.language` saved in config/workspace.yaml, so
# what the user picks here is what the dashboard later renders in.
LANG = "en-US" # mutated by select_language() before main() runs
MESSAGES = {
"en-US": {
"choose_lang_prompt": "Choose your language / Escolha seu idioma / Elige tu idioma",
"choose_lang_option_1": "English (US)",
"choose_lang_option_2": "Português (BR)",
"choose_lang_option_3": "Español",
"choose_lang_ask": "Type 1, 2 or 3",
"banner_title": "EvoNexus — Setup Wizard",
"checking_prereqs": "Checking prerequisites...",
"dashboard_access": "Dashboard Access",
"quick_remote_setup": "Quick setup for remote access...",
"ai_provider": "AI Provider",
"about_you": "About you",
"your_name": "Your name",
"company_name": "Company name",
"timezone": "Timezone",
"language": "Language",
"dashboard_port": "Dashboard port",
"creating_workspace": "Creating workspace...",
"installing_python_deps": "Installing Python dependencies...",
"installed_python_deps": "Installed Python dependencies",
"python_deps_failed": "Python dependencies failed to install",
"python_deps_needed": "This is needed for the dashboard to work.",
"try_manually": "Try running manually:",
"log_at": "Log:",
"installing_dashboard_deps": "Installing dashboard dependencies...",
"installed_dashboard_deps": "Installed dashboard dependencies",
"dashboard_deps_failed": "Dashboard dependencies failed",
"building_dashboard": "Building dashboard frontend...",
"built_dashboard": "Built dashboard frontend",
"dashboard_build_failed": "Dashboard build failed",
"installing_terminal_deps": "Installing terminal-server dependencies...",
"installed_terminal_deps": "Installed terminal-server dependencies",
"terminal_deps_failed": "Terminal-server dependencies failed",
"services_user_note": "(services will run as {user})",
"terminal_started": "Terminal server started (port 32352)",
"terminal_not_started": "Terminal server may not have started — check logs/terminal-server.log",
"dashboard_started": "Dashboard started (port 8080)",
"dashboard_not_started": "Dashboard may not have started — check logs/dashboard.log",
"setup_done": "Setup complete!",
"dashboard_available_at": "Dashboard available at:",
"next_steps_header": "Next steps:",
"next_step_1_remote": "1. Open the link above and create your admin account",
"next_step_2_remote": "2. Go to {bold}Providers{reset} and configure your AI Provider",
"next_step_3_remote": "3. Open an agent and start using it!",
"next_step_1_local": "1. Edit {bold}.env{reset} with your API keys",
"next_step_2_local": "2. Run: {bold}make dashboard-app{reset}",
"next_step_3_local": "3. Open {bold}{url}{reset} to create your admin account",
"systemd_section": "systemd service:",
"systemd_status": "check status",
"systemd_restart": "restart",
"systemd_logs": "view logs",
"systemd_su": "switch to service user",
# Prerequisites + tools
"sys_packages_updating": "Updating system packages...",
"sys_packages_updated": "System packages updated",
"tool_installed": "installed",
# Dashboard access wizard
"local_only_option": "Local only (http://localhost:8080)",
"domain_ssl_option": "Domain with SSL (recommended for remote servers)",
"type_1_or_2": "Type 1 or 2",
"domain_prompt": "Domain (e.g. nexus.example.com)",
"installing_nginx": "Installing nginx...",
"nginx_installed": "nginx installed",
"nginx_install_failed": "nginx installation failed, using local mode",
"ssl_cert_prompt": "SSL certificate (1=certbot, 2=self-signed, 3=manual path)",
"cert_existing_found": "Existing certbot certificate found for {domain}",
"installing_certbot": "Installing certbot...",
"certbot_installed": "certbot installed",
"obtaining_ssl_certbot": "Obtaining SSL certificate via certbot...",
"ssl_obtained_certbot": "SSL certificate obtained via certbot",
"certbot_failed_fallback": "certbot failed — falling back to self-signed",
"generating_self_signed": "Generating self-signed SSL certificate...",
"self_signed_generated": "Self-signed SSL certificate generated",
"self_signed_cloudflare_note": "(Compatible with Cloudflare SSL mode: Full)",
"self_signed_failed": "Failed to generate SSL certificate",
"no_ssl_cert_local_mode": "No SSL certificate available, using local mode",
"manual_cert_prompt": "Path to certificate (.crt or .pem)",
"manual_key_prompt": "Path to private key (.key)",
"nginx_configured_for": "Nginx configured for {domain}",
"nginx_config_failed": "Failed to configure nginx",
"configuring_firewall": "Configuring firewall...",
"firewall_ports_opened": "Firewall ports opened (80, 443)",
"firewall_using_ufw": "Using ufw",
"firewall_using_iptables": "Using iptables (ufw not installed)",
"firewall_persisted": "Rules persisted via {tool} (will survive reboot)",
"firewall_persistence_missing": "Rules opened in memory only — install netfilter-persistent OR ufw to persist across reboots",
"firewall_install_persistence": "Installing netfilter-persistent so rules survive reboot...",
"firewall_failed": "Firewall step failed: {err}",
"firewall_cloud_provider_hint": "Cloud provider firewall: if 80/443 still appear blocked from outside, also open them in your provider's Security List/Group ({provider}).",
# Workspace file creation
"generated_workspace_yaml": "Generated config/workspace.yaml",
"env_created_from_example": "Created .env from .env.example",
"env_example_missing": ".env.example not found, creating empty .env",
"env_already_exists": ".env already exists, skipping",
"generated_master_key": "Generated KNOWLEDGE_MASTER_KEY (Knowledge Base encryption)",
"master_key_already_set": "KNOWLEDGE_MASTER_KEY already set — preserved",
"master_key_skip_crypto_missing": "Skipped KNOWLEDGE_MASTER_KEY generation ({exc})",
"master_key_run_init_hint": "Run `make init-key` after setup completes to generate it.",
"master_key_ensure_failed": "Could not ensure KNOWLEDGE_MASTER_KEY: {exc}",
"generated_routines_yaml": "Created config/routines.yaml",
"routines_already_exists": "config/routines.yaml already exists, skipping",
"generated_claude_md": "Generated CLAUDE.md",
"created_workspace_folders": "Created workspace folders ({count})",
# Systemd / service lifecycle
"fixing_ownership": "Fixing file ownership for {user}...",
"ownership_fixed": "Ownership fixed",
"starting_dashboard_services": "Starting dashboard services...",
"creating_systemd_service": "Creating systemd service...",
"systemd_service_created": "Systemd service created and enabled (auto-starts on boot)",
"systemd_manage_hint": "Manage with: systemctl {{start|stop|restart|status}} {service}",
# Prerequisite tool check
"tool_not_found": "{name} not found",
"tool_installing_verb": "Installing {name}...",
"tool_upgrading_verb": "Upgrading {name}...",
"tool_install_failed": "Failed to install {name}",
"tool_upgrade_failed": "Failed to upgrade {name}",
"tool_required": "{name} is required for EvoNexus",
"tool_install_manually": "{name} not found — install manually",
"tool_skip_noninteractive": "Skipping auto-install in non-interactive mode.",
"tool_run_manually": "Run manually: {cmd}",
"tool_install_prompt": "Install {name}? (Y/n): ",
"tool_upgrade_hint": "(upgrading to {required}+)",
"installing_build_essential": "Installing build-essential...",
"build_essential_failed": "build-essential install failed",
"npm_not_found": "npm not found (should come with Node.js)",
"prereq_install_failed_header": "The following tools could not be installed:",
"prereq_install_manually_retry": "Install them manually and run setup again.",
"invalid_choice_local_mode": "Invalid choice '{choice}'. Using local mode.",
"no_domain_local_mode": "No domain provided, using local mode",
"nginx_config_test_failed": "Nginx config test failed",
"nginx_config_saved_at": "The config is saved at {path}",
"nginx_fix_and_reload": "Fix the issue and run: nginx -t && systemctl reload nginx",
"nginx_config_not_created": "Nginx config file was not created at {path}",
"nginx_no_permission": "No permission to write nginx config — run setup as root/sudo",
"removed_nginx_default_site": "Removed nginx default site",
# Install dir auto-relocation
"install_inaccessible": "User '{user}' cannot access {path} (likely /root/* with mode 700)",
"install_relocating": "Relocating install to {dest} so the service user can read it...",
"install_relocated": "Install relocated to {dest}",
"install_relocate_failed": "Failed to relocate install — check disk space / permissions",
"install_relocate_hint": "Original copy at {orig} can be removed after setup completes",
# AI Provider wizard
"choose_ai_provider_header": "Choose your AI provider:",
"provider_opt1_anthropic": "Anthropic (native Claude)",
"provider_opt1_note": "default, no extra config",
"provider_opt2_openrouter": "OpenRouter (200+ models)",
"provider_opt2_note": "requires API key + openclaude",
"provider_opt3_openai": "OpenAI (GPT-4.x / GPT-5.x)",
"provider_opt3_note": "API key or OAuth + openclaude",
"provider_opt4_gemini": "Google Gemini",
"provider_opt5_bedrock": "AWS Bedrock",
"provider_opt6_vertex": "Google Vertex AI",
"provider_coming_soon_label": "coming soon",
"provider_select_prompt": "Provider (1-3)",
"provider_coming_soon_fallback": "This provider is coming soon. Using Anthropic for now.",
"openclaude_not_found_for_provider": "openclaude not found — needed for {provider}",
"install_now_prompt": "Install now? (y/n)",
"provider_config_saved": "Saved provider config: {provider}",
"provider_remember_logout": "Remember to run /logout in Claude Code if previously logged into Anthropic",
"openai_auth_header": "OpenAI Authentication",
"openai_auth_opt_a": "API Key (GPT-4.x)",
"openai_auth_opt_b": "Codex OAuth (GPT-5.x) — via Dashboard",
"openai_auth_method_prompt": "Auth method (a/b)",
"openai_provider_configured": "Provider configured: OpenAI (Codex OAuth)",
"openai_complete_via_dashboard": "To complete authentication, open the Dashboard",
"openai_dashboard_path": "Providers → Login with OpenAI",
"configure_provider_header": "Configure {name}",
"multi_select_hint": "Enter keys to toggle (comma-separated), or press Enter to accept:",
# Brain Repo (versioning)
"brain_repo_enable_prompt": "Enable Brain Repo? (version your memory/workspace to GitHub)",
"brain_repo_auth_method": "Authentication method",
"brain_repo_defer_to_web": "Configure in web UI later",
"brain_repo_pat_instructions": "Create a GitHub PAT at https://github.com/settings/tokens/new?scopes=repo",
"brain_repo_pat_prompt": "GitHub Personal Access Token (scope: repo)",
"brain_repo_pat_saved": "Brain repo PAT saved. Complete setup in the web UI.",
"brain_repo_pat_skipped": "Skipped. You can configure Brain Repo in Settings > Integrations.",
"brain_repo_configure_later": "OK! You can connect GitHub in Settings > Integrations.",
},
"pt-BR": {
"choose_lang_prompt": "Choose your language / Escolha seu idioma / Elige tu idioma",
"choose_lang_option_1": "English (US)",
"choose_lang_option_2": "Português (BR)",
"choose_lang_option_3": "Español",
"choose_lang_ask": "Digite 1, 2 ou 3",
"banner_title": "EvoNexus — Assistente de Instalação",
"checking_prereqs": "Verificando pré-requisitos...",
"dashboard_access": "Acesso ao Dashboard",
"quick_remote_setup": "Configuração rápida para acesso remoto...",
"ai_provider": "Provedor de IA",
"about_you": "Sobre você",
"your_name": "Seu nome",
"company_name": "Nome da empresa",
"timezone": "Fuso horário",
"language": "Idioma",
"dashboard_port": "Porta do dashboard",
"creating_workspace": "Criando workspace...",
"installing_python_deps": "Instalando dependências Python...",
"installed_python_deps": "Dependências Python instaladas",
"python_deps_failed": "Falha ao instalar dependências Python",
"python_deps_needed": "Isso é necessário para o dashboard funcionar.",
"try_manually": "Execute manualmente:",
"log_at": "Log:",
"installing_dashboard_deps": "Instalando dependências do dashboard...",
"installed_dashboard_deps": "Dependências do dashboard instaladas",
"dashboard_deps_failed": "Falha ao instalar dependências do dashboard",
"building_dashboard": "Construindo o dashboard...",
"built_dashboard": "Dashboard construído",
"dashboard_build_failed": "Falha ao construir o dashboard",
"installing_terminal_deps": "Instalando dependências do terminal-server...",
"installed_terminal_deps": "Dependências do terminal-server instaladas",
"terminal_deps_failed": "Falha ao instalar dependências do terminal-server",
"services_user_note": "(serviços serão executados como {user})",
"terminal_started": "Terminal server iniciado (porta 32352)",
"terminal_not_started": "Terminal server pode não ter iniciado — verifique logs/terminal-server.log",
"dashboard_started": "Dashboard iniciado (porta 8080)",
"dashboard_not_started": "Dashboard pode não ter iniciado — verifique logs/dashboard.log",
"setup_done": "Instalação concluída!",
"dashboard_available_at": "Dashboard disponível em:",
"next_steps_header": "Próximos passos:",
"next_step_1_remote": "1. Acesse o link acima e crie sua conta de administrador",
"next_step_2_remote": "2. Vá em {bold}Provedores{reset} e configure seu provedor de IA",
"next_step_3_remote": "3. Abra um agente e comece a usar!",
"next_step_1_local": "1. Edite {bold}.env{reset} com suas chaves de API",
"next_step_2_local": "2. Execute: {bold}make dashboard-app{reset}",
"next_step_3_local": "3. Abra {bold}{url}{reset} e crie sua conta de administrador",
"systemd_section": "Serviço systemd:",
"systemd_status": "verificar status",
"systemd_restart": "reiniciar",
"systemd_logs": "ver logs",
"systemd_su": "acessar o usuário do serviço",
# Prerequisites + tools
"sys_packages_updating": "Atualizando pacotes do sistema...",
"sys_packages_updated": "Pacotes do sistema atualizados",
"tool_installed": "instalado",
# Dashboard access wizard
"local_only_option": "Apenas local (http://localhost:8080)",
"domain_ssl_option": "Domínio com SSL (recomendado para servidores remotos)",
"type_1_or_2": "Digite 1 ou 2",
"domain_prompt": "Domínio (ex: nexus.exemplo.com)",
"installing_nginx": "Instalando nginx...",
"nginx_installed": "nginx instalado",
"nginx_install_failed": "Falha ao instalar nginx, usando modo local",
"ssl_cert_prompt": "Certificado SSL (1=certbot, 2=auto-assinado, 3=caminho manual)",
"cert_existing_found": "Certificado certbot existente encontrado para {domain}",
"installing_certbot": "Instalando certbot...",
"certbot_installed": "certbot instalado",
"obtaining_ssl_certbot": "Obtendo certificado SSL via certbot...",
"ssl_obtained_certbot": "Certificado SSL obtido via certbot",
"certbot_failed_fallback": "certbot falhou — usando auto-assinado",
"generating_self_signed": "Gerando certificado SSL auto-assinado...",
"self_signed_generated": "Certificado SSL auto-assinado gerado",
"self_signed_cloudflare_note": "(Compatível com o modo SSL Full do Cloudflare)",
"self_signed_failed": "Falha ao gerar certificado SSL",
"no_ssl_cert_local_mode": "Sem certificado SSL disponível, usando modo local",
"manual_cert_prompt": "Caminho do certificado (.crt ou .pem)",
"manual_key_prompt": "Caminho da chave privada (.key)",
"nginx_configured_for": "Nginx configurado para {domain}",
"nginx_config_failed": "Falha ao configurar o nginx",
"configuring_firewall": "Configurando firewall...",
"firewall_ports_opened": "Portas do firewall abertas (80, 443)",
"firewall_using_ufw": "Usando ufw",
"firewall_using_iptables": "Usando iptables (ufw não instalado)",
"firewall_persisted": "Regras persistidas via {tool} (vão sobreviver ao reboot)",
"firewall_persistence_missing": "Regras abertas só na memória — instale netfilter-persistent OU ufw para persistir entre reboots",
"firewall_install_persistence": "Instalando netfilter-persistent para persistir regras no reboot...",
"firewall_failed": "Etapa do firewall falhou: {err}",
"firewall_cloud_provider_hint": "Firewall do provedor: se 80/443 ainda aparecerem bloqueados de fora, abra também na Security List/Group do seu provedor ({provider}).",
# Workspace file creation
"generated_workspace_yaml": "Gerado config/workspace.yaml",
"env_created_from_example": "Criado .env a partir do .env.example",
"env_example_missing": ".env.example não encontrado, criando .env vazio",
"env_already_exists": ".env já existe, ignorando",
"generated_master_key": "KNOWLEDGE_MASTER_KEY gerado (criptografia da Knowledge Base)",
"master_key_already_set": "KNOWLEDGE_MASTER_KEY já definido — preservado",
"master_key_skip_crypto_missing": "Geração de KNOWLEDGE_MASTER_KEY ignorada ({exc})",
"master_key_run_init_hint": "Rode `make init-key` após o setup para gerá-lo.",
"master_key_ensure_failed": "Não foi possível garantir KNOWLEDGE_MASTER_KEY: {exc}",
"generated_routines_yaml": "Criado config/routines.yaml",
"routines_already_exists": "config/routines.yaml já existe, ignorando",
"generated_claude_md": "Gerado CLAUDE.md",
"created_workspace_folders": "Pastas do workspace criadas ({count})",
# Systemd / service lifecycle
"fixing_ownership": "Ajustando permissões de arquivos para {user}...",
"ownership_fixed": "Permissões ajustadas",
"starting_dashboard_services": "Iniciando serviços do dashboard...",
"creating_systemd_service": "Criando serviço systemd...",
"systemd_service_created": "Serviço systemd criado e habilitado (inicia no boot)",
"systemd_manage_hint": "Gerencie com: systemctl {{start|stop|restart|status}} {service}",
# Prerequisite tool check
"tool_not_found": "{name} não encontrado",
"tool_installing_verb": "Instalando {name}...",
"tool_upgrading_verb": "Atualizando {name}...",
"tool_install_failed": "Falha ao instalar {name}",
"tool_upgrade_failed": "Falha ao atualizar {name}",
"tool_required": "{name} é obrigatório para o EvoNexus",
"tool_install_manually": "{name} não encontrado — instale manualmente",
"tool_skip_noninteractive": "Pulando instalação automática em modo não-interativo.",
"tool_run_manually": "Execute manualmente: {cmd}",
"tool_install_prompt": "Instalar {name}? (S/n): ",
"tool_upgrade_hint": "(atualizando para {required}+)",
"installing_build_essential": "Instalando build-essential...",
"build_essential_failed": "Falha ao instalar build-essential",
"npm_not_found": "npm não encontrado (deveria vir com o Node.js)",
"prereq_install_failed_header": "Os seguintes utilitários não puderam ser instalados:",
"prereq_install_manually_retry": "Instale-os manualmente e execute o setup novamente.",
"invalid_choice_local_mode": "Opção inválida '{choice}'. Usando modo local.",
"no_domain_local_mode": "Nenhum domínio informado, usando modo local",
"nginx_config_test_failed": "Teste de configuração do nginx falhou",
"nginx_config_saved_at": "A configuração foi salva em {path}",
"nginx_fix_and_reload": "Corrija o problema e execute: nginx -t && systemctl reload nginx",
"nginx_config_not_created": "Arquivo de configuração do nginx não foi criado em {path}",
"nginx_no_permission": "Sem permissão para escrever a configuração do nginx — execute o setup como root/sudo",
"removed_nginx_default_site": "Site padrão do nginx removido",
# Install dir auto-relocation
"install_inaccessible": "Usuário '{user}' não consegue acessar {path} (provavelmente /root/* com modo 700)",
"install_relocating": "Movendo instalação para {dest} para que o usuário do serviço consiga ler...",
"install_relocated": "Instalação movida para {dest}",
"install_relocate_failed": "Falha ao mover a instalação — verifique espaço em disco / permissões",
"install_relocate_hint": "A cópia original em {orig} pode ser removida após o setup terminar",
# AI Provider wizard
"choose_ai_provider_header": "Escolha seu provedor de IA:",
"provider_opt1_anthropic": "Anthropic (Claude nativo)",
"provider_opt1_note": "padrão, sem configuração extra",
"provider_opt2_openrouter": "OpenRouter (200+ modelos)",
"provider_opt2_note": "requer chave de API + openclaude",
"provider_opt3_openai": "OpenAI (GPT-4.x / GPT-5.x)",
"provider_opt3_note": "chave de API ou OAuth + openclaude",
"provider_opt4_gemini": "Google Gemini",
"provider_opt5_bedrock": "AWS Bedrock",
"provider_opt6_vertex": "Google Vertex AI",
"provider_coming_soon_label": "em breve",
"provider_select_prompt": "Provedor (1-3)",
"provider_coming_soon_fallback": "Este provedor estará disponível em breve. Usando Anthropic por enquanto.",
"openclaude_not_found_for_provider": "openclaude não encontrado — necessário para {provider}",
"install_now_prompt": "Instalar agora? (s/n)",
"provider_config_saved": "Configuração de provedor salva: {provider}",
"provider_remember_logout": "Lembre de rodar /logout no Claude Code se estava logado na Anthropic",
"openai_auth_header": "Autenticação OpenAI",
"openai_auth_opt_a": "Chave de API (GPT-4.x)",
"openai_auth_opt_b": "Codex OAuth (GPT-5.x) — via Dashboard",
"openai_auth_method_prompt": "Método de autenticação (a/b)",
"openai_provider_configured": "Provedor configurado: OpenAI (Codex OAuth)",
"openai_complete_via_dashboard": "Para concluir a autenticação, acesse o Dashboard",
"openai_dashboard_path": "Provedores → Login com OpenAI",
"configure_provider_header": "Configurar {name}",
"multi_select_hint": "Digite as teclas para alternar (separadas por vírgula), ou Enter para aceitar:",
# Brain Repo (versionamento)
"brain_repo_enable_prompt": "Ativar Brain Repo? (versione sua memória/workspace no GitHub)",
"brain_repo_auth_method": "Método de autenticação",
"brain_repo_defer_to_web": "Configurar pela interface web depois",
"brain_repo_pat_instructions": "Crie um PAT do GitHub em https://github.com/settings/tokens/new?scopes=repo",
"brain_repo_pat_prompt": "Token de Acesso Pessoal do GitHub (escopo: repo)",
"brain_repo_pat_saved": "PAT do Brain Repo salvo. Conclua a configuração pela interface web.",
"brain_repo_pat_skipped": "Pulado. Configure o Brain Repo em Configurações > Integrações.",
"brain_repo_configure_later": "Certo! Conecte o GitHub em Configurações > Integrações.",
},
"es": {
"choose_lang_prompt": "Choose your language / Escolha seu idioma / Elige tu idioma",
"choose_lang_option_1": "English (US)",
"choose_lang_option_2": "Português (BR)",
"choose_lang_option_3": "Español",
"choose_lang_ask": "Escribe 1, 2 o 3",
"banner_title": "EvoNexus — Asistente de Instalación",
"checking_prereqs": "Verificando requisitos...",
"dashboard_access": "Acceso al Dashboard",
"quick_remote_setup": "Configuración rápida para acceso remoto...",
"ai_provider": "Proveedor de IA",
"about_you": "Sobre ti",
"your_name": "Tu nombre",
"company_name": "Nombre de la empresa",
"timezone": "Zona horaria",
"language": "Idioma",
"dashboard_port": "Puerto del dashboard",
"creating_workspace": "Creando workspace...",
"installing_python_deps": "Instalando dependencias Python...",
"installed_python_deps": "Dependencias Python instaladas",
"python_deps_failed": "Error al instalar dependencias Python",
"python_deps_needed": "Esto es necesario para que el dashboard funcione.",
"try_manually": "Ejecuta manualmente:",
"log_at": "Registro:",
"installing_dashboard_deps": "Instalando dependencias del dashboard...",
"installed_dashboard_deps": "Dependencias del dashboard instaladas",
"dashboard_deps_failed": "Error al instalar dependencias del dashboard",
"building_dashboard": "Construyendo el dashboard...",
"built_dashboard": "Dashboard construido",
"dashboard_build_failed": "Error al construir el dashboard",
"installing_terminal_deps": "Instalando dependencias del terminal-server...",
"installed_terminal_deps": "Dependencias del terminal-server instaladas",
"terminal_deps_failed": "Error al instalar dependencias del terminal-server",
"services_user_note": "(los servicios se ejecutarán como {user})",
"terminal_started": "Terminal server iniciado (puerto 32352)",
"terminal_not_started": "Terminal server puede no haber iniciado — revisa logs/terminal-server.log",
"dashboard_started": "Dashboard iniciado (puerto 8080)",
"dashboard_not_started": "El dashboard puede no haber iniciado — revisa logs/dashboard.log",
"setup_done": "¡Instalación completada!",
"dashboard_available_at": "Dashboard disponible en:",
"next_steps_header": "Próximos pasos:",
"next_step_1_remote": "1. Abre el enlace de arriba y crea tu cuenta de administrador",
"next_step_2_remote": "2. Ve a {bold}Proveedores{reset} y configura tu proveedor de IA",
"next_step_3_remote": "3. ¡Abre un agente y comienza a usarlo!",
"next_step_1_local": "1. Edita {bold}.env{reset} con tus claves de API",
"next_step_2_local": "2. Ejecuta: {bold}make dashboard-app{reset}",
"next_step_3_local": "3. Abre {bold}{url}{reset} y crea tu cuenta de administrador",
"systemd_section": "Servicio systemd:",
"systemd_status": "verificar estado",
"systemd_restart": "reiniciar",
"systemd_logs": "ver registros",
"systemd_su": "entrar al usuario del servicio",
# Prerequisites + tools
"sys_packages_updating": "Actualizando paquetes del sistema...",
"sys_packages_updated": "Paquetes del sistema actualizados",
"tool_installed": "instalado",
# Dashboard access wizard
"local_only_option": "Solo local (http://localhost:8080)",
"domain_ssl_option": "Dominio con SSL (recomendado para servidores remotos)",
"type_1_or_2": "Escribe 1 o 2",
"domain_prompt": "Dominio (ej. nexus.ejemplo.com)",
"installing_nginx": "Instalando nginx...",
"nginx_installed": "nginx instalado",
"nginx_install_failed": "Error al instalar nginx, usando modo local",
"ssl_cert_prompt": "Certificado SSL (1=certbot, 2=autofirmado, 3=ruta manual)",
"cert_existing_found": "Certificado certbot existente encontrado para {domain}",
"installing_certbot": "Instalando certbot...",
"certbot_installed": "certbot instalado",
"obtaining_ssl_certbot": "Obteniendo certificado SSL vía certbot...",
"ssl_obtained_certbot": "Certificado SSL obtenido vía certbot",
"certbot_failed_fallback": "certbot falló — usando autofirmado",
"generating_self_signed": "Generando certificado SSL autofirmado...",
"self_signed_generated": "Certificado SSL autofirmado generado",
"self_signed_cloudflare_note": "(Compatible con el modo SSL Full de Cloudflare)",
"self_signed_failed": "Error al generar el certificado SSL",
"no_ssl_cert_local_mode": "Sin certificado SSL disponible, usando modo local",
"manual_cert_prompt": "Ruta del certificado (.crt o .pem)",
"manual_key_prompt": "Ruta de la clave privada (.key)",
"nginx_configured_for": "Nginx configurado para {domain}",
"nginx_config_failed": "Error al configurar nginx",
"configuring_firewall": "Configurando firewall...",
"firewall_ports_opened": "Puertos del firewall abiertos (80, 443)",
"firewall_using_ufw": "Usando ufw",
"firewall_using_iptables": "Usando iptables (ufw no está instalado)",
"firewall_persisted": "Reglas persistidas vía {tool} (sobrevivirán al reinicio)",
"firewall_persistence_missing": "Reglas abiertas solo en memoria — instala netfilter-persistent O ufw para persistir entre reinicios",
"firewall_install_persistence": "Instalando netfilter-persistent para que las reglas sobrevivan al reinicio...",
"firewall_failed": "El paso del firewall falló: {err}",
"firewall_cloud_provider_hint": "Firewall del proveedor: si 80/443 siguen bloqueados desde fuera, ábrelos también en la Security List/Group de tu proveedor ({provider}).",
# Workspace file creation
"generated_workspace_yaml": "Generado config/workspace.yaml",
"env_created_from_example": "Creado .env desde .env.example",
"env_example_missing": ".env.example no encontrado, creando .env vacío",
"env_already_exists": ".env ya existe, omitiendo",
"generated_master_key": "KNOWLEDGE_MASTER_KEY generada (cifrado de la Knowledge Base)",
"master_key_already_set": "KNOWLEDGE_MASTER_KEY ya definida — preservada",
"master_key_skip_crypto_missing": "Generación de KNOWLEDGE_MASTER_KEY omitida ({exc})",
"master_key_run_init_hint": "Ejecuta `make init-key` después del setup para generarla.",
"master_key_ensure_failed": "No se pudo asegurar KNOWLEDGE_MASTER_KEY: {exc}",
"generated_routines_yaml": "Creado config/routines.yaml",
"routines_already_exists": "config/routines.yaml ya existe, omitiendo",
"generated_claude_md": "Generado CLAUDE.md",
"created_workspace_folders": "Carpetas del workspace creadas ({count})",
# Systemd / service lifecycle
"fixing_ownership": "Ajustando permisos de archivos para {user}...",
"ownership_fixed": "Permisos ajustados",
"starting_dashboard_services": "Iniciando servicios del dashboard...",
"creating_systemd_service": "Creando servicio systemd...",
"systemd_service_created": "Servicio systemd creado y habilitado (inicia al arrancar)",
"systemd_manage_hint": "Administra con: systemctl {{start|stop|restart|status}} {service}",
# Prerequisite tool check
"tool_not_found": "{name} no encontrado",
"tool_installing_verb": "Instalando {name}...",
"tool_upgrading_verb": "Actualizando {name}...",
"tool_install_failed": "Error al instalar {name}",
"tool_upgrade_failed": "Error al actualizar {name}",
"tool_required": "{name} es necesario para EvoNexus",
"tool_install_manually": "{name} no encontrado — instálalo manualmente",
"tool_skip_noninteractive": "Omitiendo instalación automática en modo no interactivo.",
"tool_run_manually": "Ejecuta manualmente: {cmd}",
"tool_install_prompt": "¿Instalar {name}? (S/n): ",
"tool_upgrade_hint": "(actualizando a {required}+)",
"installing_build_essential": "Instalando build-essential...",
"build_essential_failed": "Error al instalar build-essential",
"npm_not_found": "npm no encontrado (debería venir con Node.js)",
"prereq_install_failed_header": "Las siguientes herramientas no pudieron instalarse:",
"prereq_install_manually_retry": "Instálalas manualmente y ejecuta el setup de nuevo.",
"invalid_choice_local_mode": "Opción inválida '{choice}'. Usando modo local.",
"no_domain_local_mode": "No se proporcionó dominio, usando modo local",
"nginx_config_test_failed": "La prueba de configuración de nginx falló",
"nginx_config_saved_at": "La configuración se guardó en {path}",
"nginx_fix_and_reload": "Corrige el problema y ejecuta: nginx -t && systemctl reload nginx",
"nginx_config_not_created": "El archivo de configuración de nginx no se creó en {path}",
"nginx_no_permission": "Sin permisos para escribir la configuración de nginx — ejecuta el setup como root/sudo",
"removed_nginx_default_site": "Sitio predeterminado de nginx eliminado",
# Install dir auto-relocation
"install_inaccessible": "El usuario '{user}' no puede acceder a {path} (probablemente /root/* con modo 700)",
"install_relocating": "Reubicando la instalación en {dest} para que el usuario del servicio pueda leerla...",
"install_relocated": "Instalación reubicada en {dest}",
"install_relocate_failed": "Error al reubicar la instalación — revisa espacio en disco / permisos",
"install_relocate_hint": "La copia original en {orig} puede eliminarse cuando el setup termine",
# AI Provider wizard
"choose_ai_provider_header": "Elige tu proveedor de IA:",
"provider_opt1_anthropic": "Anthropic (Claude nativo)",
"provider_opt1_note": "predeterminado, sin configuración extra",
"provider_opt2_openrouter": "OpenRouter (200+ modelos)",
"provider_opt2_note": "requiere clave de API + openclaude",
"provider_opt3_openai": "OpenAI (GPT-4.x / GPT-5.x)",
"provider_opt3_note": "clave de API u OAuth + openclaude",
"provider_opt4_gemini": "Google Gemini",
"provider_opt5_bedrock": "AWS Bedrock",
"provider_opt6_vertex": "Google Vertex AI",
"provider_coming_soon_label": "próximamente",
"provider_select_prompt": "Proveedor (1-3)",
"provider_coming_soon_fallback": "Este proveedor estará disponible próximamente. Usando Anthropic por ahora.",
"openclaude_not_found_for_provider": "openclaude no encontrado — necesario para {provider}",
"install_now_prompt": "¿Instalar ahora? (s/n)",
"provider_config_saved": "Configuración del proveedor guardada: {provider}",
"provider_remember_logout": "Recuerda ejecutar /logout en Claude Code si estabas conectado a Anthropic",
"openai_auth_header": "Autenticación de OpenAI",
"openai_auth_opt_a": "Clave de API (GPT-4.x)",
"openai_auth_opt_b": "Codex OAuth (GPT-5.x) — vía Dashboard",
"openai_auth_method_prompt": "Método de autenticación (a/b)",
"openai_provider_configured": "Proveedor configurado: OpenAI (Codex OAuth)",
"openai_complete_via_dashboard": "Para completar la autenticación, abre el Dashboard",
"openai_dashboard_path": "Proveedores → Iniciar sesión con OpenAI",
"configure_provider_header": "Configurar {name}",
"multi_select_hint": "Escribe las teclas para alternar (separadas por coma), o Enter para aceptar:",
# Brain Repo (control de versiones)
"brain_repo_enable_prompt": "¿Activar Brain Repo? (versiona tu memoria/workspace en GitHub)",
"brain_repo_auth_method": "Método de autenticación",
"brain_repo_defer_to_web": "Configurar en la interfaz web después",
"brain_repo_pat_instructions": "Crea un PAT de GitHub en https://github.com/settings/tokens/new?scopes=repo",
"brain_repo_pat_prompt": "Token de Acceso Personal de GitHub (alcance: repo)",
"brain_repo_pat_saved": "PAT del Brain Repo guardado. Completa la configuración en la interfaz web.",
"brain_repo_pat_skipped": "Omitido. Configura Brain Repo en Ajustes > Integraciones.",
"brain_repo_configure_later": "¡De acuerdo! Conecta GitHub en Ajustes > Integraciones.",
},
}
def T(key: str, **fmt) -> str:
"""Return a translated string for the active language with optional format args.
Always runs through ``.format()`` so that translated strings can embed
``{bold}`` / ``{reset}`` placeholders even when the caller passes no
extra args — otherwise those placeholders leak into the output as
literal text (a regression seen in the "2. Vá em {bold}Provedores{reset}"
line on the finished-setup screen).
"""
bundle = MESSAGES.get(LANG) or MESSAGES["en-US"]
# Fall back to en-US if a key is missing in the active bundle (defensive).
text = bundle.get(key) or MESSAGES["en-US"].get(key) or key
if not isinstance(text, str):
return str(text)
try:
return text.format(bold=BOLD, reset=RESET, **fmt)
except (KeyError, IndexError, ValueError):
# Translated string uses a placeholder we didn't supply — return
# the raw text rather than crashing the wizard.
return text
def select_language() -> None:
"""Ask the user for their setup-wizard language. First prompt, always.
Under non-interactive contexts (pip build backend, CI, `EVO_NEXUS_AUTO_INSTALL=1`)
keep the default "en-US" to stay predictable — the dashboard later lets
the user change it via the Settings UI.
"""
global LANG
auto = os.environ.get("EVO_NEXUS_AUTO_INSTALL") == "1"
if not _IS_TTY or auto:
return
print()
print(f" {BOLD}{T('choose_lang_prompt')}{RESET}")
print(f" {BOLD}1{RESET}) {T('choose_lang_option_1')}")
print(f" {BOLD}2{RESET}) {T('choose_lang_option_2')}")
print(f" {BOLD}3{RESET}) {T('choose_lang_option_3')}")
try:
raw = input(f" {T('choose_lang_ask')}: ").strip()
except (EOFError, KeyboardInterrupt):
print()
return
mapping = {"1": "en-US", "2": "pt-BR", "3": "es"}
LANG = mapping.get(raw, LANG)
def banner():
"""Draw a centered banner around the translated title.
Width auto-adjusts to the title so translated versions stay aligned —
em-dashes and accents are 1 display char each (we don't use any wide
CJK glyphs here), so len(title) is the visible width.
"""
title = T("banner_title")
interior = max(36, len(title) + 6) # at least 3 chars padding each side
total_pad = interior - len(title)
left = total_pad // 2
right = total_pad - left
hline = "═" * interior
print(f"""
{GREEN} ╔{hline}╗
║{' ' * left}{BOLD}{title}{RESET}{GREEN}{' ' * right}║
╚{hline}╝{RESET}
""")
def _parse_semver(s: str) -> tuple[int, int, int] | None:
"""Extract (major, minor, patch) from a version string. None on failure."""
import re
m = re.search(r'(\d+)\.(\d+)\.(\d+)', s or "")
if not m:
return None
try:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
except (ValueError, TypeError):
return None
def _check_tool(name, cmd, install_cmd=None, install_label=None, min_version=None):
"""Check if a tool is installed. If not, offer to install it.
If min_version=(major, minor, patch) is given and the installed tool is
older, treat it as missing and trigger the install path — this forces an
upgrade when the pinned install_cmd specifies a newer version.
In non-interactive contexts (pip build backend, npx pipe, CI) we skip
the input() prompt — this is what fixes EOFError from upstream PR #11.
When auto-confirm is appropriate (service user bootstrap), callers can
pass EVO_NEXUS_AUTO_INSTALL=1 to proceed without prompting.
"""
needs_upgrade = False
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
version = result.stdout.strip() or result.stderr.strip()
if min_version is not None:
parsed = _parse_semver(version)
if parsed is not None and parsed < min_version:
required = ".".join(str(x) for x in min_version)
print(f" {YELLOW}!{RESET} {name}: {DIM}{version}{RESET} {T('tool_upgrade_hint', required=required)}")
needs_upgrade = True
else:
print(f" {GREEN}✓{RESET} {name}: {DIM}{version}{RESET}")
return True
else:
print(f" {GREEN}✓{RESET} {name}: {DIM}{version}{RESET}")
return True
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
if install_cmd:
if not needs_upgrade:
print(f" {YELLOW}!{RESET} {T('tool_not_found', name=name)}")
# Non-interactive: skip the prompt entirely. Either auto-install
# (when EVO_NEXUS_AUTO_INSTALL=1) or report missing.
auto_install = os.environ.get("EVO_NEXUS_AUTO_INSTALL") == "1"
# For upgrades we always proceed silently — the user already has
# the tool and we just need a newer version.
if needs_upgrade:
choice = "y"
elif not _IS_TTY and not auto_install:
print(f" {DIM}{T('tool_skip_noninteractive')}{RESET}")
print(f" {DIM}{T('tool_run_manually', cmd=install_cmd)}{RESET}")
return False
elif auto_install:
choice = "y"
else:
choice = input(f" {T('tool_install_prompt', name=name)}").strip().lower()
if choice in ("", "y", "yes", "s", "sim"):
if needs_upgrade:
print(f" {DIM}{T('tool_upgrading_verb', name=name)}{RESET}", end="", flush=True)
else:
print(f" {DIM}{T('tool_installing_verb', name=name)}{RESET}", end="", flush=True)
ret = os.system(f"{install_cmd} > /dev/null 2>&1")
# Re-check after install
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
version = result.stdout.strip() or result.stderr.strip()
print(f"\r {GREEN}✓{RESET} {name}: {DIM}{version}{RESET} ")
return True
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
fail_msg = T('tool_upgrade_failed', name=name) if needs_upgrade else T('tool_install_failed', name=name)
print(f"\r {RED}✗{RESET} {fail_msg} ")
else:
print(f" {RED}✗{RESET} {T('tool_required', name=name)}")
else:
suffix = install_label or T('tool_install_manually', name=name)
print(f" {RED}✗{RESET} {T('tool_not_found', name=name)} — {suffix}")
return False
def check_prerequisites():
"""Check and auto-install required tools."""
# Update system packages first (ensures fresh package lists)
if os.getuid() == 0:
print(f" {DIM}{T('sys_packages_updating')}{RESET}", end="", flush=True)
os.system("DEBIAN_FRONTEND=noninteractive apt-get update -y -qq > /dev/null 2>&1")
os.system("DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1")
print(f"\r {GREEN}✓{RESET} {T('sys_packages_updated')} ")
missing = []
# build-essential (required for native npm packages like node-pty)
try:
result = subprocess.run(["g++", "--version"], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
print(f" {GREEN}✓{RESET} build-essential: {DIM}{T('tool_installed')}{RESET}")
else:
raise FileNotFoundError
except (FileNotFoundError, subprocess.TimeoutExpired):
print(f" {DIM}{T('installing_build_essential')}{RESET}", end="", flush=True)
os.system("apt install -y build-essential > /dev/null 2>&1 || yum groupinstall -y 'Development Tools' > /dev/null 2>&1")
try:
result = subprocess.run(["g++", "--version"], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
print(f" {GREEN}✓{RESET} build-essential: {DIM}{T('tool_installed')}{RESET}")
else:
print(f" {RED}✗{RESET} {T('build_essential_failed')}")
missing.append("build-essential")
except (FileNotFoundError, subprocess.TimeoutExpired):
print(f" {RED}✗{RESET} {T('build_essential_failed')}")
missing.append("build-essential")
# Node.js
if not _check_tool("Node.js", ["node", "--version"],
install_cmd="curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt install -y nodejs 2>/dev/null || echo 'Install Node.js 18+ from https://nodejs.org'",
install_label="https://nodejs.org"):
missing.append("node")
# npm (comes with Node.js)
npm_ok = False
for cmd in ["npm", "npm.cmd"]:
try:
result = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
print(f" {GREEN}✓{RESET} npm: {DIM}v{result.stdout.strip()}{RESET}")
npm_ok = True
break
except (FileNotFoundError, subprocess.TimeoutExpired):
continue
if not npm_ok:
print(f" {RED}✗{RESET} {T('npm_not_found')}")
missing.append("npm")
# uv (Python package manager)
# When running with sudo, install for the original user and add their
# ~/.local/bin to root's PATH BEFORE verification
_sudo_user_uv = os.environ.get("SUDO_USER", "")
if _sudo_user_uv and os.getuid() == 0:
# Resolve user home FIRST so we can find uv after install
try:
user_home = subprocess.run(["getent", "passwd", _sudo_user_uv], capture_output=True, text=True).stdout.split(":")[5]
except (IndexError, Exception):
user_home = f"/home/{_sudo_user_uv}"
user_uv_bin = os.path.join(user_home, ".local", "bin")
# Add user's bin to PATH before any uv checks
if user_uv_bin not in os.environ.get("PATH", ""):
os.environ["PATH"] = f"{user_uv_bin}:{os.environ.get('PATH', '')}"
# Now check/install
if not _check_tool("uv", ["uv", "--version"],
install_cmd=f"su - {_sudo_user_uv} -c 'curl -LsSf https://astral.sh/uv/install.sh | sh'"):
missing.append("uv")
else:
home_bin = os.path.join(os.path.expanduser("~"), ".local", "bin")
if home_bin not in os.environ.get("PATH", ""):
os.environ["PATH"] = f"{home_bin}:{os.environ.get('PATH', '')}"
if not _check_tool("uv", ["uv", "--version"],
install_cmd="curl -LsSf https://astral.sh/uv/install.sh | sh"):
missing.append("uv")
# Claude Code CLI
if not _check_tool("Claude Code CLI", ["claude", "--version"],
install_cmd="npm install -g @anthropic-ai/claude-code"):
missing.append("claude")
# OpenClaude (required for non-Anthropic providers)
# min_version=(0, 3, 0) forces an upgrade on systems that already have
# an older OpenClaude installed — v0.3.0 is the first release with the
# "route OpenAI Codex shortcuts to correct endpoint" fix (#566) which
# the codex_auth provider flow relies on.
if not _check_tool("OpenClaude", ["openclaude", "--version"],
install_cmd="npm install -g @gitlawb/openclaude@latest",
min_version=(0, 3, 0)):
missing.append("openclaude")
print()
if missing:
print(f" {RED}{T('prereq_install_failed_header')}{RESET}")
for m in missing:
print(f" {RED}•{RESET} {m}")
print(f"\n {YELLOW}{T('prereq_install_manually_retry')}{RESET}")
sys.exit(1)
return True
def _detect_cloud_provider() -> str | None:
"""Best-effort cloud-provider detection for the firewall hint message.
Looks at /sys/class/dmi/id/* (set by the BIOS/hypervisor) — non-fatal
if unreadable. Returns a short human-readable label or None.
"""
sources = [
("/sys/class/dmi/id/sys_vendor", {"oraclecloud": "Oracle Cloud (OCI)", "amazon ec2": "AWS EC2",
"google": "Google Cloud", "microsoft": "Azure", "digitalocean": "DigitalOcean",
"hetzner": "Hetzner Cloud"}),
("/sys/class/dmi/id/chassis_asset_tag", {"oraclecloud": "Oracle Cloud (OCI)",
"amazon": "AWS EC2", "google": "Google Cloud"}),
]
for path, mapping in sources:
try:
with open(path) as f:
value = f.read().strip().lower()
for needle, label in mapping.items():
if needle in value:
return label
except OSError:
continue
return None
def _open_firewall_ports(ports: list[int]) -> None:
"""Open inbound TCP ports robustly and PERSISTENTLY.
The previous one-liner used ``2>/dev/null`` everywhere, so any failure
(ufw missing, iptables-nft refusing the rule, no permission) was
silently swallowed and the wizard happily printed "Firewall ports
opened" while nothing actually changed. Worse, it never persisted
the rules, so on the first reboot the in-memory iptables additions
vanished and the dashboard was unreachable from outside.
Strategy:
1. Prefer ``ufw`` when present — handles persistence itself.
2. Otherwise use ``iptables -C`` to check before ``-I`` (idempotent
re-runs on the same machine don't pile up duplicate rules).
3. Persist via ``netfilter-persistent save`` if available; if not
and we're on a Debian/Ubuntu system, install
``iptables-persistent`` non-interactively then save.
4. If everything we tried fails, surface the actual error rather
than reporting success.
5. Always emit a hint about cloud-provider security lists — Oracle
Cloud, AWS, GCP, etc. enforce a separate network firewall that
no host-level command can bypass.
"""
print(f" {DIM}{T('configuring_firewall')}{RESET}")
if os.getuid() != 0:
# Non-root: we can't open the firewall anyway. Just hint.
print(f" {YELLOW}!{RESET} {T('firewall_persistence_missing')}")
return
backend_used = None
errors: list[str] = []
if shutil.which("ufw"):
backend_used = "ufw"
print(f" {DIM} {T('firewall_using_ufw')}{RESET}")
for p in ports:
rc = os.system(f"ufw allow {p}/tcp >/dev/null 2>&1")
if rc != 0:
errors.append(f"ufw allow {p}/tcp (rc={rc >> 8})")
elif shutil.which("iptables"):
backend_used = "iptables"
print(f" {DIM} {T('firewall_using_iptables')}{RESET}")
for p in ports:
# -C tests whether the rule already exists; -I inserts at
# the top of INPUT only when it doesn't (idempotent).
check = os.system(f"iptables -C INPUT -p tcp --dport {p} -j ACCEPT >/dev/null 2>&1")
if check != 0:
rc = os.system(f"iptables -I INPUT -p tcp --dport {p} -j ACCEPT 2>/dev/null")
if rc != 0:
errors.append(f"iptables -I {p} (rc={rc >> 8})")
else:
errors.append("neither ufw nor iptables available")
# Persistence — the actual fix for "works after install, dies on reboot".
persistence_tool = None
if backend_used == "ufw":
# ufw persists by default
persistence_tool = "ufw"
elif backend_used == "iptables":
if shutil.which("netfilter-persistent"):
rc = os.system("netfilter-persistent save >/dev/null 2>&1")
if rc == 0:
persistence_tool = "netfilter-persistent"
else:
errors.append(f"netfilter-persistent save (rc={rc >> 8})")
elif shutil.which("apt-get"):
print(f" {DIM} {T('firewall_install_persistence')}{RESET}")
os.system(
"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "
"iptables-persistent netfilter-persistent >/dev/null 2>&1"
)
if shutil.which("netfilter-persistent"):
rc = os.system("netfilter-persistent save >/dev/null 2>&1")
if rc == 0:
persistence_tool = "netfilter-persistent"
else:
errors.append(f"netfilter-persistent save (rc={rc >> 8})")
else:
# Last-resort manual save
os.makedirs("/etc/iptables", exist_ok=True)
rc = os.system("iptables-save > /etc/iptables/rules.v4 2>/dev/null")
if rc == 0:
persistence_tool = "/etc/iptables/rules.v4"
# Result reporting — no more silent success.
if errors:
print(f" {YELLOW}!{RESET} {T('firewall_failed', err='; '.join(errors))}")
else:
print(f" {GREEN}✓{RESET} {T('firewall_ports_opened')}")
if persistence_tool:
print(f" {GREEN}✓{RESET} {T('firewall_persisted', tool=persistence_tool)}")
else:
print(f" {YELLOW}!{RESET} {T('firewall_persistence_missing')}")