diff --git a/README.md b/README.md
index a8d7e912637125b61ae23688de1071205d421f3e..1430045da052a1ae753d8de40fc34f7f954d184d 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,126 @@
 # Practical Work Manager (pwm)
 
-Ce repository contient différents scripts python pour gérer la réalisation de travaux pratiques par les étudiants avec la contrainte d'utiliser le gitlab d'HEPIA.
+Programme python pour gérer les travaux pratiques des étudiants avec la contrainte d'utiliser le gitlab d'HEPIA.
+
+## TL;DR
+
+- Créer un groupe et les repositories en une seule commande (avec un repository "image" optionnel) : `./pwm -t TOKEN group_repos GROUP_NAME REPOS_FILE [-i IMPORT_URL]`, voir [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file)
+- Clone tous les projets des étudiants d'un groupe gitlab (`ID`) dans un répertoire créé à la volée : `./pwm -t TOKEN clone ID DIRECTORY`
+
+- [Practical Work Manager (pwm)](#practical-work-manager-pwm)
+  - [TL;DR](#tldr)
+  - [Workflow d'utilisation](#workflow-dutilisation)
+  - [Utilisation de pwm](#utilisation-de-pwm)
+    - [Création d'un groupe et des projets](#création-dun-groupe-et-des-projets)
+    - [Création d'un groupe seulement](#création-dun-groupe-seulement)
+    - [Création d'un sous-projet dans le groupe](#création-dun-sous-projet-dans-le-groupe)
+    - [Clone de tous les repositories](#clone-de-tous-les-repositories)
+  - [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file)
+    - [Noms et emails](#noms-et-emails)
+    - [Emails seulement](#emails-seulement)
 
 ## Workflow d'utilisation
 Pour expliquer la démarche d'utilisation, prenons le scénario suivant :
-- L'enseignant Michel Albuquerque prépare un nouveau travail pratique (TP). Il souhaite forcer les étudiants à utiliser git et githepia pour qu'ils versionnent leur code et pour qu'il puisse visualiser et recevoir leur rendus.
+- L'enseignant Michel Albuquerque prépare un nouveau travail pratique (TP). Il souhaite forcer les étudiants à utiliser git et gitedu pour qu'ils versionnent leur code et pour qu'il puisse visualiser et recevoir leur rendus.
 - Les TPs sont à faire par groupe ou de manière individuelle.
 - Pour transmettre son énoncé et des fichiers (exemples, squelette de code, librairies, binaires, etc.) aux étudiants, Michel Albuquerque crée un repository git accessible publiquement, nommé "super-tp".
 - Grâce à ce repository "super-tp", il peut mettre à jour le contenu distribué aux étudiants, en leur offrant la possibilité de visualiser les changements incrémentaux survenus.
 
-Le moment est venu de créer les dépôts git pour chaque groupe/étudiant suivant le cours et devant réaliser le TP. Sur la base d'une liste de groupes ou d'étudiants, Michel Albuquerque pourra utiliser les scripts suivants pour :
+Le moment est venu de créer les dépôts git pour chaque groupe/étudiant suivant le cours et devant réaliser le TP. Sur la base d'une liste de groupes ou d'étudiants, Michel Albuquerque pourra utiliser le programme pour :
 1. Créer le groupe (namespace) dédié au cours/TP, contenant tous les repositories des étudiants.
 1. Créer chaque repository pour chaque groupe/étudiant avec les contraintes nécessaires (privé, accessible à (aux) l'étudiant(s) concerné(s), aux enseignants, etc.)
 1. Récupérer (clone) sur sa machine tous les repositories d'un seul coup, dans des répertoires séparés, au moment du rendu par exemple.
 
-La section suivante décrit l'utilisation des scripts.
+## Utilisation de pwm
 
-## Utilisation des scripts
+Ce programme est écrit en python et testé avec la version 3.9.0, avec les dépendances suivantes (voir `requirements.txt`) :
 
-Tous les scripts sont (actuellement) écrits en python et testés avec la version 3.6.8, sans dépendances à des librairies externes. Ils se présentent sous la forme de programmes à lancer dans un shell en ligne de commande. Ils nécessitent tous un `token` gitlab, pouvant être généré [sur cette page](https://gitedu.hesge.ch/profile/personal_access_tokens), en cochant la case "api". Certains attendent également un `project_id` correspond à celui affiché sur la page de repo :
+```
+requests
+pyyaml
+```
 
-![image](doc/project_id.png)
+Pour rappel, pour ne pas à avoir à installer ces deux dépendances au niveau système, les commandes suivantes génèrent un environnement virtuel :
 
-### create_group.py
 ```bash
-python3 create_group.py <token> <group_name> <visibility>
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
 ```
-Crée un groupe au sens gitlab du terme, nommé `group_name`, avec la visibilité optionnelle `visibility` (`private`, `internal` ou `public`), par défaut privée. Si le groupe existe déjà, ne fait rien. Retourne le `group_id` du groupe créé, nécessaire pour `create_repo_for_students.py` par exemple.
 
-### create_repo_for_students.py
+pwm se présente sous la forme d'une CLI. Il nécessite un `token` gitlab, pouvant être généré [sur cette page](https://gitedu.hesge.ch/profile/personal_access_tokens), en cochant la case "api". Ce `token` peut ensuite être utilisé de trois manières :
+
+1. Écrit dans le fichier `~/.gitedu_token`
+2. Placé dans la variable d'environnement `GITEDU_TOKEN`
+3. Donné en argument de `pwm` avec l'option `-t` ou `--token`
+
+Selon les commandes, un `project_id` ou `group_id` est également nécessaire, il correspond à celui affiché sur la page du groupe / projet :
+
+![image](project_id.png)
+
+L'exécution du programme sans arguments affiche l'aide et le détail pour chaque sous-commande.
+
+### Création d'un groupe et des projets
 ```bash
-python3 create_repo_for_students.py <token> <import_url> <group_id> <project_name> <student-mail1,student-mail2,...,student-mailN> <expires_at>
+python pwm group_repos GROUP_NAME REPOS_FILE [-h] [--visibility VISIBILITY] [-i IMPORT_URL] [-x EXPIRES_AT]
 ```
-Crée un dépôt git (projet) au sein d'un groupe à partir de l'URL d'un projet existant pour une liste d'emails d'étudiants. Détail des arguments :
-- `token` : le token gitlab.
-- `import_url` : l'URL (http) du projet (repository) existant. Ce projet doit être public.
-- `group_id` : l'identifiant du groupe dédié au cours/TP, créé précédemment (avec `create_group.py` par exemple).
-- `project_name` : le nom du nouveau repository à créer pour le ou les étudiants concernés.
-- `student-mail1,student-mail2,...,student-mailN` : une liste d'emails des étudiants. Les emails sont séparés par une virgule. Peut contenir un seul email.
-- `expires_at`: optionnel, au format `AAAA-MM-DD`, supprime les étudiants ajoutés à la date donnée.
-
-### clone_all_repos_in_group.py
+Exécute les opérations de création de groupe et de repositories à partir d'un fichier YAML (voir [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file)). Voir les sous-sections suivantes pour les détails des sous-commandes.
+
+### Création d'un groupe seulement
 ```bash
-python3 clone_all_repos_in_group.py <token> <group_id> <directory> <until_date>
+python pwm group [-h] [--visibility VISIBILITY] GROUP_NAME
 ```
-Clone tous les repositories d'un groupe `group_id` donné dans un répertoire nommé `directory`. Si une date `until_date` (au format `AAAA-MM-DD hh:mm`) est donnée, exécute un `git checkout` sur le premier commit précédant cette date. Affiche sur la sortie standard les membres du groupe, l'url web du repo et dans quel sous-répertoire se trouvent les fichiers.
+Crée un groupe au sens gitlab du terme, nommé `GROUP_NAME`, avec la visibilité optionnelle `VISIBILITY` (`private`, `internal` ou `public`), par défaut privée. Si le groupe existe déjà, ne fait rien. Retourne le `group_id` du groupe créé, nécessaire pour la création des sous-projets par exemple.
 
-### clone_all_forks.py
+### Création d'un sous-projet dans le groupe
 ```bash
-python3 clone_all_forks.py <token> <project_id> <directory> <until_date>
+python pwm repo [-h] [-n NAME] [-i IMPORT_URL] [-x EXPIRES_AT] GROUP_ID EMAILS
 ```
-Clone tous les forks d'un projet `project_id` donné dans un répertoire nommé `directory`. Si une date `until_date` (au format `AAAA-MM-DD hh:mm`) est donnée, exécute un `git checkout` sur le premier commit précédant cette date. Affiche sur la sortie standard les membres du groupe (avec un droit d'accès supérieur à *Reporter*), l'url web du repo et dans quel sous-répertoire se trouvent les fichiers.
+Crée un dépôt git (projet) au sein d'un groupe à partir de l'URL d'un projet existant pour une liste d'emails d'étudiants. Détail des arguments :
+- `NAME` : optionnel, le nom du nouveau repository à créer pour le ou les étudiants concernés. Si non renseigné, prend la première partie du premier email dans `EMAILS`.
+- `IMPORT_URL` : optionnel, l'URL (http) du projet (repository) existant. Ce projet doit être public.
+- `EXPIRES_AT`: optionnel, au format `YYYY-MM-DD`, supprime les étudiants ajoutés à la date donnée (ils ne peuvent plus `push`).
+- `GROUP_ID` : l'identifiant du groupe dédié au cours/TP, créé précédemment.
+- `EMAILS` : une liste d'emails des étudiants. Les emails sont séparés par une virgule. Peut contenir un seul email.
 
-## "Convenient" script
+### Clone de tous les repositories
 ```bash
-./create_group_and_repos.sh <token> <group_name> <import_url> <repos_students>
+python pwm clone [-h] [-g | -f] [-u UNTIL_DATE] ID DIRECTORY
 ```
-Un script bash est également disponible, `create_group_and_repos.sh` qui permet de "batcher" les opérations de création de groupe et de repositories à partir d'un fichier texte `repos_students` formaté ainsi :
+Clone tous les repositories d'un groupe (`-g`) ou tous les forks d'un projet (`-f`) selon l'id (`ID`) donné dans un répertoire nommé `DIRECTORY`. Si une date `UNTIL_DATE` (au format `YYYY-MM-DD hh:mm`) est donnée, exécute un `git checkout` sur le premier commit précédant cette date. Affiche sur la sortie standard les membres du groupe (avec un droit d'accès supérieur à *Reporter*), l'url web du repo et dans quel sous-répertoire se trouvent les fichiers.
+
+## Syntaxe du fichier YAML (REPOS_FILE)
+Le fichier YAML doit respecter une des deux syntaxes suivantes.
+
+### Noms et emails
+Pour chaque projet créé, un nom et une liste d'emails doivent être renseignés :
+
+```yaml
+- name: group1
+  emails:
+  - prenom.nom11@hesge.ch
+  - prenom.nom12@hesge.ch
+- name: group2
+  emails:
+  - prenom.nom21@hesge.ch
+  - prenom.nom22@hesge.ch
+- name: group3
+  emails:
+  - prenom.nom31@hesge.ch
+  - prenom.nom32@hesge.ch
 ```
-repository1;email1,email2
-repository2;email3,email4
+
+### Emails seulement
+Si uniquement les emails sont fournis, prend le premier nom de chaque email pour nom de projet :
+
+```yaml
+- emails:
+  - prenom.nom11@hesge.ch # project_name = prenom.nom11
+  - prenom.nom12@hesge.ch
+- emails:
+  - prenom.nom21@hesge.ch # project_name = prenom.nom21
+  - prenom.nom22@hesge.ch
+- emails:
+  - prenom.nom31@hesge.ch # project_name = prenom.nom31
+  - prenom.nom32@hesge.ch
 ```
diff --git a/create_group_and_repos.sh b/create_group_and_repos.sh
deleted file mode 100755
index 15bc4bba545c305440b397492b461b79370ab27c..0000000000000000000000000000000000000000
--- a/create_group_and_repos.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-
-if [[ $# != 4 ]]; then
-    echo "Usage: $0 <token> <group_name> <import_url> <repos_students>"
-    exit 1
-fi
-
-token=$1
-group_name=$2
-import_url=$3
-repos_students=$4
-
-group=$(scripts/create_group.py $token $group_name)
-group_id=$(echo $group | cut -d';' -f2)
-printf "$group\n\n"
-
-for line in $(cat $repos_students); do
-    project_name=$(echo $line | cut -d';' -f1)
-    students=$(echo $line | cut -d';' -f2)
-    new_repo=$(scripts/create_repo_for_students.py $token $import_url $group_id "$project_name" $students)
-    printf "$new_repo\n\n"
-done
diff --git a/doc/project_id.png b/doc/project_id.png
deleted file mode 100644
index 2671a3fb443acc0b326d14adaf81f5473fb2ea0a..0000000000000000000000000000000000000000
Binary files a/doc/project_id.png and /dev/null differ
diff --git a/project_id.png b/project_id.png
new file mode 100644
index 0000000000000000000000000000000000000000..37a6aa7b7f25ebf52924164b71f87dacbe004c7f
Binary files /dev/null and b/project_id.png differ
diff --git a/pwm b/pwm
index ec1aaf85e4d17d8a2e58cc956aeaf8b9e29cd105..51b0a323ec8feff0893c37bb9779ed430f5003a4 100755
--- a/pwm
+++ b/pwm
@@ -248,7 +248,7 @@ if __name__ == '__main__':
     parser_group_repos.add_argument(
         "group_name", metavar="GROUP_NAME", help="The group name.")
     parser_group_repos.add_argument(
-        "repos_file", metavar="REPOS_FILE", help="A file with projects names and students emails.")
+        "repos_file", metavar="REPOS_FILE", help="YAML file with projects names and/or students emails.")
     parser_group_repos.add_argument(
         "--visibility", help="Group visibility. By default private.")
     parser_group_repos.add_argument("-i", "--import_url",
diff --git a/scripts/clone_all.py b/scripts/clone_all.py
deleted file mode 100755
index f02c74adc97c1799322c75e67568047b20e2a1a6..0000000000000000000000000000000000000000
--- a/scripts/clone_all.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/env python3
-
-import os
-import requests
-import subprocess
-import argparse
-
-parser = argparse.ArgumentParser()
-group = parser.add_mutually_exclusive_group()
-group.add_argument("-g", "--group", action="store_true", help="Clone repositories from a group (with group_id) or forks of a project (with project_id) (default behavior).")
-group.add_argument("-f", "--forks", action="store_true", help="Clone forks of a project (with project_id).")
-parser.add_argument(
-    "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
-parser.add_argument(
-    "id", metavar="ID", help="The group_id (int) of the projects or the project_id (int) of the forks.")
-parser.add_argument(
-    "directory", metavar="DIRECTORY", help="Local directory where clone all repositories.")
-parser.add_argument(
-    "-u", "--until_date", help="Do a git checkout for all repositories at given date, format \"YYYY-MM-DD hh:mm\" (optional).")
-args = parser.parse_args()
-
-try:
-    os.mkdir(args.directory)
-except OSError:
-    print("Creation of the directory '%s' failed, exit\n" % args.directory)
-    exit(1)
-
-base_url = 'https://gitedu.hesge.ch/api/v4/'
-params = {'simple': 'true', 'per_page': 100}
-headers = {'PRIVATE-TOKEN': args.token}
-
-if args.forks:
-    url = base_url + 'projects/' + args.id + '/forks'
-else:
-    url = base_url + 'groups/' + args.id + '/projects'
-
-repositories = requests.get(url, params=params, headers=headers).json()
-if 'message' in repositories:
-    print('Error retrieving repositories: ' + repositories['message'])
-    exit(1)
-
-for repo in repositories:
-    repo_url = base_url + '/projects/' + str(repo['id']) + '/members'
-    members = requests.get(repo_url, headers=headers).json()
-    if 'message' in members:
-        print('Error retrieving members: ' + members['message'])
-        exit(1)
-
-    ssh_url_to_repo = repo['ssh_url_to_repo']
-    web_url = repo['web_url']
-    members_names = ''
-
-    for member in members:
-        if member['access_level'] > 20:  # Access level greater than "Reporter"
-            members_names += member['username'] + ', '
-
-    if args.forks:
-        repo_local_name = repo['namespace']['path']
-    else:
-        repo_local_name = repo['path']
-
-    print('Members: ' + members_names)
-    print('Web url: ' + web_url)
-    print('Cloning in "' + args.directory + '/' + repo_local_name + '"')
-
-    subprocess.run(["git", "clone", "-q", ssh_url_to_repo,
-                    args.directory + '/' + repo_local_name])
-    if args.until_date:
-        commit_id = subprocess.check_output([
-            "git", "rev-list", "-n", "1", "--before=\"" + args.until_date + "\"",
-            "master"], cwd=args.directory + '/' + repo_local_name).decode('utf-8').rstrip()
-        subprocess.run(
-            ["git", "checkout", "-q", str(commit_id)],
-            cwd=args.directory + '/' + repo_local_name)
-        print("Checkout at " + str(commit_id) + "\n")
-    else:
-        print()
diff --git a/scripts/create_group.py b/scripts/create_group.py
deleted file mode 100755
index 209e4c314e4d9df4a09bd7d3af47bee10e1193ec..0000000000000000000000000000000000000000
--- a/scripts/create_group.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import requests
-
-parser = argparse.ArgumentParser()
-parser.add_argument(
-    "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
-parser.add_argument(
-    "group_name", metavar="GROUP_NAME", help="The group name.")
-parser.add_argument(
-    "--visibility", help="Group visibility. By default private.")
-args = parser.parse_args()
-
-if args.visibility:
-    visibility = args.visibility
-else:
-    visibility = 'private'
-
-base_url = 'https://gitedu.hesge.ch/api/v4/'
-params = {'path': args.group_name,
-          'name': args.group_name, 'visibility': visibility}
-headers = {'PRIVATE-TOKEN': args.token}
-
-group = requests.post(base_url + '/groups',
-                      params=params, headers=headers).json()
-if 'message' in group:
-    print('Error in creating group: %s' % group)
-    exit(1)
-
-print("Group '" + group['name'] + "' with id '" + str(group['id']) + "' and visibility '" +
-      group['visibility'] + "' available at '" + group['web_url'] + "' ;" + str(group['id']))
diff --git a/scripts/create_repo_for_students.py b/scripts/create_repo_for_students.py
deleted file mode 100755
index 89480aa4f4299845e0bafd2c4b08596801643693..0000000000000000000000000000000000000000
--- a/scripts/create_repo_for_students.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import requests
-
-parser = argparse.ArgumentParser()
-parser.add_argument(
-    "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
-parser.add_argument(
-    "group_id", metavar="GROUP_ID", help="The group id (int) where to store the created new project.")
-parser.add_argument(
-    "emails", metavar="EMAILS", help="Emails list of students working in this project, separated by commas (email1,email2).")
-parser.add_argument(
-    "-n", "--name", help="The project name. If blank, take the first student name (from email) as name.")
-parser.add_argument("-i", "--import_url",
-                    help="Import the publicly accessible project by URL given here (optional).")
-parser.add_argument("-x", "--expires_at",
-                    help="Expiration date to kick off students from this project, at 00:00:00. YYYY-MM-DD format (optional).")
-args = parser.parse_args()
-
-base_url = 'https://gitedu.hesge.ch/api/v4'
-headers = {'PRIVATE-TOKEN': args.token}
-
-# split '@' in the case when project name = student's email
-if args.name:
-    name = args.name
-else:
-    name = args.emails.split('@')[0]
-
-# Get students ids from their emails
-users_emails = args.emails.split(',')
-user_ids = []
-for email in users_emails:
-    user_requested = requests.get(
-        base_url + '/users', params={'search': email}, headers=headers).json()
-    if len(user_requested) == 0:
-        print('No user %s found, operation aborted' % email)
-        exit(1)
-    user_id = user_requested[0]['id']
-    user_ids.append(user_id)
-
-# Create project from name, import_url (if given) and group_id
-params = {'name': name, 'namespace_id': args.group_id, 'visibility': 'private'}
-if args.import_url:
-    params['import_url'] = args.import_url
-project = requests.post(base_url + '/projects',
-                        params=params, headers=headers).json()
-if 'message' in project:
-    print('Error in creating project: %s' % project)
-    exit(1)
-print("Project '" + project['name'] + "' at '" +
-      project['web_url'] + "' created")
-
-# Allow users with developer access level to push and merge on master
-access_level = 30
-params = {'name': 'master', 'push_access_level': str(
-    access_level), 'merge_access_level': str(access_level)}
-requests.post(base_url + '/projects/' +
-              str(project['id']) + '/protected_branches', params=params, headers=headers).json()
-
-# Add each student as project's developer (level 30)
-for user_id in user_ids:
-    params = {'user_id': user_id, 'access_level': access_level}
-    if args.expires_at:
-        params['expires_at'] = args.expires_at
-    new_user = requests.post(base_url + '/projects/' + str(
-        project['id']) + '/members', params=params, headers=headers).json()
-    if 'message' in new_user:
-        print('Error in adding user: %s' % new_user)
-    else:
-        out = ("Adding '" + new_user['name'] + "' (" + new_user['username'] + ") in '"
-               + project['name'] + "' with access level: " + str(new_user['access_level']))
-        if args.expires_at:
-            out += ", expires at: " + new_user['expires_at']
-        print(out)
-
-# Do not forget : students have to add second remote in their local repositories for pulling last changes.