2025-08-07 06:09:37 -04:00
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2025-10-31 01:10:54 -04:00
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
import configparser
|
2025-08-07 06:09:37 -04:00
|
|
|
import datetime
|
2025-04-26 17:12:38 -04:00
|
|
|
import getpass
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
2026-07-15 07:48:12 -04:00
|
|
|
import shlex
|
2025-08-07 06:09:37 -04:00
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
2025-05-04 00:40:05 -04:00
|
|
|
import sys
|
|
|
|
|
import time
|
2025-12-27 05:23:48 -05:00
|
|
|
import xml.etree.ElementTree as ET
|
2025-10-31 01:10:54 -04:00
|
|
|
import zipfile
|
|
|
|
|
|
|
|
|
|
new_path = os.path.normpath(
|
|
|
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
|
|
|
)
|
|
|
|
|
sys.path.append(new_path)
|
|
|
|
|
|
|
|
|
|
from script.config import config_file
|
2026-02-19 12:27:35 -05:00
|
|
|
from script.execute import execute
|
2026-03-10 03:59:35 -04:00
|
|
|
from script.todo.database_manager import DatabaseManager
|
|
|
|
|
from script.todo.kdbx_manager import KdbxManager
|
2026-03-07 02:59:07 -05:00
|
|
|
from script.todo.todo_i18n import get_lang, lang_is_configured, set_lang, t
|
2026-03-10 03:59:35 -04:00
|
|
|
from script.todo.version_manager import get_odoo_version
|
2025-08-07 06:09:37 -04:00
|
|
|
|
2026-03-10 03:06:07 -04:00
|
|
|
ERROR_LOG_PATH = ".erplibre.error.txt"
|
|
|
|
|
VENV_ERPLIBRE = ".venv.erplibre"
|
2025-08-07 06:09:37 -04:00
|
|
|
ENABLE_CRASH = False
|
|
|
|
|
CRASH_E = None
|
2025-12-27 05:23:48 -05:00
|
|
|
# Support mobile ERPLibre
|
|
|
|
|
ANDROID_DIR = "android"
|
2025-12-28 00:37:45 -05:00
|
|
|
MOBILE_HOME_PATH = "./mobile/erplibre_home_mobile"
|
2025-12-27 05:23:48 -05:00
|
|
|
STRINGS_FILE = os.path.join(
|
|
|
|
|
MOBILE_HOME_PATH, ANDROID_DIR, "app/src/main/res/values/strings.xml"
|
|
|
|
|
)
|
|
|
|
|
GRADLE_FILE = os.path.join(MOBILE_HOME_PATH, ANDROID_DIR, "app/build.gradle")
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2026-02-14 05:53:31 -05:00
|
|
|
|
2025-05-04 00:40:05 -04:00
|
|
|
try:
|
|
|
|
|
import click
|
2025-10-01 04:53:08 -04:00
|
|
|
import dotenv
|
2025-08-07 06:09:37 -04:00
|
|
|
import humanize
|
2025-05-04 00:40:05 -04:00
|
|
|
import openai
|
2025-10-31 01:10:54 -04:00
|
|
|
import todo_file_browser
|
2025-08-07 06:09:37 -04:00
|
|
|
|
|
|
|
|
# import urwid
|
|
|
|
|
# TODO implement rich for beautiful print and table
|
|
|
|
|
# import rich
|
2025-10-31 01:10:54 -04:00
|
|
|
import todo_upgrade
|
|
|
|
|
from pykeepass import PyKeePass
|
2025-05-04 00:40:05 -04:00
|
|
|
except ModuleNotFoundError as e:
|
2025-08-07 06:09:37 -04:00
|
|
|
humanize = None
|
|
|
|
|
ENABLE_CRASH = True
|
|
|
|
|
CRASH_E = e
|
2025-05-04 01:02:12 -04:00
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
if not ENABLE_CRASH:
|
2026-03-07 02:59:07 -05:00
|
|
|
print(t("Importation success!"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
logging.basicConfig(
|
|
|
|
|
format=(
|
|
|
|
|
"%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]"
|
|
|
|
|
" %(message)s"
|
|
|
|
|
),
|
|
|
|
|
datefmt="%Y-%m-%d:%H:%M:%S",
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
)
|
2025-04-26 17:12:38 -04:00
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
CONFIG_FILE = "./script/todo/todo.json"
|
2025-10-31 01:10:54 -04:00
|
|
|
CONFIG_OVERRIDE_FILE = "./private/todo/todo.json"
|
2025-04-26 17:12:38 -04:00
|
|
|
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TODO:
|
|
|
|
|
def __init__(self):
|
2025-10-31 01:10:54 -04:00
|
|
|
self.dir_path = None
|
2026-03-10 03:45:11 -04:00
|
|
|
self.selected_file_path = None
|
2025-10-31 01:10:54 -04:00
|
|
|
self.config_file = config_file.ConfigFile()
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute = execute.Execute()
|
2026-03-10 03:59:35 -04:00
|
|
|
self.kdbx_manager = KdbxManager(self.config_file)
|
2026-03-10 04:05:04 -04:00
|
|
|
self.db_manager = DatabaseManager(self.execute, self.fill_help_info)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2026-03-07 02:59:07 -05:00
|
|
|
def _ask_language(self):
|
|
|
|
|
if not lang_is_configured():
|
|
|
|
|
print()
|
|
|
|
|
print("Choisir la langue / Choose language:")
|
|
|
|
|
print("[1] Francais")
|
|
|
|
|
print("[2] English")
|
|
|
|
|
choice = ""
|
|
|
|
|
while choice not in ("1", "2"):
|
|
|
|
|
choice = input("Select / Choisir : ").strip()
|
|
|
|
|
if choice == "1":
|
|
|
|
|
set_lang("fr")
|
|
|
|
|
else:
|
|
|
|
|
set_lang("en")
|
|
|
|
|
|
|
|
|
|
def _change_language(self):
|
|
|
|
|
print()
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Choose language / Choisir la langue") + ":")
|
|
|
|
|
print(f"[1] {t('French')}")
|
|
|
|
|
print(f"[2] {t('English')}")
|
|
|
|
|
print(f"[0] {t('Back')}")
|
2026-03-07 02:59:07 -05:00
|
|
|
choice = ""
|
|
|
|
|
while choice not in ("0", "1", "2"):
|
2026-03-12 05:31:45 -04:00
|
|
|
choice = input(t("Select: ")).strip()
|
2026-03-07 02:59:07 -05:00
|
|
|
if choice == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif choice == "1":
|
|
|
|
|
set_lang("fr")
|
|
|
|
|
else:
|
|
|
|
|
set_lang("en")
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Language changed to: English"))
|
2026-03-07 02:59:07 -05:00
|
|
|
|
2025-04-26 17:12:38 -04:00
|
|
|
def run(self):
|
2025-10-31 01:10:54 -04:00
|
|
|
with open(self.config_file.get_logo_ascii_file_path()) as my_file:
|
2025-04-26 17:12:38 -04:00
|
|
|
print(my_file.read())
|
2026-03-07 02:59:07 -05:00
|
|
|
self._ask_language()
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Opening TODO ..."))
|
|
|
|
|
print(f"🤖 {t('=> Enter your choice by number and press Enter!')}")
|
|
|
|
|
help_info = f"""{t("Command:")}
|
|
|
|
|
[1] {t("Execute")}
|
|
|
|
|
[2] {t("Install")}
|
|
|
|
|
[3] {t("Question")}
|
|
|
|
|
[4] {t("Fork - Open TODO in a new tab")}
|
|
|
|
|
[0] {t("Quit")}
|
2025-04-26 17:12:38 -04:00
|
|
|
"""
|
|
|
|
|
while True:
|
2025-08-07 06:09:37 -04:00
|
|
|
try:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
except NameError:
|
|
|
|
|
print("Do")
|
2026-03-10 03:06:07 -04:00
|
|
|
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
|
2025-08-07 06:09:37 -04:00
|
|
|
sys.exit(1)
|
|
|
|
|
except ImportError:
|
|
|
|
|
print("Do")
|
2026-03-10 03:06:07 -04:00
|
|
|
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
|
2025-08-07 06:09:37 -04:00
|
|
|
sys.exit(1)
|
2025-10-31 01:10:54 -04:00
|
|
|
except click.exceptions.Abort:
|
|
|
|
|
sys.exit(0)
|
2025-04-26 17:12:38 -04:00
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
break
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.prompt_execute()
|
2025-05-04 01:02:12 -04:00
|
|
|
elif status == "2":
|
2025-08-07 06:09:37 -04:00
|
|
|
self.prompt_install()
|
2025-04-26 17:12:38 -04:00
|
|
|
elif status == "3":
|
2025-08-07 06:09:37 -04:00
|
|
|
self.execute_prompt_ia()
|
|
|
|
|
elif status == "4":
|
|
|
|
|
# cmd = (
|
|
|
|
|
# f"gnome-terminal --tab -- bash -c 'source"
|
2026-03-10 03:06:07 -04:00
|
|
|
# f" ./{VENV_ERPLIBRE}/bin/activate;make todo'"
|
2025-08-07 06:09:37 -04:00
|
|
|
# )
|
|
|
|
|
cmd = "make todo"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
2025-04-26 17:12:38 -04:00
|
|
|
# elif status == "3" or status == "install":
|
|
|
|
|
# print("install")
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
print(status)
|
|
|
|
|
# manipuler()
|
|
|
|
|
|
|
|
|
|
def execute_prompt_ia(self):
|
|
|
|
|
while True:
|
2026-03-12 05:31:45 -04:00
|
|
|
help_info = f"""{t("Command:")}
|
|
|
|
|
[0] {t("Back")}
|
|
|
|
|
{t("Write your question ")}"""
|
2025-04-26 17:12:38 -04:00
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return
|
2026-03-10 03:59:35 -04:00
|
|
|
kp = self.kdbx_manager.get_kdbx()
|
2025-04-26 17:12:38 -04:00
|
|
|
if not kp:
|
|
|
|
|
return
|
2026-03-10 03:45:11 -04:00
|
|
|
config_name = self.config_file.get_config_value(
|
2025-04-26 17:12:38 -04:00
|
|
|
["kdbx_config", "openai", "kdbx_key"]
|
|
|
|
|
)
|
2026-03-10 03:45:11 -04:00
|
|
|
entry = kp.find_entries_by_title(config_name, first=True)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
client = openai.OpenAI(api_key=entry.password)
|
|
|
|
|
prompt_update = status
|
|
|
|
|
completion = client.chat.completions.create(
|
|
|
|
|
model="gpt-4o",
|
|
|
|
|
messages=[{"role": "user", "content": prompt_update}],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print(completion.choices[0].message.content)
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
def prompt_execute(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
help_info = f"""{t("Command:")}
|
|
|
|
|
[1] {t("Automation - Demonstration of developed features")}
|
|
|
|
|
[2] {t("Code - Developer tools")}
|
|
|
|
|
[3] {t("Config - Configuration file management")}
|
|
|
|
|
[4] {t("Database - Database tools")}
|
|
|
|
|
[5] {t("Doc - Documentation search")}
|
|
|
|
|
[6] {t("Git - Git tools")}
|
|
|
|
|
[7] {t("GPT code - AI assistant tools")}
|
|
|
|
|
[8] {t("Language - Change language / Changer la langue")}
|
|
|
|
|
[9] {t("Network - Network tools")}
|
|
|
|
|
[10] {t("Process - Execution tools")}
|
|
|
|
|
[11] {t("Run - Execute and install an instance")}
|
|
|
|
|
[12] {t("Security - Dependency security audit")}
|
|
|
|
|
[13] {t("Test - Test an Odoo module")}
|
|
|
|
|
[14] {t("Update - Update all developed staging source code")}
|
2026-03-12 05:40:22 -04:00
|
|
|
[15] {t("Deploy - Deploy ERPLibre locally")}
|
2026-03-12 05:31:45 -04:00
|
|
|
[0] {t("Back")}
|
2025-04-26 17:12:38 -04:00
|
|
|
"""
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return
|
|
|
|
|
elif status == "1":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_function()
|
2025-04-26 17:12:38 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
|
|
|
|
elif status == "2":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_code()
|
2025-04-26 17:12:38 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2025-05-04 01:02:12 -04:00
|
|
|
elif status == "3":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_config()
|
2025-05-04 01:02:12 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
|
|
|
|
elif status == "4":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_database()
|
2025-05-04 01:02:12 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2025-10-31 01:10:54 -04:00
|
|
|
elif status == "5":
|
|
|
|
|
status = self.prompt_execute_doc()
|
|
|
|
|
if status is not False:
|
|
|
|
|
return
|
|
|
|
|
elif status == "6":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_git()
|
2025-10-31 01:10:54 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2025-12-22 04:02:44 -05:00
|
|
|
elif status == "7":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_gpt_code()
|
2025-12-22 04:02:44 -05:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2026-02-13 03:46:27 -05:00
|
|
|
elif status == "8":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self._change_language()
|
2026-02-13 03:46:27 -05:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2026-02-28 03:24:22 -05:00
|
|
|
elif status == "9":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_network()
|
2026-02-28 03:24:22 -05:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2026-03-07 01:38:55 -05:00
|
|
|
elif status == "10":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_process()
|
2026-03-07 01:38:55 -05:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2026-03-07 02:59:07 -05:00
|
|
|
elif status == "11":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_instance()
|
2026-03-08 15:53:51 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
|
|
|
|
elif status == "12":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_security()
|
2026-03-09 16:34:43 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
|
|
|
|
elif status == "13":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_test()
|
2026-03-07 02:59:07 -05:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2026-03-10 02:05:53 -04:00
|
|
|
elif status == "14":
|
2026-03-11 04:03:25 -04:00
|
|
|
status = self.prompt_execute_update()
|
2026-03-10 02:05:53 -04:00
|
|
|
if status is not False:
|
|
|
|
|
return
|
2026-03-12 05:40:22 -04:00
|
|
|
elif status == "15":
|
|
|
|
|
status = self.prompt_execute_deploy()
|
|
|
|
|
if status is not False:
|
|
|
|
|
return
|
2025-04-26 17:12:38 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
def prompt_install(self):
|
|
|
|
|
print("Detect first installation from code source.")
|
|
|
|
|
|
|
|
|
|
first_installation_input = (
|
|
|
|
|
input(
|
2025-10-31 01:10:54 -04:00
|
|
|
"💬 First system installation? This will process system installation"
|
2025-08-07 06:09:37 -04:00
|
|
|
" before (Y/N): "
|
|
|
|
|
)
|
|
|
|
|
.strip()
|
2025-10-31 01:10:54 -04:00
|
|
|
.lower()
|
2025-08-07 06:09:37 -04:00
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
if first_installation_input == "y":
|
2025-08-07 06:09:37 -04:00
|
|
|
cmd = "./script/version/update_env_version.py --install"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
2025-08-07 06:09:37 -04:00
|
|
|
print("Wait after OS installation before continue.")
|
|
|
|
|
|
|
|
|
|
# First detect pycharm, need to be open before installation and close to increase speed
|
|
|
|
|
has_pycharm = False
|
|
|
|
|
has_pycharm_community = False
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
["which", "pycharm"],
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
has_pycharm = True
|
|
|
|
|
else:
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
["which", "pycharm-community"],
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
has_pycharm_community = result.returncode == 0
|
|
|
|
|
if (has_pycharm or has_pycharm_community) and not os.path.exists(
|
|
|
|
|
".idea"
|
|
|
|
|
):
|
|
|
|
|
pycharm_configuration_input = (
|
2025-10-31 01:10:54 -04:00
|
|
|
input("💬 Open Pycharm? (Y/N): ").strip().lower()
|
2025-08-07 06:09:37 -04:00
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
if pycharm_configuration_input == "y":
|
2025-08-07 06:09:37 -04:00
|
|
|
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
cmd = f"cd {os.getcwd()} && {pycharm_bin} ./"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
new_window=True,
|
|
|
|
|
)
|
2025-08-07 06:09:37 -04:00
|
|
|
print(
|
2025-10-31 01:10:54 -04:00
|
|
|
"👹 WAIT and Close Pycharm when processing is done before continue"
|
2025-08-07 06:09:37 -04:00
|
|
|
" this guide."
|
|
|
|
|
)
|
|
|
|
|
# TODO detect last version supported
|
2025-10-31 01:10:54 -04:00
|
|
|
# cmd_intern = "./script/install/install_erplibre.sh"
|
|
|
|
|
# TODO maybe update q to only install erplibre from install_locally
|
|
|
|
|
# TODO problem installing with q, the script depend on odoo
|
|
|
|
|
key_i = 0
|
2026-03-10 03:45:11 -04:00
|
|
|
commands_begin = {
|
2025-10-31 01:10:54 -04:00
|
|
|
"q": (
|
|
|
|
|
"q",
|
|
|
|
|
"q: ERPLibre only with system python without Odoo",
|
|
|
|
|
"./script/install/install_erplibre.sh",
|
|
|
|
|
),
|
|
|
|
|
"w": (
|
|
|
|
|
"w",
|
|
|
|
|
"w: Install all Odoo version with ERPLibre",
|
|
|
|
|
"make install_odoo_all_version",
|
|
|
|
|
),
|
2025-10-01 03:01:00 -04:00
|
|
|
"m": (
|
|
|
|
|
"m",
|
|
|
|
|
"m: ERPLibre with mobile home",
|
|
|
|
|
"./mobile/install_and_run.sh",
|
|
|
|
|
),
|
2025-10-31 01:10:54 -04:00
|
|
|
"0": (
|
|
|
|
|
"0",
|
2026-03-12 05:31:45 -04:00
|
|
|
f"0: {t('Quit')}",
|
2025-10-31 01:10:54 -04:00
|
|
|
),
|
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
commands_end = {}
|
|
|
|
|
versions, installed_versions, odoo_installed_version = (
|
2026-03-10 03:59:35 -04:00
|
|
|
get_odoo_version()
|
2025-08-07 06:09:37 -04:00
|
|
|
)
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
for version_info in versions[::-1]:
|
2025-10-31 01:10:54 -04:00
|
|
|
key_i += 1
|
|
|
|
|
key_s = str(key_i)
|
2026-03-10 03:45:11 -04:00
|
|
|
label = f"{key_s}: Odoo {version_info.get('odoo_version')}"
|
2025-10-31 01:10:54 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
odoo_version = f"odoo{version_info.get('odoo_version')}"
|
|
|
|
|
if odoo_version in installed_versions:
|
2025-10-31 01:10:54 -04:00
|
|
|
label += " - Installed"
|
|
|
|
|
if odoo_version == odoo_installed_version:
|
|
|
|
|
label += " - Actual"
|
2026-03-12 05:31:45 -04:00
|
|
|
if version_info.get("Default"):
|
2025-10-31 01:10:54 -04:00
|
|
|
label += " - Default"
|
2026-03-10 03:45:11 -04:00
|
|
|
if version_info.get("is_deprecated"):
|
2025-10-31 01:10:54 -04:00
|
|
|
label += " - Deprecated"
|
2026-03-10 03:45:11 -04:00
|
|
|
erplibre_version = version_info.get("erplibre_version")
|
|
|
|
|
commands_begin[key_s] = (
|
2025-10-31 01:10:54 -04:00
|
|
|
key_s,
|
|
|
|
|
label,
|
|
|
|
|
f"./script/version/update_env_version.py --erplibre_version {erplibre_version} --install_dev",
|
2025-08-07 06:09:37 -04:00
|
|
|
)
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
# Add final command
|
2026-03-10 03:45:11 -04:00
|
|
|
install_commands = {**commands_begin, **commands_end}
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
# Show command
|
|
|
|
|
odoo_version_input = ""
|
2026-03-10 03:45:11 -04:00
|
|
|
while odoo_version_input not in install_commands:
|
2025-10-31 01:10:54 -04:00
|
|
|
if odoo_version_input:
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
f"{t('Error, cannot understand value')} '{odoo_version_input}'"
|
|
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
str_input_dyn_odoo_version = (
|
2026-03-12 05:31:45 -04:00
|
|
|
f"💬 {t('Choose a version:')}\n\t"
|
2026-03-10 03:45:11 -04:00
|
|
|
+ "\n\t".join([a[1] for a in install_commands.values()])
|
2026-03-12 05:31:45 -04:00
|
|
|
+ f"\n{t('Select: ')}"
|
2025-10-31 01:10:54 -04:00
|
|
|
)
|
|
|
|
|
odoo_version_input = (
|
|
|
|
|
input(str_input_dyn_odoo_version).strip().lower()
|
|
|
|
|
)
|
2025-08-07 06:09:37 -04:00
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
if odoo_version_input == "0":
|
|
|
|
|
return
|
2025-08-07 06:09:37 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
cmd_intern = install_commands.get(odoo_version_input)[2]
|
2026-03-30 14:46:56 -04:00
|
|
|
|
|
|
|
|
# For numbered version selections, offer extra modules sub-menu
|
|
|
|
|
if odoo_version_input.isdigit():
|
|
|
|
|
extra_choices = {
|
|
|
|
|
"1": (
|
|
|
|
|
"1",
|
|
|
|
|
f"1: {t('Standard install (without extra modules)')}",
|
|
|
|
|
),
|
|
|
|
|
"2": (
|
|
|
|
|
"2",
|
|
|
|
|
f"2: {t('Install with extra modules (CybroOdoo - large, slow)')}",
|
|
|
|
|
),
|
|
|
|
|
"0": ("0", f"0: {t('Back')}"),
|
|
|
|
|
}
|
|
|
|
|
extra_input = ""
|
|
|
|
|
while extra_input not in extra_choices:
|
|
|
|
|
if extra_input:
|
|
|
|
|
print(
|
|
|
|
|
f"{t('Error, cannot understand value')} '{extra_input}'"
|
|
|
|
|
)
|
|
|
|
|
str_extra = (
|
|
|
|
|
f"💬 {t('Install type:')}\n\t"
|
|
|
|
|
+ "\n\t".join([a[1] for a in extra_choices.values()])
|
|
|
|
|
+ f"\n{t('Select: ')}"
|
|
|
|
|
)
|
|
|
|
|
extra_input = input(str_extra).strip()
|
|
|
|
|
if extra_input == "0":
|
|
|
|
|
return
|
|
|
|
|
if extra_input == "2":
|
|
|
|
|
cmd_intern = cmd_intern + " --with_extra"
|
|
|
|
|
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Will execute:')}\n{cmd_intern}")
|
2025-08-07 06:09:37 -04:00
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
# TODO use external script to detect terminal to use on system
|
|
|
|
|
# TODO check script open_terminal_code_generator.sh
|
|
|
|
|
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(
|
|
|
|
|
cmd_intern, shell=True, executable="/bin/bash", check=True
|
|
|
|
|
)
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
f"{t('The Bash script failed with return code')} {e.returncode}."
|
|
|
|
|
)
|
2025-10-31 01:10:54 -04:00
|
|
|
print("Wait after installation and open projects by terminal.")
|
|
|
|
|
print("make open_terminal")
|
|
|
|
|
self.restart_script(str(e))
|
2025-08-07 06:09:37 -04:00
|
|
|
|
2025-04-28 00:32:51 -04:00
|
|
|
def execute_from_configuration(
|
2026-03-10 03:45:11 -04:00
|
|
|
self, instance, exec_run_db=False, ignore_makefile=False
|
2025-04-28 00:32:51 -04:00
|
|
|
):
|
|
|
|
|
# exec_run_db need argument database
|
2026-03-10 03:45:11 -04:00
|
|
|
kdbx_key = instance.get("kdbx_key")
|
|
|
|
|
odoo_user = instance.get("user")
|
|
|
|
|
odoo_password = instance.get("password")
|
2025-04-28 00:32:51 -04:00
|
|
|
|
|
|
|
|
if kdbx_key:
|
2026-03-10 04:05:04 -04:00
|
|
|
extra_cmd_web_login = self.kdbx_manager.get_extra_command_user(
|
|
|
|
|
kdbx_key
|
|
|
|
|
)
|
2025-04-28 00:32:51 -04:00
|
|
|
elif odoo_user and odoo_password:
|
|
|
|
|
extra_cmd_web_login = (
|
|
|
|
|
f" --default_email_auth {odoo_user} --default_password_auth"
|
|
|
|
|
f" '{odoo_password}'"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
extra_cmd_web_login = ""
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
makefile_cmd = instance.get("makefile_cmd")
|
2025-04-28 00:32:51 -04:00
|
|
|
if makefile_cmd and not ignore_makefile:
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
f"make {makefile_cmd}",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
2025-11-10 03:23:57 -05:00
|
|
|
if status:
|
|
|
|
|
_logger.error(
|
|
|
|
|
f"Status {status} - exit execute_from_configuration"
|
|
|
|
|
)
|
|
|
|
|
return
|
2025-04-28 00:32:51 -04:00
|
|
|
|
|
|
|
|
if exec_run_db:
|
2026-03-10 03:45:11 -04:00
|
|
|
db_name = instance.get("database")
|
2025-04-28 00:32:51 -04:00
|
|
|
self.prompt_execute_selenium_and_run_db(
|
|
|
|
|
db_name, extra_cmd_web_login=extra_cmd_web_login
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-12 04:04:52 -04:00
|
|
|
bash_command = instance.get("bash_command")
|
|
|
|
|
if bash_command:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Will execute:')} {bash_command}")
|
2026-03-13 15:04:14 -04:00
|
|
|
self.execute.exec_command_live(bash_command, source_erplibre=False)
|
2026-03-12 04:04:52 -04:00
|
|
|
|
2026-03-12 05:31:45 -04:00
|
|
|
command = instance.get("Command:")
|
2025-04-28 00:32:51 -04:00
|
|
|
if command:
|
|
|
|
|
self.prompt_execute_selenium(
|
|
|
|
|
command=command, extra_cmd_web_login=extra_cmd_web_login
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
callback = instance.get("callback")
|
2025-10-01 04:53:08 -04:00
|
|
|
if callback:
|
2026-03-10 03:45:11 -04:00
|
|
|
callback(instance)
|
2025-10-01 04:53:08 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
def fill_help_info(self, choices):
|
2026-03-12 05:31:45 -04:00
|
|
|
help_info = t("Command:") + "\n"
|
|
|
|
|
help_end = f"[0] {t('Back')}\n"
|
2026-03-10 03:45:11 -04:00
|
|
|
for i, instance in enumerate(choices):
|
|
|
|
|
desc_key = instance.get("prompt_description_key")
|
2026-03-07 02:59:07 -05:00
|
|
|
if desc_key:
|
|
|
|
|
desc = t(desc_key)
|
|
|
|
|
else:
|
2026-03-10 03:45:11 -04:00
|
|
|
desc = instance["prompt_description"]
|
2026-03-07 02:59:07 -05:00
|
|
|
help_info += f"[{i + 1}] " + desc + "\n"
|
2025-04-26 17:12:38 -04:00
|
|
|
help_info += help_end
|
2025-04-28 00:32:51 -04:00
|
|
|
return help_info
|
|
|
|
|
|
|
|
|
|
def prompt_execute_instance(self):
|
2025-05-04 01:02:12 -04:00
|
|
|
# TODO proposer le déploiement à distance
|
|
|
|
|
# TODO proposer l'exécution de docker
|
|
|
|
|
# TODO proposer la création de docker
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = self.config_file.get_config("instance")
|
|
|
|
|
init_len = len(choices)
|
2025-10-01 04:53:08 -04:00
|
|
|
|
2026-02-28 01:35:32 -05:00
|
|
|
# Support mobile ERPLibre
|
2025-12-27 05:23:48 -05:00
|
|
|
if os.path.exists(MOBILE_HOME_PATH):
|
2026-03-10 03:45:11 -04:00
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Mobile - Compile and run software"),
|
2025-10-01 04:53:08 -04:00
|
|
|
"callback": self.callback_make_mobile_home,
|
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.append(menu_entry)
|
2026-02-28 01:35:32 -05:00
|
|
|
|
|
|
|
|
# Support custom database to execute
|
2026-03-10 03:45:11 -04:00
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Choose your database"),
|
2026-02-28 01:35:32 -05:00
|
|
|
"callback": self.callback_execute_custom_database,
|
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.insert(0, menu_entry)
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
try:
|
|
|
|
|
int_cmd = int(status)
|
2026-02-28 01:35:32 -05:00
|
|
|
if 1 < int_cmd <= init_len:
|
2025-04-26 17:12:38 -04:00
|
|
|
cmd_no_found = False
|
2026-03-13 15:04:14 -04:00
|
|
|
status = click.confirm(
|
|
|
|
|
t("Do you want a new instance?")
|
|
|
|
|
)
|
2026-03-10 03:45:11 -04:00
|
|
|
instance = choices[int_cmd - 1]
|
2025-04-28 00:32:51 -04:00
|
|
|
self.execute_from_configuration(
|
2026-03-10 03:45:11 -04:00
|
|
|
instance,
|
2025-04-28 00:32:51 -04:00
|
|
|
exec_run_db=True,
|
|
|
|
|
ignore_makefile=not bool(status),
|
|
|
|
|
)
|
2026-03-10 03:45:11 -04:00
|
|
|
elif int_cmd <= len(choices) or 1 == int_cmd:
|
2026-02-28 01:35:32 -05:00
|
|
|
cmd_no_found = False
|
2025-10-01 04:53:08 -04:00
|
|
|
# Execute dynamic instance
|
2026-03-10 03:45:11 -04:00
|
|
|
instance = choices[int_cmd - 1]
|
2025-10-01 04:53:08 -04:00
|
|
|
self.execute_from_configuration(
|
2026-03-10 03:45:11 -04:00
|
|
|
instance,
|
2025-10-01 04:53:08 -04:00
|
|
|
)
|
2025-04-26 17:12:38 -04:00
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
def prompt_execute_function(self):
|
|
|
|
|
choices = self.config_file.get_config("function")
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
try:
|
|
|
|
|
int_cmd = int(status)
|
2026-03-10 03:45:11 -04:00
|
|
|
if 0 < int_cmd <= len(choices):
|
2025-04-26 17:12:38 -04:00
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
self.execute_from_configuration(instance)
|
2025-04-26 17:12:38 -04:00
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2025-05-04 01:02:12 -04:00
|
|
|
def prompt_execute_update(self):
|
2026-02-13 01:27:38 -05:00
|
|
|
# self.execute.exec_command_live(f"make {makefile_cmd}")
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Development update')}")
|
2025-05-04 01:02:12 -04:00
|
|
|
# TODO détecter les modules en modification pour faire la mise à jour en cours
|
|
|
|
|
# TODO demander sur quel BD faire la mise à jour
|
|
|
|
|
# TODO proposer les modules manuelles selon la configuration à mettre à jour
|
|
|
|
|
# TODO proposer la mise à jour de l'IDE
|
|
|
|
|
# TODO proposer la mise à jour des git-repo
|
2025-10-31 01:10:54 -04:00
|
|
|
# TODO faire la mise à jour de ERPLibre
|
|
|
|
|
# TODO faire l'upgrade d'un odoo vers un autre
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = self.config_file.get_config("update_from_makefile")
|
|
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Upgrade Odoo - Migration Database"),
|
2025-10-31 01:10:54 -04:00
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.append(menu_entry)
|
|
|
|
|
poetry_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Upgrade Poetry - Dependency of Odoo"),
|
2025-10-31 01:10:54 -04:00
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.append(poetry_entry)
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-05-04 01:02:12 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
2026-03-10 03:45:11 -04:00
|
|
|
elif status == str(len(choices) - 1):
|
2025-10-31 01:10:54 -04:00
|
|
|
upgrade = todo_upgrade.TodoUpgrade(self)
|
|
|
|
|
upgrade.execute_odoo_upgrade()
|
2026-03-10 03:45:11 -04:00
|
|
|
elif status == str(len(choices)):
|
2025-10-31 01:10:54 -04:00
|
|
|
self.upgrade_poetry()
|
2025-05-04 01:02:12 -04:00
|
|
|
else:
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
try:
|
2025-10-31 01:10:54 -04:00
|
|
|
int_cmd = int(status) - 1
|
2026-03-10 03:45:11 -04:00
|
|
|
if 0 < int_cmd <= len(choices):
|
2025-05-04 01:02:12 -04:00
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
self.execute_from_configuration(instance)
|
2025-05-04 01:02:12 -04:00
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-05-04 01:02:12 -04:00
|
|
|
|
2026-03-12 05:40:22 -04:00
|
|
|
def prompt_execute_deploy(self):
|
2026-03-13 15:04:14 -04:00
|
|
|
print(f"🤖 {t('Deploy ERPLibre to a local directory!')}")
|
2026-03-12 05:40:22 -04:00
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
{"prompt_description": t("Clone ERPLibre locally (git clone)")},
|
2026-03-22 22:28:24 -04:00
|
|
|
{"prompt_description": t("Configure sshfs")},
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
{"prompt_description": t("SSH - Check connection")},
|
|
|
|
|
{"prompt_description": t("SSH - Sync files (rsync)")},
|
|
|
|
|
{"prompt_description": t("SSH - Install ERPLibre")},
|
|
|
|
|
{"prompt_description": t("SSH - Start Odoo")},
|
|
|
|
|
{"prompt_description": t("SSH - Stop Odoo")},
|
|
|
|
|
{"prompt_description": t("SSH - Restart Odoo")},
|
|
|
|
|
{"prompt_description": t("SSH - Service status")},
|
|
|
|
|
{"prompt_description": t("SSH - View logs")},
|
|
|
|
|
{"prompt_description": t("SSH - Run make target")},
|
|
|
|
|
{"prompt_description": t("SSH - Install systemd service")},
|
|
|
|
|
{"prompt_description": t("SSH - Configure nginx + SSL")},
|
2026-04-11 00:13:56 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Deploy - Install NTFY notification server"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-07-15 07:48:12 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"QEMU/KVM - Deploy an Ubuntu VM (libvirt)"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-12 05:40:22 -04:00
|
|
|
]
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self._deploy_clone_erplibre()
|
2026-03-22 22:28:24 -04:00
|
|
|
elif status == "2":
|
|
|
|
|
self._configure_sshfs()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
elif status == "3":
|
|
|
|
|
self._deploy_ssh_check()
|
|
|
|
|
elif status == "4":
|
|
|
|
|
self._deploy_ssh_push()
|
|
|
|
|
elif status == "5":
|
|
|
|
|
self._deploy_ssh_install()
|
|
|
|
|
elif status == "6":
|
|
|
|
|
self._deploy_ssh_run()
|
|
|
|
|
elif status == "7":
|
|
|
|
|
self._deploy_ssh_stop()
|
|
|
|
|
elif status == "8":
|
|
|
|
|
self._deploy_ssh_restart()
|
|
|
|
|
elif status == "9":
|
|
|
|
|
self._deploy_ssh_status()
|
|
|
|
|
elif status == "10":
|
|
|
|
|
self._deploy_ssh_logs()
|
|
|
|
|
elif status == "11":
|
|
|
|
|
self._deploy_ssh_make()
|
|
|
|
|
elif status == "12":
|
|
|
|
|
self._deploy_ssh_install_systemd()
|
|
|
|
|
elif status == "13":
|
|
|
|
|
self._deploy_ssh_install_nginx()
|
2026-04-11 00:13:56 -04:00
|
|
|
elif status == "14":
|
|
|
|
|
self._deploy_ntfy_server()
|
2026-07-15 07:48:12 -04:00
|
|
|
elif status == "15":
|
|
|
|
|
self.prompt_execute_qemu()
|
2026-03-12 05:40:22 -04:00
|
|
|
else:
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
2026-07-15 07:48:12 -04:00
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# QEMU / KVM (libvirt) VM deployment
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def _qemu_script_path(self):
|
|
|
|
|
"""Chemin absolu vers script/qemu/deploy_qemu.py."""
|
|
|
|
|
path = os.path.join(
|
|
|
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
|
|
|
"..",
|
|
|
|
|
"qemu",
|
|
|
|
|
"deploy_qemu.py",
|
|
|
|
|
)
|
|
|
|
|
return os.path.realpath(path)
|
|
|
|
|
|
|
|
|
|
def _qemu_default_ssh_key(self):
|
|
|
|
|
"""Première clé publique SSH trouvée dans ~/.ssh, sinon ''."""
|
|
|
|
|
for name in ("id_ed25519.pub", "id_rsa.pub"):
|
|
|
|
|
path = os.path.expanduser(f"~/.ssh/{name}")
|
|
|
|
|
if os.path.exists(path):
|
|
|
|
|
return path
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def _qemu_prompt_version(self):
|
|
|
|
|
"""Demande la version Ubuntu ; retourne une chaîne (défaut 24.04)."""
|
|
|
|
|
versions = ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10"]
|
|
|
|
|
print(f"\n{t('Ubuntu version:')}")
|
|
|
|
|
for i, v in enumerate(versions, 1):
|
|
|
|
|
suffix = " (LTS)" if v in ("20.04", "22.04", "24.04") else ""
|
|
|
|
|
print(f" [{i}] {v}{suffix}")
|
|
|
|
|
sel = input(t("Choice (number or version, default: 24.04): ")).strip()
|
|
|
|
|
if not sel:
|
|
|
|
|
return "24.04"
|
|
|
|
|
try:
|
|
|
|
|
idx = int(sel) - 1
|
|
|
|
|
if 0 <= idx < len(versions):
|
|
|
|
|
return versions[idx]
|
|
|
|
|
except ValueError:
|
|
|
|
|
if sel in versions:
|
|
|
|
|
return sel
|
|
|
|
|
print(t("Invalid selection, using 24.04"))
|
|
|
|
|
return "24.04"
|
|
|
|
|
|
|
|
|
|
def prompt_execute_qemu(self):
|
|
|
|
|
print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}")
|
|
|
|
|
script_path = self._qemu_script_path()
|
|
|
|
|
if not os.path.isfile(script_path):
|
|
|
|
|
print(f"{t('QEMU deploy script not found: ')}{script_path}")
|
|
|
|
|
return False
|
|
|
|
|
choices = [
|
|
|
|
|
{"prompt_description": t("Deploy a new Ubuntu VM")},
|
|
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Preview a deployment (dry-run, no sudo)"
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{"prompt_description": t("Download an Ubuntu cloud image only")},
|
|
|
|
|
{"prompt_description": t("List VMs (virsh list --all)")},
|
|
|
|
|
{"prompt_description": t("Show a VM IP address")},
|
|
|
|
|
]
|
|
|
|
|
config_entries = self.config_file.get_config("qemu_from_makefile")
|
|
|
|
|
if config_entries:
|
|
|
|
|
choices.extend(config_entries)
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self._qemu_deploy_vm(dry_run=False)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self._qemu_deploy_vm(dry_run=True)
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self._qemu_download_image()
|
|
|
|
|
elif status == "4":
|
|
|
|
|
self._qemu_list_vms()
|
|
|
|
|
elif status == "5":
|
|
|
|
|
self._qemu_show_ip()
|
|
|
|
|
else:
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
try:
|
|
|
|
|
int_cmd = int(status)
|
|
|
|
|
if 0 < int_cmd <= len(choices):
|
|
|
|
|
cmd_no_found = False
|
|
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
self.execute_from_configuration(instance)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
if cmd_no_found:
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
|
|
|
def _qemu_deploy_vm(self, dry_run=False):
|
|
|
|
|
script_path = self._qemu_script_path()
|
|
|
|
|
name = input(t("VM name (required): ")).strip()
|
|
|
|
|
if not name:
|
|
|
|
|
print(t("VM name is required!"))
|
|
|
|
|
return
|
|
|
|
|
version = self._qemu_prompt_version()
|
|
|
|
|
memory = input(t("RAM in MB (default: 8192): ")).strip() or "8192"
|
|
|
|
|
vcpus = input(t("vCPUs (default: 4): ")).strip() or "4"
|
|
|
|
|
disk_size = input(t("Disk size (default: 20G): ")).strip() or "20G"
|
|
|
|
|
|
|
|
|
|
default_key = self._qemu_default_ssh_key()
|
|
|
|
|
key_hint = default_key or t("none")
|
|
|
|
|
ssh_key = input(f"{t('SSH public key path')} ({key_hint}): ").strip()
|
|
|
|
|
if not ssh_key:
|
|
|
|
|
ssh_key = default_key
|
|
|
|
|
if ssh_key:
|
|
|
|
|
# Résout ~ avec le HOME de l'utilisateur courant : le script tourne
|
|
|
|
|
# sous sudo (HOME=/root) et ne pourrait pas déduire ~ correctement.
|
|
|
|
|
ssh_key = os.path.expanduser(ssh_key)
|
|
|
|
|
|
|
|
|
|
use_password = False
|
|
|
|
|
if not ssh_key:
|
|
|
|
|
ans = (
|
|
|
|
|
input(t("No SSH key found. Set a password instead? (Y/n): "))
|
|
|
|
|
.strip()
|
|
|
|
|
.lower()
|
|
|
|
|
)
|
|
|
|
|
use_password = ans != "n"
|
|
|
|
|
|
|
|
|
|
force = False
|
|
|
|
|
if not dry_run:
|
|
|
|
|
ans = (
|
|
|
|
|
input(t("Overwrite existing VM disk if present? (y/N): "))
|
|
|
|
|
.strip()
|
|
|
|
|
.lower()
|
|
|
|
|
)
|
|
|
|
|
force = ans == "y"
|
|
|
|
|
|
|
|
|
|
parts = []
|
|
|
|
|
if not dry_run:
|
|
|
|
|
parts.append("sudo")
|
|
|
|
|
parts.append(script_path)
|
|
|
|
|
parts += ["--name", name, "--version", version]
|
|
|
|
|
parts += [
|
|
|
|
|
"--memory",
|
|
|
|
|
memory,
|
|
|
|
|
"--vcpus",
|
|
|
|
|
vcpus,
|
|
|
|
|
"--disk-size",
|
|
|
|
|
disk_size,
|
|
|
|
|
]
|
|
|
|
|
if ssh_key:
|
|
|
|
|
parts += ["--ssh-key", ssh_key]
|
|
|
|
|
if use_password:
|
|
|
|
|
parts.append("--ask-password")
|
|
|
|
|
if force:
|
|
|
|
|
parts.append("--force")
|
|
|
|
|
if dry_run:
|
|
|
|
|
parts.append("--dry-run")
|
|
|
|
|
else:
|
|
|
|
|
parts.append("-y") # accepte l'installation des dépendances
|
|
|
|
|
|
|
|
|
|
cmd = " ".join(shlex.quote(p) for p in parts)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
|
|
|
|
def _qemu_download_image(self):
|
|
|
|
|
script_path = self._qemu_script_path()
|
|
|
|
|
version = self._qemu_prompt_version()
|
|
|
|
|
ans = input(t("Verify SHA256 after download? (y/N): ")).strip().lower()
|
|
|
|
|
parts = [
|
|
|
|
|
"sudo",
|
|
|
|
|
script_path,
|
|
|
|
|
"--download-only",
|
|
|
|
|
"--version",
|
|
|
|
|
version,
|
|
|
|
|
]
|
|
|
|
|
if ans == "y":
|
|
|
|
|
parts.append("--verify")
|
|
|
|
|
cmd = " ".join(shlex.quote(p) for p in parts)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
|
|
|
|
def _qemu_list_vms(self):
|
|
|
|
|
cmd = "sudo virsh list --all"
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
|
|
|
|
def _qemu_show_ip(self):
|
|
|
|
|
name = input(t("VM name: ")).strip()
|
|
|
|
|
if not name:
|
|
|
|
|
print(t("VM name is required!"))
|
|
|
|
|
return
|
|
|
|
|
cmd = f"sudo virsh domifaddr {shlex.quote(name)} --source lease"
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
2026-03-12 05:40:22 -04:00
|
|
|
def _deploy_clone_erplibre(self):
|
|
|
|
|
default_path = os.path.expanduser("~/erplibre")
|
|
|
|
|
target_path = (
|
2026-03-13 15:04:14 -04:00
|
|
|
input(t("Target directory path (default: ~/erplibre): ")).strip()
|
2026-03-12 05:40:22 -04:00
|
|
|
or default_path
|
|
|
|
|
)
|
|
|
|
|
target_path = os.path.expanduser(target_path)
|
|
|
|
|
if os.path.exists(target_path):
|
2026-03-13 15:04:14 -04:00
|
|
|
print(f"{t('Directory already exists: ')}{target_path}")
|
2026-03-12 05:40:22 -04:00
|
|
|
return
|
|
|
|
|
print(t("Cloning ERPLibre..."))
|
|
|
|
|
cmd = (
|
|
|
|
|
"git clone"
|
|
|
|
|
" https://github.com/erplibre/erplibre"
|
|
|
|
|
f" {target_path}"
|
|
|
|
|
)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
try:
|
2026-03-13 15:04:14 -04:00
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
print(f"{t('ERPLibre cloned successfully to: ')}" f"{target_path}")
|
2026-03-12 05:40:22 -04:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"{t('Error cloning ERPLibre: ')}{e}")
|
|
|
|
|
|
2026-04-11 00:13:56 -04:00
|
|
|
def _deploy_ntfy_server(self):
|
|
|
|
|
print(
|
|
|
|
|
f"\n{t('Deploy a local NTFY push notification server (Ubuntu/Arch)')}"
|
|
|
|
|
)
|
2026-07-15 07:48:12 -04:00
|
|
|
port = input(t("NTFY server port (default: 8080): ")).strip() or "8080"
|
2026-04-11 00:13:56 -04:00
|
|
|
import socket
|
|
|
|
|
|
|
|
|
|
hostname = socket.gethostname()
|
|
|
|
|
try:
|
|
|
|
|
local_ip = socket.gethostbyname(hostname)
|
|
|
|
|
except Exception:
|
|
|
|
|
local_ip = "127.0.0.1"
|
|
|
|
|
|
|
|
|
|
default_url = f"http://{local_ip}:{port}"
|
|
|
|
|
base_url = (
|
2026-07-15 07:48:12 -04:00
|
|
|
input(f"{t('NTFY base URL')} (default: {default_url}): ").strip()
|
2026-04-11 00:13:56 -04:00
|
|
|
or default_url
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
script_path = os.path.join(
|
|
|
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
|
|
|
"..",
|
|
|
|
|
"install",
|
|
|
|
|
"install_ntfy.sh",
|
|
|
|
|
)
|
|
|
|
|
script_path = os.path.realpath(script_path)
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(script_path):
|
|
|
|
|
print(f"{t('NTFY install script not found: ')}{script_path}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"\n{t('Installing NTFY server (requires sudo)...')}")
|
|
|
|
|
cmd = (
|
|
|
|
|
f"sudo NTFY_PORT={port}"
|
|
|
|
|
f" NTFY_BASE_URL={base_url}"
|
|
|
|
|
f" bash {script_path}"
|
|
|
|
|
)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}\n")
|
|
|
|
|
try:
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
print(f"\n{t('NTFY server installed and started successfully!')}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"{t('Error installing NTFY server: ')}{e}")
|
|
|
|
|
|
2026-03-22 22:28:24 -04:00
|
|
|
def _configure_sshfs(self):
|
|
|
|
|
import getpass
|
|
|
|
|
import re
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
print(f"\n{t('SSH address input method')}")
|
|
|
|
|
print(f"[1] {t('Manual entry')}")
|
|
|
|
|
print(f"[2] {t('From ~/.ssh/config')}")
|
|
|
|
|
choice = input(t("Your choice (1/2): ")).strip()
|
|
|
|
|
|
|
|
|
|
user = None
|
|
|
|
|
hostname = None
|
|
|
|
|
ssh_name = None
|
|
|
|
|
|
|
|
|
|
if choice == "2":
|
|
|
|
|
ssh_config_path = os.path.expanduser("~/.ssh/config")
|
|
|
|
|
hosts = []
|
|
|
|
|
if os.path.exists(ssh_config_path):
|
|
|
|
|
current_host = None
|
|
|
|
|
current_info = {}
|
|
|
|
|
with open(ssh_config_path) as f:
|
|
|
|
|
for line in f:
|
|
|
|
|
line = line.strip()
|
|
|
|
|
if line.lower().startswith("host "):
|
|
|
|
|
host_val = line.split(None, 1)[1].strip()
|
|
|
|
|
if host_val != "*":
|
|
|
|
|
if current_host:
|
|
|
|
|
hosts.append((current_host, current_info))
|
|
|
|
|
current_host = host_val
|
|
|
|
|
current_info = {}
|
|
|
|
|
elif current_host:
|
|
|
|
|
key = line.split(None, 1)
|
|
|
|
|
if len(key) == 2:
|
|
|
|
|
k = key[0].lower()
|
|
|
|
|
v = key[1].strip()
|
|
|
|
|
if k == "hostname":
|
|
|
|
|
current_info["hostname"] = v
|
|
|
|
|
elif k == "user":
|
|
|
|
|
current_info["user"] = v
|
|
|
|
|
if current_host:
|
|
|
|
|
hosts.append((current_host, current_info))
|
|
|
|
|
|
|
|
|
|
if not hosts:
|
|
|
|
|
print(t("No SSH hosts found in ~/.ssh/config"))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
for i, (host, info) in enumerate(hosts, 1):
|
|
|
|
|
hn = info.get("hostname", host)
|
|
|
|
|
u = info.get("user", "")
|
|
|
|
|
desc = host
|
|
|
|
|
if hn != host:
|
|
|
|
|
desc += f" ({hn})"
|
|
|
|
|
if u:
|
|
|
|
|
desc += f" [{u}]"
|
|
|
|
|
print(f"[{i}] {desc}")
|
|
|
|
|
|
|
|
|
|
sel = input(t("Select SSH host number: ")).strip()
|
|
|
|
|
try:
|
|
|
|
|
idx = int(sel) - 1
|
|
|
|
|
if idx < 0 or idx >= len(hosts):
|
|
|
|
|
print(t("Invalid selection!"))
|
|
|
|
|
return
|
|
|
|
|
except ValueError:
|
|
|
|
|
print(t("Invalid selection!"))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
host_name, host_info = hosts[idx]
|
|
|
|
|
hostname = host_info.get("hostname", host_name)
|
|
|
|
|
user = host_info.get("user", getpass.getuser())
|
|
|
|
|
ssh_name = host_name
|
|
|
|
|
target = f"{host_name}:/"
|
|
|
|
|
else:
|
|
|
|
|
ssh_host = input(
|
|
|
|
|
t("SSH host (e.g.: user@192.168.1.100): ")
|
|
|
|
|
).strip()
|
|
|
|
|
if not ssh_host:
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
return
|
|
|
|
|
if "@" in ssh_host:
|
|
|
|
|
user, hostname = ssh_host.split("@", 1)
|
|
|
|
|
else:
|
|
|
|
|
hostname = ssh_host
|
|
|
|
|
user = getpass.getuser()
|
|
|
|
|
ssh_name = hostname
|
|
|
|
|
target = f"{user}@{hostname}:/"
|
|
|
|
|
|
|
|
|
|
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", ssh_name)
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
mount_point = f"/tmp/sshfs_{safe_name}_{timestamp}"
|
|
|
|
|
os.makedirs(mount_point, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
cmd = f"sshfs {target} {mount_point}"
|
|
|
|
|
print(f"{t('Mounting sshfs on: ')}{mount_point}")
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
try:
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
print(f"{t('Mounted on: ')}{mount_point}")
|
|
|
|
|
print(f"mount | grep sshfs")
|
|
|
|
|
print(f"{t('To unmount: ')}" f"fusermount -u {mount_point}")
|
|
|
|
|
print(f"nautilus {mount_point}/home/{user}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"{t('Error mounting sshfs: ')}{e}")
|
|
|
|
|
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
def _get_ssh_params(self):
|
|
|
|
|
"""Prompt for SSH connection parameters. Returns dict or None on cancel."""
|
2026-07-15 07:48:12 -04:00
|
|
|
host = click.prompt(
|
|
|
|
|
t("Remote host (user@hostname or hostname): ")
|
|
|
|
|
).strip()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
if not host:
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
return None
|
|
|
|
|
user = (
|
|
|
|
|
click.prompt(t("SSH user (default: erplibre): ")).strip()
|
|
|
|
|
or "erplibre"
|
|
|
|
|
)
|
|
|
|
|
port = click.prompt(t("SSH port (default: 22): ")).strip() or "22"
|
2026-07-15 07:48:12 -04:00
|
|
|
key = click.prompt(
|
|
|
|
|
t("SSH key path (default: ~/.ssh/id_rsa, empty for none): ")
|
|
|
|
|
).strip()
|
|
|
|
|
path = (
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
click.prompt(
|
2026-07-15 07:48:12 -04:00
|
|
|
t("Remote path (default: ~/erplibre_deploy_2): ")
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
).strip()
|
|
|
|
|
or "~/erplibre_deploy_2"
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"SSH_HOST": host,
|
|
|
|
|
"SSH_USER": user,
|
|
|
|
|
"SSH_PORT": port,
|
|
|
|
|
"SSH_KEY": key,
|
|
|
|
|
"SSH_PATH": path,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _build_ssh_make_cmd(self, target, params, extra=None):
|
|
|
|
|
"""Build a make SSH command string from params dict."""
|
|
|
|
|
parts = [f"make {target}"]
|
|
|
|
|
for k, v in params.items():
|
|
|
|
|
if v:
|
|
|
|
|
parts.append(f'{k}="{v}"')
|
|
|
|
|
if extra:
|
|
|
|
|
for k, v in extra.items():
|
|
|
|
|
if v:
|
|
|
|
|
parts.append(f'{k}="{v}"')
|
|
|
|
|
return " ".join(parts)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_check(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_check", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_push(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_push", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_install(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_install", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_run(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_run", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_stop(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_stop", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_restart(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_restart", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_status(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_status", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_logs(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_logs", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_make(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
target = click.prompt(t("Make target to run remotely: ")).strip()
|
|
|
|
|
if not target:
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd(
|
|
|
|
|
"ssh_make", params, extra={"SSH_TARGET": target}
|
|
|
|
|
)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_install_systemd(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_install_systemd", params)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_install_nginx(self):
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
if not params:
|
|
|
|
|
return
|
|
|
|
|
domain = click.prompt(t("Domain name (e.g.: example.com): ")).strip()
|
|
|
|
|
if not domain:
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
return
|
|
|
|
|
email = click.prompt(t("Admin email for SSL certificate: ")).strip()
|
|
|
|
|
cmd = self._build_ssh_make_cmd(
|
|
|
|
|
"ssh_install_nginx",
|
|
|
|
|
params,
|
|
|
|
|
extra={"SSH_DOMAIN": domain, "SSH_ADMIN_EMAIL": email},
|
|
|
|
|
)
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-04 01:02:12 -04:00
|
|
|
def prompt_execute_code(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('What do you need for development?')}")
|
2025-05-04 01:02:12 -04:00
|
|
|
# help_info = """Commande :
|
|
|
|
|
# [1] Status Git local et distant
|
|
|
|
|
# [2] Démarrer le générateur de code
|
|
|
|
|
# [3] Format - Formatage automatique selon changement [ou manuelle]
|
|
|
|
|
# [4] Qualité - Qualité logiciel, détecter les fichiers qui manquent les licences AGPLv3
|
|
|
|
|
# [0] Retour
|
|
|
|
|
# """
|
|
|
|
|
# help_info = """Commande :
|
|
|
|
|
# [1] Status Git local et distant
|
|
|
|
|
# [0] Retour
|
|
|
|
|
# """
|
2025-10-31 01:10:54 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = self.config_file.get_config("code_from_makefile")
|
2026-02-13 04:07:02 -05:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Open SHELL"),
|
2026-02-13 04:07:02 -05:00
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.append(menu_entry)
|
2026-02-13 04:07:02 -05:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Upgrade Module"),
|
2025-10-31 01:10:54 -04:00
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.append(menu_entry)
|
2026-02-13 04:07:02 -05:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
choices.append(
|
2026-02-28 02:08:04 -05:00
|
|
|
{
|
2026-03-12 05:31:45 -04:00
|
|
|
"prompt_description": t("Debug"),
|
2026-02-28 02:08:04 -05:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2025-05-04 01:02:12 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
2026-03-10 03:45:11 -04:00
|
|
|
elif status == str(len(choices)):
|
2026-02-28 02:08:04 -05:00
|
|
|
self.debug_ide()
|
2026-03-10 03:45:11 -04:00
|
|
|
elif status == str(len(choices) - 1):
|
2026-02-28 02:08:04 -05:00
|
|
|
self.upgrade_module()
|
2026-03-10 03:45:11 -04:00
|
|
|
elif status == str(len(choices) - 2):
|
2026-02-13 04:07:02 -05:00
|
|
|
self.open_shell_on_database()
|
2025-05-04 01:02:12 -04:00
|
|
|
else:
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
try:
|
|
|
|
|
int_cmd = int(status)
|
2026-03-10 03:45:11 -04:00
|
|
|
if 0 < int_cmd <= len(choices):
|
2025-05-04 01:02:12 -04:00
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
self.execute_from_configuration(instance)
|
2025-05-04 01:02:12 -04:00
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-10-31 01:10:54 -04:00
|
|
|
|
2026-03-09 16:34:43 -04:00
|
|
|
def prompt_execute_git(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Git management tools!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Local git server")},
|
|
|
|
|
{"prompt_description": t("Add a remote to a local repository")},
|
2026-03-09 16:34:43 -04:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Append config-driven entries
|
2026-03-10 03:45:11 -04:00
|
|
|
config_entries = self.config_file.get_config("git_from_makefile")
|
|
|
|
|
if config_entries:
|
|
|
|
|
choices.extend(config_entries)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.prompt_execute_git_local_server()
|
2026-03-12 04:04:52 -04:00
|
|
|
elif status == "2":
|
|
|
|
|
self._git_add_remote()
|
2026-03-09 16:34:43 -04:00
|
|
|
else:
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
try:
|
|
|
|
|
int_cmd = int(status)
|
2026-03-10 03:45:11 -04:00
|
|
|
if 0 < int_cmd <= len(choices):
|
2026-03-09 16:34:43 -04:00
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
self.execute_from_configuration(instance)
|
2026-03-09 16:34:43 -04:00
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
2026-03-12 04:04:52 -04:00
|
|
|
def _git_add_remote(self):
|
|
|
|
|
remote_name = (
|
2026-03-13 15:04:14 -04:00
|
|
|
input(t("Remote name (default: localhost): ")).strip()
|
|
|
|
|
or "localhost"
|
2026-03-12 04:04:52 -04:00
|
|
|
)
|
2026-03-13 15:04:14 -04:00
|
|
|
remote_url = input(
|
|
|
|
|
t("Repository address (e.g.: git://192.168.1.100/my-repo.git): ")
|
|
|
|
|
).strip()
|
2026-03-12 04:04:52 -04:00
|
|
|
if not remote_url:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Repository address is required!"))
|
2026-03-12 04:04:52 -04:00
|
|
|
return
|
|
|
|
|
cmd = f"git remote add {remote_name} {remote_url}"
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
2026-03-12 04:04:52 -04:00
|
|
|
try:
|
2026-03-13 15:04:14 -04:00
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Remote added successfully!"))
|
2026-03-12 04:04:52 -04:00
|
|
|
except Exception as e:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Error adding remote: ')}{e}")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
2026-03-09 16:34:43 -04:00
|
|
|
def prompt_execute_git_local_server(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Manage local git repository server!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Deploy a local git server (~/.git-server)"
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Deploy a production git server (/srv/git, root required)"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-09 16:34:43 -04:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self._prompt_git_server_actions(production_ready=False)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self._prompt_git_server_actions(production_ready=True)
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
def _prompt_git_server_actions(self, production_ready=False):
|
|
|
|
|
mode = (
|
2026-03-12 05:31:45 -04:00
|
|
|
t("Production mode (/srv/git, root required)")
|
2026-03-09 16:34:43 -04:00
|
|
|
if production_ready
|
2026-03-12 05:31:45 -04:00
|
|
|
else t("Local mode (~/.git-server)")
|
2026-03-09 16:34:43 -04:00
|
|
|
)
|
|
|
|
|
print(f"🤖 {mode}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Run all (init + remote + push + serve)"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Init - Create bare repos")},
|
|
|
|
|
{"prompt_description": t("Remote - Add local remotes")},
|
|
|
|
|
{"prompt_description": t("Push - Push to local server")},
|
|
|
|
|
{"prompt_description": t("Serve - Start git daemon")},
|
2026-03-09 16:34:43 -04:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
action="all",
|
|
|
|
|
)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
action="init",
|
|
|
|
|
)
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
action="remote",
|
|
|
|
|
)
|
|
|
|
|
elif status == "4":
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
action="push",
|
|
|
|
|
)
|
|
|
|
|
elif status == "5":
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
action="serve",
|
|
|
|
|
)
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
def _deploy_git_server(self, production_ready=False, action="all"):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Starting git server deployment..."))
|
2026-03-09 16:34:43 -04:00
|
|
|
cmd = (
|
|
|
|
|
"python3 ./script/git/git_local_server.py -v" f" --action {action}"
|
|
|
|
|
)
|
|
|
|
|
if production_ready:
|
|
|
|
|
cmd += " --production-ready"
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 02:05:53 -04:00
|
|
|
def prompt_execute_gpt_code(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('AI assistant tools for development!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Configure Claude Code configurations")},
|
2026-03-13 15:04:14 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Add an automation with Claude in todo.py"
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"RTK - CLI proxy to reduce LLM token consumption"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-10 02:05:53 -04:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
2026-03-12 04:37:19 -04:00
|
|
|
self._prompt_claude_configs()
|
2026-03-12 02:51:01 -04:00
|
|
|
elif status == "2":
|
2026-03-12 04:04:52 -04:00
|
|
|
self._claude_add_automation()
|
|
|
|
|
elif status == "3":
|
2026-03-12 02:51:01 -04:00
|
|
|
self.prompt_execute_rtk()
|
2026-03-10 02:05:53 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-10 02:05:53 -04:00
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
def _prompt_claude_configs(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Deploy Claude Code commands!')}")
|
2026-03-12 04:37:19 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Commit - OCA/Odoo commit command")},
|
2026-03-12 04:37:19 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
2026-03-12 05:31:45 -04:00
|
|
|
"Todo Add Command - Add a command to todo.py menu"
|
2026-03-12 04:37:19 -04:00
|
|
|
)
|
|
|
|
|
},
|
2026-03-13 15:04:14 -04:00
|
|
|
{"prompt_description": t("Show installed custom commands")},
|
2026-03-12 04:37:19 -04:00
|
|
|
]
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-10 02:05:53 -04:00
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
"commit",
|
|
|
|
|
"template_claude_commands_commit.md",
|
|
|
|
|
personalize=True,
|
|
|
|
|
)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
"todo_add_command",
|
|
|
|
|
"template_claude_commands_todo_add_command.md",
|
|
|
|
|
)
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self._list_claude_commands()
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-12 04:37:19 -04:00
|
|
|
|
|
|
|
|
def _list_claude_commands(self):
|
|
|
|
|
commands_dir = os.path.expanduser("~/.claude/commands")
|
|
|
|
|
if not os.path.isdir(commands_dir):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("No custom commands found in ~/.claude/commands/"))
|
2026-03-10 02:05:53 -04:00
|
|
|
return
|
2026-03-12 04:37:19 -04:00
|
|
|
files = sorted(
|
2026-03-13 15:04:14 -04:00
|
|
|
f for f in os.listdir(commands_dir) if f.endswith(".md")
|
2026-03-12 04:37:19 -04:00
|
|
|
)
|
|
|
|
|
if not files:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("No custom commands found in ~/.claude/commands/"))
|
2026-03-12 04:37:19 -04:00
|
|
|
return
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Claude Code custom commands:"))
|
2026-03-12 04:37:19 -04:00
|
|
|
print("-" * 50)
|
|
|
|
|
for f in files:
|
|
|
|
|
filepath = os.path.join(commands_dir, f)
|
|
|
|
|
mtime = os.path.getmtime(filepath)
|
|
|
|
|
date_str = datetime.datetime.fromtimestamp(mtime).strftime(
|
|
|
|
|
"%Y-%m-%d %H:%M"
|
|
|
|
|
)
|
|
|
|
|
name = f[:-3] # remove .md
|
|
|
|
|
print(f" /{name:<30} {date_str}")
|
|
|
|
|
print("-" * 50)
|
2026-03-13 15:04:14 -04:00
|
|
|
print(f"{t('Total:')}" f" {len(files)}")
|
2026-03-10 02:05:53 -04:00
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
def _setup_claude_command(
|
|
|
|
|
self, command_name, template_filename, personalize=False
|
|
|
|
|
):
|
|
|
|
|
dest_dir = os.path.expanduser("~/.claude/commands")
|
|
|
|
|
dest_file = os.path.join(dest_dir, f"{command_name}.md")
|
|
|
|
|
|
|
|
|
|
if os.path.exists(dest_file):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('File already exists: ')}{dest_file}")
|
2026-03-12 04:37:19 -04:00
|
|
|
overwrite = input(
|
2026-03-12 05:31:45 -04:00
|
|
|
t("Do you want to overwrite the file? (y/Y): ")
|
2026-03-12 04:37:19 -04:00
|
|
|
).strip()
|
|
|
|
|
if overwrite not in ("y", "Y"):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Nothing to do."))
|
2026-03-12 04:37:19 -04:00
|
|
|
return
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
|
|
|
template_path = os.path.join(
|
|
|
|
|
os.path.dirname(__file__),
|
|
|
|
|
"..",
|
|
|
|
|
"..",
|
|
|
|
|
"conf",
|
2026-03-12 04:37:19 -04:00
|
|
|
template_filename,
|
2026-03-10 02:05:53 -04:00
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with open(template_path) as f:
|
|
|
|
|
content = f.read()
|
|
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
if personalize:
|
2026-03-12 05:31:45 -04:00
|
|
|
name = input(t("Enter your full name: ")).strip()
|
|
|
|
|
email = input(t("Enter your email: ")).strip()
|
2026-03-12 04:37:19 -04:00
|
|
|
content = content.replace(
|
|
|
|
|
"Your Name <your@email.com>",
|
|
|
|
|
f"{name} <{email}>",
|
|
|
|
|
)
|
|
|
|
|
content = content.replace(
|
|
|
|
|
"Your Name ",
|
|
|
|
|
f"{name} ",
|
|
|
|
|
)
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
|
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
|
|
|
with open(dest_file, "w") as f:
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('File created successfully: ')}{dest_file}")
|
2026-03-10 02:05:53 -04:00
|
|
|
except Exception as e:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Error creating file: ')}{e}")
|
2026-03-10 02:05:53 -04:00
|
|
|
|
2026-03-12 04:04:52 -04:00
|
|
|
def _claude_add_automation(self):
|
2026-03-13 15:04:14 -04:00
|
|
|
description = input(t("Description of the command to add: ")).strip()
|
2026-03-12 04:04:52 -04:00
|
|
|
if not description:
|
|
|
|
|
return
|
2026-03-13 15:04:14 -04:00
|
|
|
command = input(t("Bash command to execute: ")).strip()
|
2026-03-12 04:04:52 -04:00
|
|
|
if not command:
|
|
|
|
|
return
|
|
|
|
|
section = (
|
|
|
|
|
input(
|
2026-03-12 05:31:45 -04:00
|
|
|
t("Menu section (git/code/config/network/process): ")
|
2026-03-12 04:04:52 -04:00
|
|
|
).strip()
|
|
|
|
|
or "git"
|
|
|
|
|
)
|
|
|
|
|
section_key = f"{section}_from_makefile"
|
2026-03-13 15:04:14 -04:00
|
|
|
config_path = os.path.join(os.path.dirname(__file__), "todo.json")
|
2026-03-12 04:04:52 -04:00
|
|
|
try:
|
|
|
|
|
with open(config_path) as f:
|
|
|
|
|
config = json.load(f)
|
|
|
|
|
if section_key not in config:
|
|
|
|
|
config[section_key] = []
|
|
|
|
|
config[section_key].append(
|
|
|
|
|
{
|
|
|
|
|
"prompt_description": description,
|
|
|
|
|
"bash_command": command,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
with open(config_path, "w") as f:
|
|
|
|
|
json.dump(config, f, indent=4, ensure_ascii=False)
|
|
|
|
|
f.write("\n")
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Automation added successfully in todo.json!"))
|
2026-03-12 04:04:52 -04:00
|
|
|
except Exception as e:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Error adding automation: ')}{e}")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
def prompt_execute_doc(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Looking for documentation?')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Migration module coverage")},
|
|
|
|
|
{"prompt_description": t("What change between version")},
|
|
|
|
|
{"prompt_description": t("OCA guidelines")},
|
|
|
|
|
{"prompt_description": t("OCA migration Odoo 19 milestone")},
|
2025-10-31 01:10:54 -04:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
str_version = input(
|
|
|
|
|
"Select version to upgrade Odoo CE (5-17) : "
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
int_version = int(str_version)
|
|
|
|
|
print(
|
|
|
|
|
"https://oca.github.io/OpenUpgrade/coverage_analysis/modules"
|
|
|
|
|
f"{int_version * 10}-{(int_version + 1) * 10}.html"
|
|
|
|
|
)
|
|
|
|
|
except ValueError:
|
|
|
|
|
print(
|
|
|
|
|
"https://oca.github.io/OpenUpgrade/030_coverage_analysis.html"
|
|
|
|
|
)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
str_version = input(
|
|
|
|
|
"Select version to show what change for Odoo CE version 8-18) : "
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
int_version = int(str_version)
|
|
|
|
|
print(
|
|
|
|
|
f"https://github.com/OCA/maintainer-tools/wiki/Migration-to-version-{int_version}.0"
|
|
|
|
|
)
|
|
|
|
|
except ValueError:
|
|
|
|
|
print("https://github.com/OCA/maintainer-tools/wiki")
|
|
|
|
|
elif status == "3":
|
|
|
|
|
print(
|
|
|
|
|
"https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/CONTRIBUTING.rst"
|
|
|
|
|
)
|
|
|
|
|
elif status == "4":
|
|
|
|
|
print("https://github.com/OCA/maintainer-tools/issues/658")
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
def prompt_execute_database(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Make changes to databases!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"Download database to create backup (.zip)"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Restore from backup (.zip)")},
|
|
|
|
|
{"prompt_description": t("Create backup (.zip)")},
|
2026-06-25 02:25:40 -04:00
|
|
|
{"prompt_description": t("Erase a database")},
|
2025-10-31 01:10:54 -04:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
2026-03-10 03:59:35 -04:00
|
|
|
self.db_manager.download_database_backup_cli()
|
2025-10-31 01:10:54 -04:00
|
|
|
elif status == "2":
|
2026-03-10 03:59:35 -04:00
|
|
|
self.db_manager.restore_from_database()
|
2026-03-02 16:06:30 -05:00
|
|
|
elif status == "3":
|
2026-03-10 03:59:35 -04:00
|
|
|
self.db_manager.create_backup_from_database()
|
2026-06-25 02:25:40 -04:00
|
|
|
elif status == "4":
|
|
|
|
|
self.db_manager.drop_database()
|
2025-10-31 01:10:54 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-10-31 01:10:54 -04:00
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
def prompt_execute_process(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Manage execution processes!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Kill Odoo process from actual port")},
|
|
|
|
|
{"prompt_description": t("Kill git daemon server process")},
|
2025-12-22 04:02:44 -05:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.process_kill_from_port()
|
2026-03-09 16:34:43 -04:00
|
|
|
elif status == "2":
|
|
|
|
|
self.process_kill_git_daemon()
|
2025-12-22 04:02:44 -05:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2025-12-22 04:02:44 -05:00
|
|
|
|
2026-03-09 16:34:43 -04:00
|
|
|
def process_kill_git_daemon(self):
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"pkill -f 'git daemon'",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Git daemon process killed."))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
2026-03-12 02:51:01 -04:00
|
|
|
def prompt_execute_rtk(self):
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}"
|
|
|
|
|
)
|
2026-03-12 02:51:01 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Install RTK")},
|
|
|
|
|
{"prompt_description": t("Check RTK version")},
|
|
|
|
|
{"prompt_description": t("Show cumulative token savings")},
|
|
|
|
|
{"prompt_description": t("Discover optimization opportunities")},
|
|
|
|
|
{"prompt_description": t("Initialize global auto-rewrite hook")},
|
|
|
|
|
{"prompt_description": t("Check RTK status")},
|
2026-03-12 02:51:01 -04:00
|
|
|
]
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.rtk_install()
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self.rtk_check_version()
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self.rtk_show_gain()
|
|
|
|
|
elif status == "4":
|
|
|
|
|
self.rtk_discover()
|
|
|
|
|
elif status == "5":
|
|
|
|
|
self.rtk_init_global()
|
|
|
|
|
elif status == "6":
|
|
|
|
|
self.rtk_check_status()
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
def rtk_install(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Installation method:')}")
|
2026-03-12 02:51:01 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("curl - Automatic install script")},
|
|
|
|
|
{"prompt_description": t("brew - Homebrew (macOS/Linux)")},
|
2026-03-13 15:04:14 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"cargo - Build from source (Rust required)"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-12 02:51:01 -04:00
|
|
|
]
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"brew install rtk",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"cargo install --git https://github.com/rtk-ai/rtk",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
def rtk_check_version(self):
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"rtk --version",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def rtk_show_gain(self):
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"rtk gain",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def rtk_discover(self):
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"rtk discover",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def rtk_init_global(self):
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
"rtk init --global",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def rtk_check_status(self):
|
|
|
|
|
rtk_path = shutil.which("rtk")
|
|
|
|
|
if rtk_path is None:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("RTK is not installed. Use option 1 to install it."))
|
2026-03-12 02:51:01 -04:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
result = self.execute.exec_command_live(
|
|
|
|
|
"rtk --version",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
quiet=True,
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
)
|
|
|
|
|
if isinstance(result, tuple) and result[0] == 0:
|
|
|
|
|
version_output = " ".join(result[1]).strip()
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('RTK is installed, version: ')}{version_output}")
|
2026-03-12 02:51:01 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('RTK is installed, version: ')}?")
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
config_path = os.path.expanduser("~/.config/rtk/config.toml")
|
|
|
|
|
if os.path.exists(config_path):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Global auto-rewrite hook: active"))
|
2026-03-12 02:51:01 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Global auto-rewrite hook: inactive"))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
2026-02-13 03:46:27 -05:00
|
|
|
def prompt_execute_config(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Manage ERPLibre and Odoo configuration!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Generate all configuration")},
|
|
|
|
|
{"prompt_description": t("Generate from pre-configuration")},
|
|
|
|
|
{"prompt_description": t("Generate from backup file")},
|
|
|
|
|
{"prompt_description": t("Generate from database")},
|
|
|
|
|
{"prompt_description": t("Setup queue job for parallelism")},
|
2026-02-13 03:46:27 -05:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-13 03:46:27 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.generate_config()
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self.generate_config_from_preconfiguration()
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self.generate_config_from_backup()
|
|
|
|
|
elif status == "4":
|
|
|
|
|
self.generate_config_from_database()
|
2026-02-28 02:14:38 -05:00
|
|
|
elif status == "5":
|
|
|
|
|
self.generate_config_queue_job()
|
2026-02-13 03:46:27 -05:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-02-13 03:46:27 -05:00
|
|
|
|
2026-02-28 03:24:22 -05:00
|
|
|
def prompt_execute_network(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Network tools!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("SSH port-forwarding")},
|
2026-03-07 02:59:07 -05:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
2026-03-12 05:31:45 -04:00
|
|
|
"Network performance request per second"
|
2026-03-07 02:59:07 -05:00
|
|
|
)
|
|
|
|
|
},
|
2026-02-28 03:24:22 -05:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-28 03:24:22 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.generate_network_port_forwarding()
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self.generate_network_performance_test()
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-02-28 03:24:22 -05:00
|
|
|
|
|
|
|
|
def generate_network_port_forwarding(self, add_arg=None):
|
|
|
|
|
# ssh -L local_port:localhost:remote_port SSH_connection
|
|
|
|
|
ssh_connection = click.prompt(
|
|
|
|
|
"SSH connection, check ~/.ssh/config or user@address"
|
|
|
|
|
)
|
|
|
|
|
local_port = click.prompt("local port (8069)")
|
|
|
|
|
remote_port = click.prompt("remote port (8069)")
|
|
|
|
|
cmd = f"ssh -L {local_port}:localhost:{remote_port} {ssh_connection}"
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_network_performance_test(self, add_arg=None):
|
|
|
|
|
# ./script/performance/test_performance.sh
|
|
|
|
|
address = click.prompt("https address, like https://erplibre.com")
|
|
|
|
|
cmd = f"./script/performance/test_performance.sh {address}"
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-07 01:38:55 -05:00
|
|
|
def prompt_execute_security(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Dependency security audit!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"pip-audit - Check vulnerabilities on Python environments"
|
|
|
|
|
)
|
|
|
|
|
},
|
2026-03-07 01:38:55 -05:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-07 01:38:55 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.execute_pip_audit()
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-07 01:38:55 -05:00
|
|
|
|
2026-03-08 15:53:51 -04:00
|
|
|
def prompt_execute_test(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Test an Odoo module on a temporary database!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Test a module")},
|
|
|
|
|
{"prompt_description": t("Test a module with code coverage")},
|
|
|
|
|
{"prompt_description": t("ERPLibre unit tests")},
|
2026-03-08 15:53:51 -04:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-08 15:53:51 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.execute_test_module(coverage=False)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self.execute_test_module(coverage=True)
|
2026-03-10 01:44:10 -04:00
|
|
|
elif status == "3":
|
|
|
|
|
self.execute_unit_tests()
|
2026-03-08 15:53:51 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-03-08 15:53:51 -04:00
|
|
|
|
|
|
|
|
def execute_test_module(self, coverage=False):
|
|
|
|
|
# Module name
|
2026-03-12 05:31:45 -04:00
|
|
|
module_name = input(t("Module name to test: ")).strip()
|
2026-03-08 15:53:51 -04:00
|
|
|
if not module_name:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Module name is required!"))
|
2026-03-08 15:53:51 -04:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Database name
|
2026-03-13 15:04:14 -04:00
|
|
|
db_name = input(
|
|
|
|
|
t("Temporary database name (default: test_todo_tmp): ")
|
|
|
|
|
).strip()
|
2026-03-08 15:53:51 -04:00
|
|
|
if not db_name:
|
|
|
|
|
db_name = "test_todo_tmp"
|
|
|
|
|
|
|
|
|
|
# Extra modules
|
2026-03-13 15:04:14 -04:00
|
|
|
extra_modules = input(
|
|
|
|
|
t("Extra modules to install (comma-separated, empty for none): ")
|
|
|
|
|
).strip()
|
2026-03-08 15:53:51 -04:00
|
|
|
|
|
|
|
|
# Log level
|
2026-03-12 05:31:45 -04:00
|
|
|
log_level = input(t("Log level (default: test): ")).strip()
|
2026-03-08 15:53:51 -04:00
|
|
|
if not log_level:
|
|
|
|
|
log_level = "test"
|
|
|
|
|
|
|
|
|
|
# Build module list
|
|
|
|
|
modules_to_install = module_name
|
|
|
|
|
if extra_modules:
|
|
|
|
|
modules_to_install += f",{extra_modules}"
|
|
|
|
|
|
|
|
|
|
# Step 1: Create temp DB
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n--- {t('Creating temporary database')} '{db_name}' ---")
|
2026-03-09 16:34:48 -04:00
|
|
|
cmd_restore = f"./script/database/db_restore.py --database {db_name}"
|
2026-03-08 15:53:51 -04:00
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd_restore,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Step 2: Install modules
|
2026-03-13 15:04:14 -04:00
|
|
|
print(f"\n--- {t('Installing modules')}: {modules_to_install} ---")
|
2026-03-08 15:53:51 -04:00
|
|
|
cmd_install = (
|
|
|
|
|
f"./script/addons/install_addons.sh"
|
|
|
|
|
f" {db_name} {modules_to_install}"
|
|
|
|
|
)
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd_install,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Step 3: Run tests
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n--- {t('Running tests')}: {module_name} ---")
|
2026-03-08 15:53:51 -04:00
|
|
|
cmd_test = (
|
|
|
|
|
f"ODOO_MODE_TEST=true"
|
|
|
|
|
f" ./run.sh"
|
|
|
|
|
f" -d {db_name}"
|
|
|
|
|
f" -u {module_name}"
|
|
|
|
|
f" --log-level={log_level}"
|
|
|
|
|
)
|
|
|
|
|
if coverage:
|
|
|
|
|
cmd_test = f"ODOO_MODE_COVERAGE=true {cmd_test}"
|
|
|
|
|
status_code, output = self.execute.exec_command_live(
|
|
|
|
|
cmd_test,
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if status_code == 0:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n✅ {t('Tests completed successfully!')}")
|
2026-03-08 15:53:51 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n❌ {t('Tests failed with return code')} {status_code}")
|
2026-03-08 15:53:51 -04:00
|
|
|
|
|
|
|
|
# Step 4: Cleanup
|
|
|
|
|
lang = get_lang()
|
2026-03-13 15:04:14 -04:00
|
|
|
keep_input = (
|
|
|
|
|
input(t("Keep the temporary database? (y/N): ")).strip().lower()
|
|
|
|
|
)
|
2026-03-09 16:34:48 -04:00
|
|
|
keep = keep_input in (("o", "oui") if lang == "fr" else ("y", "yes"))
|
2026-03-08 15:53:51 -04:00
|
|
|
if keep:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Database kept')}: {db_name}")
|
2026-03-08 15:53:51 -04:00
|
|
|
else:
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
f"\n--- {t('Cleaning up temporary database')} '{db_name}' ---"
|
|
|
|
|
)
|
2026-03-09 16:34:48 -04:00
|
|
|
cmd_drop = f"./odoo_bin.sh db --drop --database {db_name}"
|
2026-03-08 15:53:51 -04:00
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd_drop,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 01:44:10 -04:00
|
|
|
def execute_unit_tests(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n--- {t('Running unit tests')} ---")
|
2026-03-10 01:44:10 -04:00
|
|
|
cmd = (
|
|
|
|
|
".venv.erplibre/bin/python -m unittest discover"
|
|
|
|
|
" -s test -p 'test_*.py' -v"
|
|
|
|
|
)
|
|
|
|
|
status_code, output = self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
)
|
|
|
|
|
if status_code == 0:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n✅ {t('All unit tests passed')}")
|
2026-03-10 01:44:10 -04:00
|
|
|
else:
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
f"\n❌ {t('Some unit tests failed, exit code')}: {status_code}"
|
|
|
|
|
)
|
2026-03-10 01:44:10 -04:00
|
|
|
|
2026-03-07 01:38:55 -05:00
|
|
|
def execute_pip_audit(self):
|
2026-03-10 03:45:11 -04:00
|
|
|
versions, installed_versions, odoo_installed_version = (
|
2026-03-10 03:59:35 -04:00
|
|
|
get_odoo_version()
|
2026-03-07 01:38:55 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Build list of installed environments
|
2026-03-10 03:45:11 -04:00
|
|
|
environments = {}
|
2026-03-07 01:38:55 -05:00
|
|
|
key_i = 0
|
2026-03-10 03:45:11 -04:00
|
|
|
for version_info in versions[::-1]:
|
|
|
|
|
erplibre_version = version_info.get("erplibre_version")
|
2026-03-07 01:38:55 -05:00
|
|
|
venv_path = f".venv.{erplibre_version}"
|
|
|
|
|
req_path = f"requirement/requirements.{erplibre_version}.txt"
|
2026-03-10 03:45:11 -04:00
|
|
|
odoo_version = f"odoo{version_info.get('odoo_version')}"
|
2026-03-07 01:38:55 -05:00
|
|
|
|
|
|
|
|
if not os.path.isdir(venv_path):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
key_i += 1
|
|
|
|
|
key_s = str(key_i)
|
|
|
|
|
label = f"{key_s}: {erplibre_version}"
|
|
|
|
|
if odoo_version == odoo_installed_version:
|
2026-03-12 05:31:45 -04:00
|
|
|
label += f" - {t('Current')}"
|
|
|
|
|
if version_info.get("Default"):
|
|
|
|
|
label += f" - {t('Default')}"
|
2026-03-07 01:38:55 -05:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
environments[key_s] = {
|
2026-03-07 01:38:55 -05:00
|
|
|
"label": label,
|
|
|
|
|
"venv_path": venv_path,
|
|
|
|
|
"req_path": req_path,
|
|
|
|
|
"erplibre_version": erplibre_version,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
if not environments:
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
t(
|
|
|
|
|
"No installed environment found. Install an Odoo version first."
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-03-07 01:38:55 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Show selection menu
|
|
|
|
|
str_input = (
|
2026-03-12 05:31:45 -04:00
|
|
|
f"💬 {t('Choose an environment for the audit:')}\n\t"
|
2026-03-10 03:45:11 -04:00
|
|
|
+ "\n\t".join([v["label"] for v in environments.values()])
|
2026-03-12 05:31:45 -04:00
|
|
|
+ f"\n\t0: {t('Back')}"
|
|
|
|
|
+ f"\n{t('Select: ')}"
|
2026-03-07 01:38:55 -05:00
|
|
|
)
|
|
|
|
|
env_input = ""
|
2026-03-10 03:45:11 -04:00
|
|
|
while env_input not in environments and env_input != "0":
|
2026-03-07 01:38:55 -05:00
|
|
|
if env_input:
|
2026-03-13 15:04:14 -04:00
|
|
|
print(
|
|
|
|
|
f"{t('Error, cannot understand value')}" f" '{env_input}'"
|
|
|
|
|
)
|
2026-03-07 01:38:55 -05:00
|
|
|
env_input = input(str_input).strip()
|
|
|
|
|
|
|
|
|
|
if env_input == "0":
|
|
|
|
|
return
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
selected = environments[env_input]
|
2026-03-07 01:38:55 -05:00
|
|
|
venv_path = selected["venv_path"]
|
|
|
|
|
req_path = selected["req_path"]
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(req_path):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Dependencies file not found: ')}{req_path}")
|
2026-03-07 01:38:55 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# TODO support bash from parameter if open gnome-terminal
|
|
|
|
|
cmd = f"pip-audit -r {req_path} -l;bash"
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"{t('Execution: ')}{cmd}")
|
2026-03-07 01:38:55 -05:00
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=True,
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-13 03:46:27 -05:00
|
|
|
def generate_config(self, add_arg=None):
|
|
|
|
|
# Repeating to get all item before get group
|
|
|
|
|
cmd = (
|
2026-02-16 17:59:13 -05:00
|
|
|
f"./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA;"
|
2026-02-13 03:46:27 -05:00
|
|
|
f"./script/git/git_repo_update_group.py;"
|
|
|
|
|
f"./script/generate_config.sh"
|
|
|
|
|
)
|
|
|
|
|
if add_arg:
|
|
|
|
|
cmd += (
|
|
|
|
|
f";./script/git/git_repo_update_group.py {add_arg};"
|
|
|
|
|
f"./script/generate_config.sh"
|
|
|
|
|
)
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def generate_config_from_preconfiguration(self):
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("base")},
|
|
|
|
|
{"prompt_description": t("base + code_generator")},
|
|
|
|
|
{"prompt_description": t("base + image_db")},
|
|
|
|
|
{"prompt_description": t("all")},
|
2026-02-13 03:46:27 -05:00
|
|
|
# {"prompt_description": "base + migration"},
|
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-13 03:46:27 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
group = "base"
|
|
|
|
|
str_group = f"--group {group}"
|
|
|
|
|
self.generate_config(add_arg=str_group)
|
|
|
|
|
elif status == "2":
|
|
|
|
|
group = "base,code_generator"
|
|
|
|
|
str_group = f"--group {group}"
|
|
|
|
|
self.generate_config(add_arg=str_group)
|
|
|
|
|
elif status == "3":
|
|
|
|
|
group = "base,image_db"
|
|
|
|
|
str_group = f"--group {group}"
|
|
|
|
|
self.generate_config(add_arg=str_group)
|
|
|
|
|
elif status == "4":
|
|
|
|
|
self.generate_config()
|
|
|
|
|
# elif status == "5":
|
|
|
|
|
# group = "base,migration"
|
|
|
|
|
# str_group = f"--group {group}"
|
|
|
|
|
# self.generate_config(add_arg=str_group)
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-02-13 03:46:27 -05:00
|
|
|
|
2026-02-28 02:08:04 -05:00
|
|
|
def debug_ide(self):
|
2026-03-10 03:45:11 -04:00
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
{"prompt_description": t("Debug todo.py")},
|
2026-02-28 02:08:04 -05:00
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-28 02:08:04 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self.open_pycharm_file(
|
|
|
|
|
os.getcwd(),
|
|
|
|
|
os.path.join(os.getcwd(), "script/todo/todo.py"),
|
|
|
|
|
)
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Command not found !"))
|
2026-02-28 02:08:04 -05:00
|
|
|
|
2026-02-13 03:46:27 -05:00
|
|
|
def generate_config_from_backup(self):
|
2026-03-10 03:59:35 -04:00
|
|
|
file_name = self.db_manager.open_file_image_db()
|
2026-02-13 03:46:27 -05:00
|
|
|
add_arg = f"--from_backup_name {file_name} --add_repo odoo18.0/addons/MathBenTech_development"
|
|
|
|
|
self.generate_config(add_arg=add_arg)
|
|
|
|
|
|
|
|
|
|
def generate_config_from_database(self):
|
2026-03-10 03:59:35 -04:00
|
|
|
database_name = self.db_manager.select_database()
|
2026-02-13 04:07:02 -05:00
|
|
|
str_arg = f"--database {database_name}"
|
|
|
|
|
self.generate_config(add_arg=str_arg)
|
|
|
|
|
return False
|
|
|
|
|
|
2026-02-28 02:14:38 -05:00
|
|
|
def generate_config_queue_job(self):
|
|
|
|
|
cmd = "./script/config/setup_odoo_config_conf_devops.py"
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
cmd,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 04:05:04 -04:00
|
|
|
def prompt_execute_selenium_and_run_db(
|
|
|
|
|
self, db_name, extra_cmd_web_login=""
|
|
|
|
|
):
|
2026-03-10 03:45:11 -04:00
|
|
|
cmd_server = f"./run.sh -d {db_name};bash"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd_server)
|
2025-10-31 01:10:54 -04:00
|
|
|
cmd_client = (
|
|
|
|
|
f"sleep 3;./script/selenium/web_login.py{extra_cmd_web_login};bash"
|
2025-04-28 00:32:51 -04:00
|
|
|
)
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd_client)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2025-04-28 00:32:51 -04:00
|
|
|
def prompt_execute_selenium(self, command=None, extra_cmd_web_login=""):
|
2026-03-10 03:45:11 -04:00
|
|
|
commands = []
|
2025-04-26 17:12:38 -04:00
|
|
|
if not command:
|
|
|
|
|
cmd = "./script/selenium/web_login.py"
|
|
|
|
|
else:
|
|
|
|
|
cmd = command
|
|
|
|
|
|
|
|
|
|
if type(extra_cmd_web_login) is list:
|
|
|
|
|
for item in extra_cmd_web_login:
|
2026-03-10 03:45:11 -04:00
|
|
|
commands.append(cmd + item)
|
2025-04-26 17:12:38 -04:00
|
|
|
else:
|
2026-03-10 03:45:11 -04:00
|
|
|
commands.append(cmd + extra_cmd_web_login)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
if len(commands) == 1:
|
|
|
|
|
self.execute.exec_command_live(commands[0])
|
|
|
|
|
elif len(commands) > 1:
|
2025-04-26 17:12:38 -04:00
|
|
|
new_cmd = "parallel ::: "
|
2026-03-10 03:45:11 -04:00
|
|
|
for i, cmd in enumerate(commands):
|
2025-04-26 17:12:38 -04:00
|
|
|
new_cmd += f' "sleep {1 * i};{cmd}"'
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(new_cmd)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
def crash_diagnostic(self, e):
|
2026-03-10 03:06:07 -04:00
|
|
|
# TODO show message at start if os.path.exists(ERROR_LOG_PATH)
|
|
|
|
|
if os.path.exists(ERROR_LOG_PATH) and not os.path.exists(
|
|
|
|
|
VENV_ERPLIBRE
|
2025-08-07 06:09:37 -04:00
|
|
|
):
|
|
|
|
|
print("Got error : ")
|
|
|
|
|
print(e)
|
2026-03-10 03:06:07 -04:00
|
|
|
print("Got error at first execution.", ERROR_LOG_PATH)
|
2025-08-07 06:09:37 -04:00
|
|
|
try:
|
2026-03-10 03:06:07 -04:00
|
|
|
file = open(ERROR_LOG_PATH, "r")
|
2025-08-07 06:09:37 -04:00
|
|
|
content = file.read()
|
|
|
|
|
# TODO si vide, ajouter notre erreur
|
|
|
|
|
print(content)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
print("Error: File not found.")
|
|
|
|
|
finally:
|
|
|
|
|
if "file" in locals() and file:
|
|
|
|
|
file.close()
|
|
|
|
|
# Force auto installation
|
|
|
|
|
print("Auto installation")
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
cmd = "./script/todo/source_todo.sh"
|
|
|
|
|
# self.restart_script(e)
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
2025-08-07 06:09:37 -04:00
|
|
|
sys.exit(1)
|
2026-03-10 03:06:07 -04:00
|
|
|
if os.path.exists(VENV_ERPLIBRE):
|
2025-08-07 06:09:37 -04:00
|
|
|
print("Import error : ")
|
|
|
|
|
print(e)
|
|
|
|
|
# TODO auto-detect gnome-terminal, or choose another. Is it done already?
|
|
|
|
|
self.restart_script(e)
|
|
|
|
|
# self.prompt_install()
|
|
|
|
|
|
|
|
|
|
# print(
|
2026-03-10 03:06:07 -04:00
|
|
|
# f"You forgot to activate source \nsource ./{VENV_ERPLIBRE}/bin/activate"
|
2025-08-07 06:09:37 -04:00
|
|
|
# )
|
|
|
|
|
# time.sleep(0.5)
|
|
|
|
|
# cmd = "./script/todo/source_todo.sh"
|
|
|
|
|
print("Re-execute TODO 🤖 or execute :")
|
|
|
|
|
print()
|
2026-03-10 03:06:07 -04:00
|
|
|
print(f"source {VENV_ERPLIBRE}/bin/activate;make")
|
2025-08-07 06:09:37 -04:00
|
|
|
print()
|
|
|
|
|
cmd = "./script/todo/todo.py"
|
|
|
|
|
# # self.restart_script(e)
|
|
|
|
|
try:
|
|
|
|
|
# TODO duplicate
|
|
|
|
|
import click
|
|
|
|
|
import humanize
|
|
|
|
|
import openai
|
2025-10-31 01:10:54 -04:00
|
|
|
import urwid
|
2025-08-07 06:09:37 -04:00
|
|
|
from pykeepass import PyKeePass
|
|
|
|
|
except ImportError:
|
|
|
|
|
print("Rerun and exit")
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
2025-08-07 06:09:37 -04:00
|
|
|
sys.exit(1)
|
|
|
|
|
print("No error")
|
|
|
|
|
else:
|
|
|
|
|
self.prompt_install()
|
|
|
|
|
|
2026-02-13 04:07:02 -05:00
|
|
|
def open_shell_on_database(self):
|
2026-03-10 03:59:35 -04:00
|
|
|
database = self.db_manager.select_database()
|
2026-03-07 02:59:07 -05:00
|
|
|
if database:
|
|
|
|
|
cmd_server = f"./odoo_bin.sh shell -d {database}"
|
2026-03-10 03:45:11 -04:00
|
|
|
status, databases = self.execute.exec_command_live(
|
2026-03-07 02:59:07 -05:00
|
|
|
cmd_server,
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
new_window=True,
|
|
|
|
|
)
|
2026-02-13 04:07:02 -05:00
|
|
|
|
2026-02-28 02:08:04 -05:00
|
|
|
def open_pycharm_file(self, folder, filename):
|
|
|
|
|
cmd = "~/.local/share/JetBrains/Toolbox/scripts/pycharm"
|
|
|
|
|
# cmd = "/snap/bin/pycharm-community"
|
|
|
|
|
# if pycharm_arg:
|
|
|
|
|
# cmd += f" {pycharm_arg}"
|
|
|
|
|
if folder:
|
|
|
|
|
cmd += f" {folder}"
|
|
|
|
|
if filename:
|
|
|
|
|
cmd += f" --line 1 {filename}"
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
def upgrade_module(self):
|
|
|
|
|
upgrade = todo_upgrade.TodoUpgrade(self)
|
|
|
|
|
upgrade.execute_module_upgrade()
|
|
|
|
|
|
|
|
|
|
def upgrade_poetry(self):
|
|
|
|
|
# Only show the version to the user
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
f"make version",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
# TODO maybe autodetect to update it
|
|
|
|
|
git_repo_update_input = input(
|
|
|
|
|
"💬 Would you like to fetch all your git repositories, you need it (y/Y) : "
|
|
|
|
|
)
|
|
|
|
|
if git_repo_update_input.strip().lower() == "y":
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
f"./script/manifest/update_manifest_local_dev.sh",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
poetry_lock = "./poetry.lock"
|
|
|
|
|
try:
|
|
|
|
|
os.remove(poetry_lock)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
pass
|
|
|
|
|
odoo_long_version = ""
|
|
|
|
|
if os.path.exists("./.erplibre-version"):
|
|
|
|
|
with open("./.erplibre-version") as f:
|
|
|
|
|
odoo_long_version = f.read()
|
|
|
|
|
path_file_odoo_lock = f"./requirement/poetry.{odoo_long_version}.lock"
|
|
|
|
|
if odoo_long_version:
|
|
|
|
|
try:
|
|
|
|
|
os.remove(path_file_odoo_lock)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-10-31 01:10:54 -04:00
|
|
|
f"pip install -r requirement/erplibre_require-ments-poetry.txt && "
|
|
|
|
|
f"./script/poetry/poetry_update.py -f",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
source_odoo=odoo_long_version,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if os.path.exists(poetry_lock):
|
|
|
|
|
shutil.copy2(poetry_lock, path_file_odoo_lock)
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
def callback_execute_custom_database(self, config):
|
2026-03-10 03:59:35 -04:00
|
|
|
database_name = self.db_manager.select_database()
|
2026-02-28 01:35:32 -05:00
|
|
|
self.prompt_execute_selenium_and_run_db(database_name)
|
|
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
def process_kill_from_port(self):
|
|
|
|
|
cfg = configparser.ConfigParser()
|
|
|
|
|
cfg.read("./config.conf")
|
|
|
|
|
http_port = cfg.getint("options", "http_port")
|
|
|
|
|
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-12-22 04:02:44 -05:00
|
|
|
f"./script/process/kill_process_by_port.py {http_port} --kill-tree --nb_parent 2",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
)
|
|
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
def restart_script(self, last_error):
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"🤖 {t('Reboot TODO ...')}")
|
2025-08-07 06:09:37 -04:00
|
|
|
# os.execv(sys.executable, ['python'] + sys.argv)
|
|
|
|
|
# TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger
|
2026-03-10 03:06:07 -04:00
|
|
|
if os.path.exists(VENV_ERPLIBRE) and not os.path.exists(
|
|
|
|
|
ERROR_LOG_PATH
|
2025-08-07 06:09:37 -04:00
|
|
|
):
|
|
|
|
|
# TODO mettre check import suivant ne vont pas planter
|
|
|
|
|
try:
|
2026-03-10 03:06:07 -04:00
|
|
|
with open(ERROR_LOG_PATH, "w") as f_file:
|
2025-08-07 06:09:37 -04:00
|
|
|
f_file.write(str(last_error))
|
|
|
|
|
pass # The file is created and closed here, no content is written
|
|
|
|
|
print(
|
2026-03-10 03:06:07 -04:00
|
|
|
f"Try to reopen process with before :\nsource ./{VENV_ERPLIBRE}/bin/activate && exec python "
|
2025-08-07 06:09:37 -04:00
|
|
|
+ " ".join(sys.argv)
|
|
|
|
|
)
|
|
|
|
|
os.execv(
|
|
|
|
|
"/bin/bash",
|
|
|
|
|
[
|
|
|
|
|
"/bin/bash",
|
|
|
|
|
"-c",
|
2026-03-10 03:06:07 -04:00
|
|
|
f"source ./{VENV_ERPLIBRE}/bin/activate && exec python "
|
2025-08-07 06:09:37 -04:00
|
|
|
+ " ".join(sys.argv),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print("Error detect at first execution.")
|
|
|
|
|
print(e)
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
def on_dir_selected(self, dir_path):
|
|
|
|
|
self.dir_path = dir_path
|
|
|
|
|
todo_file_browser.exit_program()
|
|
|
|
|
|
2026-03-10 04:18:17 -04:00
|
|
|
def callback_make_mobile_home(self, config):
|
2025-12-27 05:23:48 -05:00
|
|
|
# Read file
|
2025-10-01 04:53:08 -04:00
|
|
|
default_project_name = "ERPLibre"
|
2025-12-27 05:23:48 -05:00
|
|
|
default_package_name = "ca.erplibre.home"
|
|
|
|
|
# Read default information
|
|
|
|
|
if os.path.exists(STRINGS_FILE):
|
|
|
|
|
tree = ET.parse(STRINGS_FILE)
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
for elem in root.findall("string"):
|
|
|
|
|
if elem.get("name") == "app_name":
|
|
|
|
|
default_project_name = elem.text
|
|
|
|
|
if elem.get("name") == "package_name":
|
|
|
|
|
default_package_name = elem.text
|
|
|
|
|
|
2025-10-01 04:53:08 -04:00
|
|
|
default_project_url_name = "https://erplibre.ca"
|
2025-12-27 05:23:48 -05:00
|
|
|
# Read default information
|
|
|
|
|
dotenv_file = dotenv.find_dotenv(
|
|
|
|
|
filename=os.path.join(MOBILE_HOME_PATH, "src", ".env.production")
|
|
|
|
|
)
|
|
|
|
|
default_project_url_name = dotenv.get_key(
|
|
|
|
|
dotenv_file, "VITE_WEBSITE_URL"
|
|
|
|
|
)
|
2025-12-28 01:40:42 -05:00
|
|
|
default_project_note_subject = dotenv.get_key(
|
|
|
|
|
dotenv_file, "VITE_LABEL_NOTE"
|
|
|
|
|
)
|
2025-12-27 05:23:48 -05:00
|
|
|
|
2025-10-01 04:53:08 -04:00
|
|
|
default_debug = False
|
|
|
|
|
project_name = default_project_name
|
|
|
|
|
project_url_name = default_project_url_name
|
2025-12-28 01:40:42 -05:00
|
|
|
project_principal_subject = default_project_note_subject
|
2025-12-27 05:23:48 -05:00
|
|
|
package_name = default_package_name
|
2025-10-01 04:53:08 -04:00
|
|
|
do_debug = default_debug
|
2025-12-27 05:23:48 -05:00
|
|
|
do_change_picture_menu = False
|
2025-10-01 04:53:08 -04:00
|
|
|
|
|
|
|
|
do_personalize = input(
|
|
|
|
|
"Do you want to personalize the mobile application (Y) : "
|
|
|
|
|
)
|
|
|
|
|
if do_personalize.strip().lower() == "y":
|
|
|
|
|
project_name = (
|
|
|
|
|
input(
|
2025-12-27 05:23:48 -05:00
|
|
|
f'Your project name (Separate by space in title), default "{default_project_name}" : '
|
2025-10-01 04:53:08 -04:00
|
|
|
).strip()
|
|
|
|
|
or default_project_name
|
|
|
|
|
)
|
2025-12-27 05:23:48 -05:00
|
|
|
package_name = (
|
|
|
|
|
input(
|
|
|
|
|
f'Your package name (separate by . lower case, 3 works like DOMAIN.NAME.OBJECT), default "{default_package_name}" : '
|
|
|
|
|
).strip()
|
|
|
|
|
or default_package_name
|
|
|
|
|
)
|
2025-10-01 04:53:08 -04:00
|
|
|
project_url_name = (
|
|
|
|
|
input(
|
2025-12-28 01:40:42 -05:00
|
|
|
f'Your project url website, default "{default_project_url_name}" : '
|
2025-10-01 04:53:08 -04:00
|
|
|
).strip()
|
|
|
|
|
or default_project_url_name
|
|
|
|
|
)
|
2025-12-28 01:40:42 -05:00
|
|
|
project_principal_subject = (
|
|
|
|
|
input(
|
|
|
|
|
f'Your project subject, default "{default_project_note_subject}" : '
|
|
|
|
|
).strip()
|
|
|
|
|
or default_project_note_subject
|
|
|
|
|
)
|
2025-10-01 04:53:08 -04:00
|
|
|
do_debug = (
|
2025-12-27 05:23:48 -05:00
|
|
|
input("Compilation with debug information, default No (Y) : ")
|
|
|
|
|
.strip()
|
|
|
|
|
.lower()
|
2025-10-01 04:53:08 -04:00
|
|
|
== "y"
|
|
|
|
|
)
|
2025-12-27 05:23:48 -05:00
|
|
|
do_change_picture_menu = (
|
|
|
|
|
input(
|
|
|
|
|
"Want to change picture from menu, you need android-studio (Y) : "
|
|
|
|
|
)
|
|
|
|
|
.strip()
|
|
|
|
|
.lower()
|
|
|
|
|
== "y"
|
2025-10-01 04:53:08 -04:00
|
|
|
)
|
2025-12-27 05:23:48 -05:00
|
|
|
|
|
|
|
|
# Rename with script bash
|
|
|
|
|
cmd_client = f'cd {MOBILE_HOME_PATH} && npx cap init "{project_name}" "{package_name}" && ./rename_android.sh "{project_name}" "{package_name}" && npx cap sync android'
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd_client, source_erplibre=False)
|
2025-12-27 05:23:48 -05:00
|
|
|
|
2025-10-01 04:53:08 -04:00
|
|
|
# dotenv_mobile = dotenv.dotenv_values(dotenv_file)
|
|
|
|
|
# dotenv_mobile["VITE_TITLE"] = project_name
|
|
|
|
|
# dotenv_mobile["VITE_WEBSITE_URL"] = project_url_name
|
|
|
|
|
dotenv.set_key(
|
|
|
|
|
dotenv_file, "VITE_TITLE", project_name, quote_mode="always"
|
|
|
|
|
)
|
|
|
|
|
dotenv.set_key(
|
|
|
|
|
dotenv_file,
|
|
|
|
|
"VITE_WEBSITE_URL",
|
|
|
|
|
project_url_name,
|
|
|
|
|
quote_mode="always",
|
|
|
|
|
)
|
2025-12-28 01:40:42 -05:00
|
|
|
dotenv.set_key(
|
|
|
|
|
dotenv_file,
|
|
|
|
|
"VITE_LABEL_NOTE",
|
|
|
|
|
project_principal_subject,
|
|
|
|
|
quote_mode="always",
|
|
|
|
|
)
|
2025-10-01 04:53:08 -04:00
|
|
|
dotenv.set_key(
|
|
|
|
|
dotenv_file,
|
|
|
|
|
"VITE_DEBUG_DEV",
|
|
|
|
|
"true" if do_debug else "false",
|
|
|
|
|
quote_mode="never",
|
|
|
|
|
)
|
2025-12-27 05:23:48 -05:00
|
|
|
|
|
|
|
|
if do_change_picture_menu:
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-12-27 05:23:48 -05:00
|
|
|
f"cd {MOBILE_HOME_PATH} && npx cap open android;bash",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
new_window=True,
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
"Guide for Android-Studio, wait loading is finish. Right-click to app/New/Image Asset and load your image."
|
|
|
|
|
)
|
|
|
|
|
input(
|
|
|
|
|
"Did you finish to update image with Android-Studio ? Press to continue ..."
|
|
|
|
|
)
|
2025-12-28 01:40:42 -05:00
|
|
|
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/company_logo.png"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd_client, source_erplibre=False)
|
2025-12-28 01:40:42 -05:00
|
|
|
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/imgs/logo.png"
|
2026-02-13 01:27:38 -05:00
|
|
|
self.execute.exec_command_live(cmd_client, source_erplibre=False)
|
2025-12-27 05:23:48 -05:00
|
|
|
|
2026-02-13 01:27:38 -05:00
|
|
|
status = self.execute.exec_command_live(
|
2025-10-01 04:53:08 -04:00
|
|
|
"./mobile/compile_and_run.sh", source_erplibre=False
|
|
|
|
|
)
|
|
|
|
|
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2025-08-07 06:09:37 -04:00
|
|
|
start_time = time.time()
|
|
|
|
|
try:
|
|
|
|
|
todo = TODO()
|
|
|
|
|
if ENABLE_CRASH:
|
|
|
|
|
todo.crash_diagnostic(CRASH_E)
|
|
|
|
|
todo.run()
|
|
|
|
|
except KeyboardInterrupt:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(t("Keyboard interrupt"))
|
2025-08-07 06:09:37 -04:00
|
|
|
finally:
|
|
|
|
|
end_time = time.time()
|
|
|
|
|
duration_sec = end_time - start_time
|
|
|
|
|
if humanize:
|
|
|
|
|
duration_delta = datetime.timedelta(seconds=duration_sec)
|
|
|
|
|
humain_time = humanize.precisedelta(duration_delta)
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n{t('TODO execution time')} {humain_time}\n")
|
2025-08-07 06:09:37 -04:00
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
print(f"\n{t('TODO execution time')} {duration_sec:.2f} sec.\n")
|