Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/).

## [Non publié]

## [0.1.62] - 2026-08-24

### Corrigé

- **Le premier `Tab` d'une session ne proposait rien**, le second fonctionnait.
zsh charge le fichier `#compdef` à la première tabulation et attend qu'il
produise les propositions **de cette invocation-là** ; le script que typer
génère se contente de définir la fonction, puis de l'enregistrer pour la
suite. Un `Tab` muet se lit comme « la complétion ne marche pas », et personne
ne rappuie une seconde fois pour vérifier une fonctionnalité qu'il croit
absente : le coût est un abandon silencieux, pas une gêne. Le script installé
appelle désormais sa fonction après l'avoir enregistrée, et la raison de cette
divergence avec l'amont est écrite **dans le fichier posé**, pour que personne
ne la retire un jour sans savoir pourquoi elle existe. Reproduit et vérifié
dans un zsh réel sous pseudo-terminal, avant et après : un appel direct au
mécanisme de complétion ne traverse pas la couche en cause.

### Ajouté

- **`dsoxlab completion install` et `dsoxlab completion show`.** Le premier
installe l'auto-complétion, le second imprime le script sans rien écrire, pour
qui veut le poser lui-même.

### Déprécié

- **`dsoxlab install` est déprécié, et sera retiré en 0.3.0.** C'était le premier
nom de commande que voyait un utilisateur dans l'aide, et il promettait
d'installer l'outil, déjà installé. Il continue de faire ce que fait
`completion install`, en le signalant.

Il **n'écrit plus de wrapper** dans `~/.local/bin`. Deux défauts vécus tenaient
à ce fichier : un chemin contenant une espace cassait le `exec` faute de
quoting, et surtout `write_text()` sur un lien symbolique écrit dans **la
cible**, donc le binaire réel d'`uv` était remplacé par un script pointant sur
lui-même. `uv tool install` et `pipx` posent déjà leur lanceur exactement là :
le remplacer ne faisait que défaire ce que leur prochaine mise à jour
remettrait.

## [0.1.61] - 2026-08-24

### Ajouté
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.62] - 2026-08-24

### Fixed

- **The first `Tab` of a session proposed nothing**, the second one worked. zsh
loads the `#compdef` file on the first tab and expects it to produce the
completions **for that very invocation**; the script typer generates only
defines the function and registers it for later. A silent `Tab` reads as
"completion does not work", and nobody presses a second time to check a
feature they believe is missing: the cost is a silent give-up, not an
annoyance. The installed script now calls its function after registering it,
and the reason for that divergence from upstream is written **inside the file
it drops**, so nobody removes it later without knowing why it is there.
Reproduced and verified in a real zsh under a pseudo-terminal, before and
after: calling the completion mechanism directly does not cross the layer at
fault.

### Added

- **`dsoxlab completion install` and `dsoxlab completion show`.** The first
installs shell completion, the second prints the script without writing
anything, for those who would rather place it themselves.

### Deprecated

- **`dsoxlab install` is deprecated and will be removed in 0.3.0.** It was the
first command name a user saw in the help, and it promised to install the
tool, which is already installed. It still does what `completion install`
does, and says so.

It **no longer writes a wrapper** in `~/.local/bin`. Two real defects came
from that file: a path containing a space broke the `exec` for lack of
quoting, and above all `write_text()` on a symlink writes into **the target**,
so uv's real binary was replaced by a script pointing at itself. `uv tool
install` and `pipx` already put their launcher exactly there: replacing it
only undid what their next update would restore.

## [0.1.61] - 2026-08-24

### Added
Expand Down
4 changes: 3 additions & 1 deletion README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,16 @@ test référencés sont présents.
| `dsoxlab challenge` | Affiche la mission du challenge (challenge/README.md). |
| `dsoxlab check` | Exécute les tests, calcule le score (hints déduits) et enregistre le résultat. |
| `dsoxlab clean` | Supprime toutes les ressources créées par le lab. |
| `dsoxlab completion install` | Installe l'auto-complétion pour le shell courant (zsh, bash). |
| `dsoxlab completion show` | Imprime le script de complétion sur la sortie standard, sans rien écrire. |
| `dsoxlab course` | Affiche une section du cours, ou le sommaire si aucune section n'est précisée. |
| `dsoxlab demo` | Installe un catalogue de démonstration et joue un premier lab, sans rien cloner ni provisionner. |
| `dsoxlab destroy` | Détruit l'infrastructure du lab (terraform destroy), machines restées hors du state comprises. |
| `dsoxlab doctor` | Diagnostique l'environnement (runtimes, outils, labs détectés). |
| `dsoxlab fullhelp` | Affiche le guide complet de la plateforme (concepts, workflow, commandes). |
| `dsoxlab guide` | Ouvre le guide en ligne du lab dans le navigateur. |
| `dsoxlab hint` | Affiche le prochain indice du challenge (déduit des points au score final). |
| `dsoxlab install` | Installe le wrapper dsoxlab dans ~/.local/bin et l'auto-complétion shell. |
| `dsoxlab install` | Déprécié : utilise « dsoxlab completion install ». Installe l'auto-complétion. |
| `dsoxlab instructor bootstrap` | Génère la clé SSH du lab (si absente) et vérifie que terraform/ansible-runner sont installés. |
| `dsoxlab list-labs` | Liste tous les labs disponibles (filtrés par contexte actif si défini). |
| `dsoxlab next` | Recommande le prochain lab ou challenge à compléter dans le contexte actif. |
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,16 @@ scripts and test files are present.
| `dsoxlab challenge` | Display the challenge mission for this lab (challenge/README.md). |
| `dsoxlab check` | Run tests, calculate score (hints deducted) and record result. |
| `dsoxlab clean` | Remove all resources created by the lab. |
| `dsoxlab completion install` | Install completion for the current shell (zsh, bash). |
| `dsoxlab completion show` | Print the completion script on stdout, writing nothing. |
| `dsoxlab course` | Display a course section, or the table of contents if no section is given. |
| `dsoxlab demo` | Install a demonstration catalog and play a first lab, with nothing to clone and nothing to provision. |
| `dsoxlab destroy` | Destroy the lab infrastructure (terraform destroy), including machines left outside the state. |
| `dsoxlab doctor` | Diagnose the environment (runtimes, tools, detected labs). |
| `dsoxlab fullhelp` | Show the complete platform guide (concepts, workflow, commands). |
| `dsoxlab guide` | Open the lab's online guide in your web browser. |
| `dsoxlab hint` | Show the next challenge hint (deducts points from final score). |
| `dsoxlab install` | Install the dsoxlab wrapper in ~/.local/bin and shell auto-completion. |
| `dsoxlab install` | Deprecated: use `dsoxlab completion install`. Installs shell completion. |
| `dsoxlab instructor bootstrap` | Generate the lab SSH key (if missing) and check that terraform/ansible-runner are installed. |
| `dsoxlab list-labs` | List all available labs (filtered by active context if set). |
| `dsoxlab next` | Recommend the next lab or challenge to complete in the active context. |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "dsoxlab"
version = "0.1.61"
version = "0.1.62"
description = "Turn declarative exercises into reproducible, runnable and verifiable lab environments"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
109 changes: 74 additions & 35 deletions src/dsoxlab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import atexit
import logging
import os
import shlex
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -201,6 +200,17 @@ def invoke(self, ctx: Any) -> Any:
)
app.add_typer(instructor_app, name="instructor")

# ── Sous-application 'completion' ─────────────────────────────────────────────

completion_app = typer.Typer(
name="completion",
help=_("cmd_completion_help"),
no_args_is_help=True,
rich_markup_mode="rich",
cls=_I18nGroup,
)
app.add_typer(completion_app, name="completion")

# ── Option globale lab-home ───────────────────────────────────────────────────

LabHomeOption = Annotated[
Expand Down Expand Up @@ -585,35 +595,38 @@ def _notify_update_available() -> None:
_COMPLETE_VAR = f"_{_PROG_NAME.replace('-', '_').upper()}_COMPLETE"


@app.command("install", help=_("cmd_install_help"))
def install() -> None:
"""Install the dsoxlab wrapper in ~/.local/bin and shell completion."""
#: Ce que dsoxlab ajoute au script que typer génère pour zsh, et pourquoi.
#:
#: Le commentaire part DANS le fichier installé, à dessein : sans lui, la ligne
#: ressemble à une scorie qu'un lecteur pressé retirerait, et le défaut
#: reviendrait sans que personne ne comprenne pourquoi.
_ZSH_PREMIER_TAB = """
# ── ajouté par dsoxlab, et pas par typer ──────────────────────────────────────
# zsh autoload ce fichier au PREMIER Tab, et attend qu'il produise les
# propositions de cette invocation-là. Le script amont se contente de définir la
# fonction puis de l'enregistrer pour la suite : la première tabulation ne rend
# donc rien, et la seconde fonctionne. Un Tab muet se lit comme « la complétion
# ne marche pas », et personne ne rappuie pour vérifier.
# Ne pas retirer cette ligne sans rejouer le cas dans un zsh réel.
_dsoxlab_completion "$@"
"""


def _script_completion(shell: str) -> str:
"""Le script de complétion pour ``shell``, corrigé pour zsh (#134)."""
from typer.completion import get_completion_script

# ── 1. Wrapper script in ~/.local/bin ─────────────────────────────────────
local_bin = Path.home() / ".local" / "bin"
local_bin.mkdir(parents=True, exist_ok=True)
script = get_completion_script(
prog_name=_PROG_NAME, complete_var=_COMPLETE_VAR, shell=shell
)
# bash source son script au démarrage du shell, fish le charge par fichier
# de complétion : ni l'un ni l'autre ne passe par l'autoload qui pose
# problème. La divergence ne vaut donc que pour zsh.
return script + _ZSH_PREMIER_TAB if shell == "zsh" else script

venv_binary = Path(sys.argv[0]).resolve()
wrapper = local_bin / "dsoxlab"

# Ne pas écraser un lanceur qui mène déjà à ce binaire. `uv tool install` et
# `pipx` en posent un exactement ici : le remplacer par un script shell ne
# fait que défaire ce que leur prochaine mise à jour remettra. Surtout, si ce
# lanceur EST le fichier qu'on vient de résoudre (cas d'un vrai fichier
# plutôt que d'un lien), le wrapper s'exécuterait lui-même, en boucle.
if wrapper.exists() and wrapper.resolve() == venv_binary:
info(_("install_wrapper_deja", path=str(wrapper)))
else:
# shlex.quote : un chemin d'installation contenant une espace
# (« /home/moi/My Tools/… ») produisait un `exec` découpé en plusieurs
# arguments, donc un wrapper qui échouait sur « not found ».
cible = shlex.quote(str(venv_binary))
wrapper.write_text(f'#!/bin/sh\nexec {cible} "$@"\n')
wrapper.chmod(0o755)
success(_("install_wrapper", path=str(wrapper), source=str(venv_binary)))

# ── 2. Shell completion ────────────────────────────────────────────────────
def _installer_completion() -> None:
"""Pose le script de complétion du shell courant, et raccorde son rc."""
shell_name = Path(os.environ.get("SHELL", "bash")).name

if shell_name == "zsh":
Expand All @@ -622,10 +635,7 @@ def install() -> None:
# Le nom du fichier compte : zsh autoload la fonction `_dsoxlab` pour
# compléter `dsoxlab`, et cherche donc un fichier de ce nom exact.
comp_file = zfunc_dir / f"_{_PROG_NAME}"
script = get_completion_script( # noqa: S604 — `shell` = nom du shell Typer ("zsh"), pas un subprocess shell=True
prog_name=_PROG_NAME, complete_var=_COMPLETE_VAR, shell="zsh"
)
comp_file.write_text(script)
comp_file.write_text(_script_completion("zsh"))
success(_("install_completion", path=str(comp_file)))

zshrc = Path.home() / ".zshrc"
Expand All @@ -644,10 +654,7 @@ def install() -> None:
bash_comp_dir = Path.home() / ".bash_completion.d"
bash_comp_dir.mkdir(exist_ok=True)
comp_file = bash_comp_dir / "dsoxlab"
script = get_completion_script( # noqa: S604 — `shell` = nom du shell Typer ("bash"), pas un subprocess shell=True
prog_name=_PROG_NAME, complete_var=_COMPLETE_VAR, shell="bash"
)
comp_file.write_text(script)
comp_file.write_text(_script_completion("bash"))
success(_("install_completion", path=str(comp_file)))

bashrc = Path.home() / ".bashrc"
Expand All @@ -660,7 +667,39 @@ def install() -> None:

else:
info(_("install_completion_unsupported", shell=shell_name))
info(_("install_reload"))
info(_("install_reload"))


@completion_app.command("install", help=_("cmd_completion_install_help"))
def completion_install() -> None:
_installer_completion()


@completion_app.command("show", help=_("cmd_completion_show_help"))
def completion_show(
shell: Annotated[str | None, typer.Option("--shell", help=_("opt_completion_shell"))] = None,
) -> None:
"""Imprime le script, sans rien écrire : à rediriger où l'on veut."""
nom = shell or Path(os.environ.get("SHELL", "bash")).name
if nom not in ("zsh", "bash", "fish"):
error(_("install_completion_unsupported", shell=nom))
raise typer.Exit(2)
# `print` et non `info` : c'est une sortie destinée à être redirigée, elle
# ne doit porter ni couleur ni encadrement.
print(_script_completion(nom))


@app.command("install", help=_("cmd_install_help"))
def install() -> None:
"""Déprécié depuis 0.1.62, retiré en 0.3.0 : voir `completion install`.

Le nom promettait d'installer l'outil, déjà installé. La commande posait en
plus un wrapper dans ``~/.local/bin``, exactement où ``uv tool install`` et
``pipx`` posent le leur : le remplacer ne faisait que défaire ce que leur
prochaine mise à jour remettrait. Il n'est donc plus écrit du tout.
"""
warn(_("install_deprecie"))
_installer_completion()


# ── use ──────────────────────────────────────────────────────────────────────
Expand Down
34 changes: 31 additions & 3 deletions src/dsoxlab/i18n/strings/en.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"Say what the engine is doing, on standard error. Repeatable: -v for information, -vv for full detail.",
"opt_debug": "Same as -vv. The full log is written to ~/.local/state/dsoxlab/dsoxlab.log either way.",
"opt_version_help": "Show the dsoxlab version and exit.",
"cmd_install_help": "Install the dsoxlab wrapper in ~/.local/bin and shell auto-completion.",
"cmd_install_help": "Deprecated: use `dsoxlab completion install`. Installs shell completion.",
"cmd_demo_help":
"Install a demonstration catalog and play a first lab, with nothing to "
"clone and nothing to provision.",
Expand Down Expand Up @@ -402,8 +402,13 @@
Exits non-zero if any of them remains.
[dim]--yes[/dim] Do not ask for confirmation, orphan machines included.

[cyan]install[/cyan] Install dsoxlab in [bold]~/.local/bin[/bold] + shell auto-completion.
Supports bash and zsh. Reload your shell after running.
[cyan]completion install[/cyan] Install shell auto-completion (bash, zsh).
Reload your shell afterwards: [bold]exec $SHELL[/bold]
[cyan]completion show[/cyan] Print the script on stdout, writing nothing.
[dim]--shell <name>[/dim] zsh, bash or fish. Default: the current shell.

[cyan]install[/cyan] [bold]Deprecated[/bold] since 0.1.62, removed in 0.3.0.
Does what [bold]completion install[/bold] does, and warns.

[cyan]support[/cyan] Diagnostic report to paste into an issue:
versions, tools, catalog, latest traces. Anonymised by
Expand Down Expand Up @@ -1061,4 +1066,27 @@
# ── #132 : un « 0 lab » muet oblige à chercher ailleurs ──
"detail_labs_ecart":
"{ecart} of the {presents} lab.yaml files on disk could not be loaded. `dsoxlab list-labs` names them and says why.",

# ── #90 / #134 : la complétion sous un nom qui la nomme ──
"cmd_completion_help":
"Install or print the shell completion script.",

# ── #90 / #134 : la complétion sous un nom qui la nomme ──
"cmd_completion_install_help":
"Install completion for the current shell (zsh, bash).",

# ── #90 / #134 : la complétion sous un nom qui la nomme ──
"cmd_completion_show_help":
"Print the completion script on stdout, writing nothing.",

# ── #90 / #134 : la complétion sous un nom qui la nomme ──
"opt_completion_shell":
"Shell to generate for (zsh, bash, fish). Default: the current shell.",

# ── #90 / #134 : la complétion sous un nom qui la nomme ──
"install_deprecie":
"`dsoxlab install` is deprecated since 0.1.62 and will be removed in "
"0.3.0. Use `dsoxlab completion install`, which does the same thing "
"under a name that says it. The wrapper in ~/.local/bin is no longer "
"written: `uv tool install` and `pipx` already put theirs there.",
}
Loading
Loading