diff --git a/tools/yunopackage/YunohostAppGenerator.py b/tools/yunopackage/YunohostAppGenerator.py
new file mode 100644
index 0000000..cdd8536
--- /dev/null
+++ b/tools/yunopackage/YunohostAppGenerator.py
@@ -0,0 +1,577 @@
+#### Imports
+import jinja2 as j2
+from flask import Flask, render_template, render_template_string, request, redirect, flash, send_file
+from markupsafe import Markup # No longer imported from Flask
+# Form libraries
+from flask_wtf import FlaskForm
+from wtforms import StringField, RadioField, SelectField, SubmitField, TextAreaField, BooleanField, SelectMultipleField
+from wtforms.validators import DataRequired, InputRequired, Optional, Regexp, URL, Length
+# Markdown to HTML - for debugging purposes
+from misaka import Markdown, HtmlRenderer
+# Managing zipfiles
+import zipfile
+from flask_cors import CORS
+from urllib import parse
+from secrets import token_urlsafe
+
+#### GLOBAL VARIABLES
+YOLOGEN_VERSION = '0.7.5'
+GENERATOR_DICT = {'GENERATOR_VERSION': YOLOGEN_VERSION}
+
+#### Create FLASK and Jinja Environments
+url_prefix = ''
+# url_prefix = '/yunohost-app-generator'
+
+# app = Flask(__name__)
+app = Flask(__name__) # Blueprint('main', __name__, url_prefix=url_prefix)
+app.config['SECRET_KEY'] = token_urlsafe(16) # Necessary for the form CORS
+cors = CORS(app)
+
+environment = j2.Environment(loader=j2.FileSystemLoader("templates/"))
+
+
+#### Custom functions
+
+# Define custom filter
+@app.template_filter('render_markdown')
+def render_markdown(text):
+ renderer = HtmlRenderer()
+ markdown = Markdown(renderer)
+ return markdown(text)
+
+
+# Add custom filter
+j2.filters.FILTERS['render_markdown'] = render_markdown
+
+
+# Converting markdown to html
+def markdown_file_to_html_string(file):
+ with open(file, 'r') as file:
+ markdown_content = file.read()
+ # Convert content from Markdown to HTML
+ html_content = render_markdown(markdown_content)
+ # Return Markdown and HTML contents
+ return markdown_content, html_content
+
+
+### Forms
+
+## PHP forms
+class Form_PHP_Config(FlaskForm):
+ php_config_file = SelectField('Type de fichier PHP :', choices=[
+ ('php-fpm.conf', Markup('Fichier de configuration PHP complet (php-fpm.conf)')),
+ ('php_extra-fpm.conf',
+ Markup('Fichier de configuration PHP particulier (extra_php-fpm.conf)'))],
+ default='php_extra-fpm.conf', validators=[DataRequired()])
+ ## TODO : figure out how to include these comments/title values
+ # 'title': 'Remplace la configuration générée par défaut par un fichier de configuration complet. À éviter si possible.
+ # 'title': "Choisir un fichier permettant un paramétrage d'options complémentaires. C'est généralement recommandé."
+
+ php_config_file_content = TextAreaField("Saisissez le contenu du fichier de configuration PHP :",
+ validators=[Optional()],
+ render_kw={"class": "form-control",
+ "style": "width: 50%;height:11em;min-height: 5.5em; max-height: 55em;flex-grow: 1;box-sizing: border-box;",
+ "title": "TODO",
+ "placeholder": "; Additional php.ini defines, specific to this pool of workers. \n\nphp_admin_value[upload_max_filesize] = 100M \nphp_admin_value[post_max_size] = 100M"})
+
+
+class Form_PHP(Form_PHP_Config):
+ use_php = BooleanField('Nécessite PHP', default=False)
+
+
+## NodeJS forms
+class Form_NodeJS(FlaskForm):
+ use_nodejs = BooleanField('Nécessite NodeJS', default=False)
+ use_nodejs_version = StringField("Version de NodeJS :",
+ render_kw={"placeholder": "20", "class": "form-control",
+ "title": "Saisissez la version de NodeJS à installer. Cela peut-être une version majeure (ex: 20) ou plus précise (ex: 20.1)."}) # TODO : this should be validated using a regex, should be only numbers and any (≥0) number of dots in between
+ use_nodejs_needs_yarn = BooleanField('Nécessite Yarn', default=False, render_kw={
+ "title": "Faut-il installer automatiquement Yarn ? Cela configurera les dépôts spécifiques à Yarn."})
+
+
+## Python forms
+
+class Form_Python(FlaskForm):
+ use_python = BooleanField('Nécessite Python',
+ default=False) ## TODO -> python3, python3-pip, python3-ven dependencies by default
+ python_dependencies_type = SelectField('Configuration des dépendances Python :', choices=[
+ ('requirements.txt', Markup('Fichier requirements.txt')),
+ ('manual_list', 'Liste manuelle')],
+ default='requirements.txt', validators=[DataRequired(), Optional()])
+ python_requirements = TextAreaField("La liste de dépendances inclue dans le fichier requirements.txt :",
+ render_kw={"class": "form-control",
+ "style": "width: 50%;height:5.5em;min-height: 5.5em; max-height: 55em;flex-grow: 1;box-sizing: border-box;",
+ "title": "Lister les dépendances à installer, une par ligne, avec un numéro de version derrière.\nEx: 'dépendance==1.0'.",
+ "placeholder": "tensorflow==2.3.1 \nuvicorn==0.12.2 \nfastapi==0.63.0"})
+ python_dependencies_list = StringField("Liste de dépendances python :",
+ render_kw={"placeholder": "tensorflow uvicorn fastapi",
+ "class": "form-control",
+ "title": "Lister les dépendances à installer, séparées d'un espace."})
+
+
+## Manifest form
+# Dependencies form
+class DependenciesForm(FlaskForm):
+ auto_update = BooleanField("Activer le robot de mise à jour automatiques :", default=False,
+ render_kw={
+ "title": "Si le logiciel est disponible sur github et publie des releases ou des tags pour ses nouvelles versions, un robot proposera automatiquement des mises à jours."})
+
+ ## TODO
+ # These infos are used by https://github.com/YunoHost/apps/blob/master/tools/autoupdate_app_sources/autoupdate_app_sources.py
+ # to auto-update the previous asset urls and sha256sum + manifest version
+ # assuming the upstream's code repo is on github and relies on tags or releases
+ # See the 'sources' resource documentation for more details
+
+ # autoupdate.strategy = "latest_github_tag"
+
+ dependencies = StringField("Dépendances de l'application (liste des paquets apt) à installer :",
+ render_kw={"placeholder": "foo foo2.1-ext somerandomdep", "class": "form-control",
+ "title": "Lister les paquets dont dépend l'application, séparés par un espace."})
+
+ use_db = SelectField("Configurer une base de données :", choices=[
+ ('false', "Non"),
+ ('mysql', "MySQL/MariaDB"),
+ ('postgresql', "PostgreSQL")],
+ default='false',
+ render_kw={"title": "L'application nécessite-t-elle une base de données ?"})
+
+
+# manifest
+class manifestForm(DependenciesForm):
+ version = StringField('Version', validators=[Regexp('\d{1,4}.\d{1,4}(.\d{1,4})?(.\d{1,4})?~ynh\d+')],
+ render_kw={"class": "form-control",
+ "placeholder": "1.0~ynh1"})
+ description_en = TextAreaField("Description en quelques lignes de l'application, en anglais :",
+ validators=[DataRequired()],
+ render_kw={"class": "form-control", "style": "resize: none;",
+ "title": "Explain in *a few (10~15) words* the purpose of the app \\"
+ "or what it actually does (it is meant to give a rough idea to users browsing a catalog of 100+ apps)"})
+ description_fr = TextAreaField("Description en quelques lignes de l'application :", validators=[DataRequired()],
+ render_kw={"class": "form-control", "style": "resize: none;",
+ "title": "Expliquez en *quelques* (10~15) mots l'utilité de l'app \\"
+ "ou ce qu'elle fait (l'objectif est de donner une idée grossière pour des utilisateurs qui naviguent dans un catalogue de 100+ apps)"})
+
+ # TODO : handle multiple names separated by commas (.split(',') ?
+ maintainers = StringField('Mainteneurs et mainteneuses', render_kw={"class": "form-control",
+ "placeholder": "Généralement vous mettez votre nom ici… Si vous êtes d'accord ;)"}) # TODO : Usually you put your name here… if you like ;)
+ architectures = SelectMultipleField('Architectures supportées :', choices=[
+ ('all', 'Toutes les architectures'),
+ ('amd64', 'amd64'),
+ ('arm64', 'arm64'),
+ ('i386', 'i386'),
+ ('todo', 'TODO : list more architectures')],
+ default=['all'], validators=[DataRequired()])
+ yunohost_required_version = StringField('Mainteneurs et mainteneuses', render_kw={"class": "form-control",
+ "placeholder": "11.1.21",
+ "title": "Version minimale de Yunohost pour que l'application fonctionne."})
+
+ multi_instance = BooleanField("Application multi-instance", default=False,
+ render_kw={"class": "",
+ "title": "Peux-t-on installer simultannément plusieurs fois l'application sur un même serveur ?"})
+
+ ldap = SelectField('Integrate with LDAP (user can login using Yunohost credentials :', choices=[
+ ('false', 'False'),
+ ('true', 'True'),
+ ('not_relevant', 'Not relevant')], default='not_relevant', validators=[DataRequired()], render_kw={
+ "title": """Not to confuse with the "sso" key: the "ldap" key corresponds to wether or not a user *can* login on the app using its YunoHost credentials."""})
+ sso = SelectField('Integrate with Yunohost SingleSignOn (SSO) :', choices=[
+ ('false', 'False'),
+ ('true', 'True'),
+ ('not_relevant', 'Not relevant')], default='not_relevant', validators=[DataRequired()], render_kw={
+ "title": """Not to confuse with the "ldap" key: the "sso" key corresponds to wether or not a user is *automatically logged-in* on the app when logged-in on the YunoHost portal."""})
+
+ license = StringField('Licence', validators=[DataRequired()], render_kw={"class": "form-control",
+ "placeholder": "GPL"})
+
+ website = StringField('Site web', validators=[URL(), Optional()], render_kw={"class": "form-control",
+ "placeholder": "https://awesome-app-website.com"})
+ demo = StringField('Site de démonstration', validators=[URL(), Optional()], render_kw={"class": "form-control",
+ "placeholder": "https://awesome-app-website.com/demo"})
+ admindoc = StringField("Documentation d'aministration", validators=[URL(), Optional()],
+ render_kw={"class": "form-control",
+ "placeholder": "https://awesome-app-website.com/doc/admin"})
+ userdoc = StringField("Documentation d'utilisation", validators=[URL(), Optional()],
+ render_kw={"class": "form-control",
+ "placeholder": "https://awesome-app-website.com/doc/user"})
+ code = StringField('Dépôt de code', validators=[URL(), Optional()], render_kw={"class": "form-control",
+ "placeholder": "https://awesome-app-website.com/get-the-code"})
+
+ data_dir = BooleanField("L'application nécessite un répertoire dédié pour ses données", default=False,
+ render_kw={"title": "Faut-il créer un répertoire /home/yunohost.app/votreApplication ?"})
+ data_subdirs = StringField('Si nécessaire, lister les sous-répertoires à configurer :',
+ validators=[Optional()], render_kw={"class": "form-control",
+ "placeholder": "data, uploads, themes"})
+ use_whole_domain = BooleanField("L'application nécessite d'utiliser tout un domaine (installation à la racine) :",
+ default=False,
+ render_kw={
+ "title": "Doit-on installer l'application à la racine du domaine ? Sinon, on pourra l'installer dans un sous-dossier, par exemple /mon_app."})
+ supports_change_url = BooleanField(
+ "L'application autorise le changement d'adresse (changement de domaine ou de chemin)", default=True,
+ render_kw={"title": "Faut-il permettre le changement d'URL pour l'application ? (fichier change_url)"})
+
+ needs_admin = BooleanField("L'application nécessite de configurer un compte d'administration :", default=False,
+ render_kw={"class": "",
+ "title": "Faut-il configurer un compte admin à l'installation ?"})
+
+ # admin_password_help_message = BooleanField("TODO :", default=False,
+ # render_kw={"class": "",
+ # "title": "TODO"})
+
+ language = SelectMultipleField('Langues supportées :', choices=[
+ ('en', 'English'),
+ ('fr', 'Français'),
+ ('en', 'Spanish'),
+ ('it', 'Italian'),
+ ('de', 'German'),
+ ('zh', 'Chinese'),
+ ('jp', 'Japanese'),
+ ('da', 'Danish'),
+ ('pt', 'Portugese'),
+ ('nl', 'Dutch'),
+ ('ru', 'Russian')],
+ default=['en'], validators=[DataRequired()])
+
+ default_language = SelectField('Langues par défaut :', choices=[
+ ('en', 'English'),
+ ('fr', 'Français'),
+ ('en', 'Spanish'),
+ ('it', 'Italian'),
+ ('zh', 'Chinese'),
+ ('jp', 'Japanese'),
+ ('da', 'Danish'),
+ ('pt', 'Portugese'),
+ ('nl', 'Dutch'),
+ ('ru', 'Russian')],
+ default=['en'])
+
+ visibility = RadioField("Visibilité de l'application :", choices=[
+ ('admin', "Administrateur/administratrice uniquement"),
+ ('all_users', "Personnes connectées"),
+ ('visitors', "Publique")],
+ default='all_users', validators=[DataRequired()])
+
+ source_url = StringField("Code source ou exécutable de l'application", validators=[DataRequired(), URL()],
+ render_kw={"class": "form-control",
+ "placeholder": "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz"}) # Application source code URL
+ sha256sum = StringField('Empreinte du code source (format sha256sum)',
+ validators=[DataRequired(), Length(min=64, max=64)],
+ render_kw={"class": "form-control",
+ "placeholder": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ "title": "Sha256sum of the archive. Should be 64 characters-long."}) # Source code hash (sha256sum format)
+
+
+## Main form
+
+class appGeneratorForm(manifestForm, DependenciesForm, Form_PHP, Form_NodeJS, Form_Python):
+ app_name = StringField("Nom de l'application :", validators=[DataRequired()],
+ render_kw={"placeholder": "My Great App", "class": "form-control",
+ "title": "Définir le nom de l'application, affiché dans l'interface"})
+ app_id = StringField(
+ Markup(
+ """Identifiant (id) de l'application (en minuscule et sans espaces) :"""),
+ validators=[DataRequired(), Regexp("[a-z_1-9]+.*(?