diff --git a/CHANGES.md b/CHANGES.md
index e65c7a6..5332e13 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,31 @@
+## Release 0.4.0p74 du 14/03/2025-15:23
+
+* `cf9fab5` maj src/php
+* `5c5d878` pdev: option --force-merge
+* `9723c14` ajout cl::same_keys()
+* `5ae3e8b` pdev: ajout --after-merge
+* `cfc386d` Revert "prel/pdev: tracer les hooks"
+* `d44537a` prel/pdev: tracer les hooks
+* `e8abaca` supprimer tests qui sont encore dans nur/ture
+* `a3116e5` ajout du schéma
+* `0d2ca20` prel/pdev: option --fake
+* `bd0da9c` prel/pdev: ajouter les hook BEFORE_*
+* `7e05caf` runphp: passer les arguments inchangés à composer
+* `9def939` p: support des projets dépendants composer
+* `01c65a6` tests verbosity et interaction
+* `8e3569a` ne plus utiliser tooenc par défaut. renommer tooenc en uecho
+* `ead5b5a` support automatique options verbosity et interaction
+* `8825493` pman: tenir compte des branches distantes
+* `e129e0a` ajout pwip
+* `9a2378b` pman: améliorer l'ergonomie
+* `8cfd803` gestion des profils composer
+* `d9989c6` supprimer composer.phar puisque c'est fourni par runphp
+* `7eb5efb` ajout config .pman.yml
+* `92363cd` pman: option --force-create
+* `3b379eb` maj doc
+* `bbb5559` pman: ajout init pman
+* `939f772` pman: support des config standard nommées
+
 ## Release 0.3.4p82 du 01/03/2025-06:23
 
 ## Release 0.3.4p74 du 01/03/2025-06:22
diff --git a/VERSION.txt b/VERSION.txt
index 42045ac..1d0ba9e 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-0.3.4
+0.4.0
diff --git a/bash/src/_output_color.sh b/bash/src/_output_color.sh
index ce0dd77..afe6428 100644
--- a/bash/src/_output_color.sh
+++ b/bash/src/_output_color.sh
@@ -6,15 +6,15 @@ function __esection() {
     local length="${COLUMNS:-80}"
     setx lsep=__complete "$prefix" "$length" -
 
-    tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
+    recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
     [ -n "$*" ] || return 0
     length=$((length - 1))
     setx -a lines=echo "$1"
     for line in "${lines[@]}"; do
         setx line=__complete "$prefix- $line" "$length"
-        tooenc "$COULEUR_BLEUE$line-$COULEUR_NORMALE"
+        recho "$COULEUR_BLEUE$line-$COULEUR_NORMALE"
     done
-    tooenc "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
+    recho "$COULEUR_BLEUE$lsep$COULEUR_NORMALE"
 }
 function __etitle() {
     local -a lines; local maxlen=0
@@ -23,10 +23,10 @@ function __etitle() {
     setx -a lines=echo "$1"
     for line in "${lines[@]}"; do
         [ ${#line} -gt $maxlen ] && maxlen=${#line}
-        tooenc "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE"
+        recho "${prefix}${COULEUR_BLEUE}T $line$COULEUR_NORMALE"
     done
     maxlen=$((maxlen + 2))
-    tooenc "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}"
+    recho "${prefix}${COULEUR_BLEUE}T$(__complete "" $maxlen -)${COULEUR_NORMALE}"
 }
 function __edesc() {
     local -a lines
@@ -34,7 +34,7 @@ function __edesc() {
     
     setx -a lines=echo "$1"
     for line in "${lines[@]}"; do
-        tooenc "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line"
+        recho "${prefix}${COULEUR_BLEUE}>${COULEUR_NORMALE} $line"
     done
 }
 function __ebanner() {
@@ -43,35 +43,35 @@ function __ebanner() {
     local length="${COLUMNS:-80}"
     setx lsep=__complete "$prefix" "$length" =
 
-    tooenc "$COULEUR_ROUGE$lsep"
+    recho "$COULEUR_ROUGE$lsep"
     length=$((length - 1))
     setx -a lines=echo "$1"
     for line in "" "${lines[@]}" ""; do
         setx line=__complete "$prefix= $line" "$length"
-        tooenc "$line="
+        recho "$line="
     done
-    tooenc "$lsep$COULEUR_NORMALE"
+    recho "$lsep$COULEUR_NORMALE"
 }
-function __eimportant() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __eattention() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __eerror() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __ewarn() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __enote() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __einfo() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __edebug() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __eimportant() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}!${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __eattention() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}*${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __eerror() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}E${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __ewarn() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}W${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __enote() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}N${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __einfo() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}I${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __edebug() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}D${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
 
-function __estep() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepe() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepw() { tooenc "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepn() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepi() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estep_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepe_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepw_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepn_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __estepi_() { tooenc_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estep() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepe() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepw() { recho "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepn() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepi() { recho "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estep_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepe_() { recho_ "$(__edate)$(__eindent0)${COULEUR_ROUGE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepw_() { recho_ "$(__edate)$(__eindent0)${COULEUR_JAUNE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepn_() { recho_ "$(__edate)$(__eindent0)${COULEUR_VERTE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __estepi_() { recho_ "$(__edate)$(__eindent0)${COULEUR_BLEUE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
 
-function __action() { tooenc "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __asuccess() { tooenc "$(__edate)$(__eindent0)${COULEUR_VERTE}✔${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __afailure() { tooenc "$(__edate)$(__eindent0)${COULEUR_ROUGE}✘${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
-function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
+function __action() { recho "$(__edate)$(__eindent0)${COULEUR_BLANCHE}.${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __asuccess() { recho "$(__edate)$(__eindent0)${COULEUR_VERTE}✔${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __afailure() { recho "$(__edate)$(__eindent0)${COULEUR_ROUGE}✘${COULEUR_NORMALE} $(__eindent "$1" "  ")"; }
+function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }
diff --git a/bash/src/_output_vanilla.sh b/bash/src/_output_vanilla.sh
index c37509d..cbd466f 100644
--- a/bash/src/_output_vanilla.sh
+++ b/bash/src/_output_vanilla.sh
@@ -6,23 +6,23 @@ function __esection() {
     local length="${COLUMNS:-80}"
     setx lsep=__complete "$prefix" "$length" -
 
-    tooenc "$lsep"
+    recho "$lsep"
     [ -n "$*" ] || return 0
     length=$((length - 1))
     setx -a lines=echo "$1"
     for line in "${lines[@]}"; do
         setx line=__complete "$prefix- $line" "$length"
-        tooenc "$line-"
+        recho "$line-"
     done
-    tooenc "$lsep"
+    recho "$lsep"
 }
 function __etitle() {
     local p="TITLE: " i="       "
-    tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
+    recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
 }
 function __edesc() {
     local p="DESC: " i="      "
-    tooenc "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
+    recho "$(__edate)$(__eindent0)${p}$(__eindent "$1" "$i")"
 }
 function __ebanner() {
     local -a lines
@@ -30,37 +30,37 @@ function __ebanner() {
     local length="${COLUMNS:-80}"
     setx lsep=__complete "$prefix" "$length" =
 
-    tooenc "$lsep"
+    recho "$lsep"
     length=$((length - 1))
     setx -a lines=echo "$1"
     for line in "" "${lines[@]}" ""; do
         setx line=__complete "$prefix= $line" "$length"
-        tooenc "$line="
+        recho "$line="
     done
-    tooenc "$lsep"
+    recho "$lsep"
 }
-function __eimportant() { tooenc "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" "           ")"; }
-function __eattention() { tooenc "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" "           ")"; }
-function __eerror() { tooenc "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" "       ")"; }
-function __ewarn() { tooenc "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" "         ")"; }
-function __enote() { tooenc "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" "      ")"; }
-function __einfo() { tooenc "$(__edate)$(__eindent0)INFO: $(__eindent "$1" "      ")"; }
-function __edebug() { tooenc "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" "       ")"; }
-function __eecho() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
-function __eecho_() { tooenc_ "$(__edate)$(__eindent0)$(__eindent "$1")"; }
+function __eimportant() { recho "$(__edate)$(__eindent0)IMPORTANT! $(__eindent "$1" "           ")"; }
+function __eattention() { recho "$(__edate)$(__eindent0)ATTENTION! $(__eindent "$1" "           ")"; }
+function __eerror() { recho "$(__edate)$(__eindent0)ERROR: $(__eindent "$1" "       ")"; }
+function __ewarn() { recho "$(__edate)$(__eindent0)WARNING: $(__eindent "$1" "         ")"; }
+function __enote() { recho "$(__edate)$(__eindent0)NOTE: $(__eindent "$1" "      ")"; }
+function __einfo() { recho "$(__edate)$(__eindent0)INFO: $(__eindent "$1" "      ")"; }
+function __edebug() { recho "$(__edate)$(__eindent0)DEBUG: $(__eindent "$1" "       ")"; }
+function __eecho() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }
+function __eecho_() { recho_ "$(__edate)$(__eindent0)$(__eindent "$1")"; }
 
-function __estep() { tooenc  "$(__edate)$(__eindent0). $(__eindent "$1" "  ")"; }
-function __estepe() { tooenc  "$(__edate)$(__eindent0).E $(__eindent "$1" "   ")"; }
-function __estepw() { tooenc  "$(__edate)$(__eindent0).W $(__eindent "$1" "   ")"; }
-function __estepn() { tooenc  "$(__edate)$(__eindent0).N $(__eindent "$1" "   ")"; }
-function __estepi() { tooenc  "$(__edate)$(__eindent0).I $(__eindent "$1" "   ")"; }
-function __estep_() { tooenc_ "$(__edate)$(__eindent0). $(__eindent "$1" "  ")"; }
-function __estepe_() { tooenc_ "$(__edate)$(__eindent0).E $(__eindent "$1" "   ")"; }
-function __estepw_() { tooenc_ "$(__edate)$(__eindent0).W $(__eindent "$1" "   ")"; }
-function __estepn_() { tooenc_ "$(__edate)$(__eindent0).N $(__eindent "$1" "   ")"; }
-function __estepi_() { tooenc_ "$(__edate)$(__eindent0).I $(__eindent "$1" "   ")"; }
+function __estep() { recho  "$(__edate)$(__eindent0). $(__eindent "$1" "  ")"; }
+function __estepe() { recho  "$(__edate)$(__eindent0).E $(__eindent "$1" "   ")"; }
+function __estepw() { recho  "$(__edate)$(__eindent0).W $(__eindent "$1" "   ")"; }
+function __estepn() { recho  "$(__edate)$(__eindent0).N $(__eindent "$1" "   ")"; }
+function __estepi() { recho  "$(__edate)$(__eindent0).I $(__eindent "$1" "   ")"; }
+function __estep_() { recho_ "$(__edate)$(__eindent0). $(__eindent "$1" "  ")"; }
+function __estepe_() { recho_ "$(__edate)$(__eindent0).E $(__eindent "$1" "   ")"; }
+function __estepw_() { recho_ "$(__edate)$(__eindent0).W $(__eindent "$1" "   ")"; }
+function __estepn_() { recho_ "$(__edate)$(__eindent0).N $(__eindent "$1" "   ")"; }
+function __estepi_() { recho_ "$(__edate)$(__eindent0).I $(__eindent "$1" "   ")"; }
 
-function __action() { tooenc "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" "        ")"; }
-function __asuccess() { tooenc "$(__edate)$(__eindent0)(OK) $(__eindent "$1" "     ")"; }
-function __afailure() { tooenc "$(__edate)$(__eindent0)(KO) $(__eindent "$1" "     ")"; }
-function __adone() { tooenc "$(__edate)$(__eindent0)$(__eindent "$1")"; }
+function __action() { recho "$(__edate)$(__eindent0)ACTION: $(__eindent "$1" "        ")"; }
+function __asuccess() { recho "$(__edate)$(__eindent0)(OK) $(__eindent "$1" "     ")"; }
+function __afailure() { recho "$(__edate)$(__eindent0)(KO) $(__eindent "$1" "     ")"; }
+function __adone() { recho "$(__edate)$(__eindent0)$(__eindent "$1")"; }
diff --git a/bash/src/base.args.sh b/bash/src/base.args.sh
index 393a7c5..43e63ae 100644
--- a/bash/src/base.args.sh
+++ b/bash/src/base.args.sh
@@ -124,8 +124,8 @@ optdesc
   commence par ++, c'est une option avancée qui n'est pas affichée par défaut."
 function parse_args() {
     eval "$NULIB__DISABLE_SET_X"
+    [ -n "$NULIB_ARGS_ONERROR_RETURN" ] && set_die_return
     local __r=
-    local __DIE='[ -n "$NULIB_ARGS_ONERROR_RETURN" ] && return 1 || die'
 
     if ! is_array args; then
         eerror "Invalid args definition: args must be defined"
@@ -145,17 +145,33 @@ function parse_args() {
         __r=1
     else
         __DEFS=("$@")
-        __parse_args || __r=1
+        __nulib_args_parse_args || __r=1
     fi
     eval "$NULIB__ENABLE_SET_X"
     if [ -n "$__r" ]; then
-        eval "$__DIE"
+        die || return
     fi
 }
-function __parse_args() {
+function __nulib_args_add_sopt() {
+    local noauto="$1"; shift
+    local def
+    for def in "$@"; do
+        array_contains "$noauto" "$def" || __sopts="$__sopts$def"
+    done
+}
+function __nulib_args_add_lopt() {
+    local noauto="$1"; shift
+    local def
+    for def in "$@"; do
+        array_contains "$noauto" "$def" || __lopts="$__lopts${__lopts:+,}$def"
+    done
+}
+function __nulib_args_parse_args() {
     ## tout d'abord, construire la liste des options
     local __AUTOH=1 __AUTOHELP=1 # faut-il gérer automatiquement l'affichage de l'aide?
-    local __AUTOD=1 __AUTODEBUG=1 # faut-il rajouter les options -D et --debug
+    local -a __NOAUTOL __NOAUTOLOGTO # faut-il rajouter les options pour gérer la journalisation?
+    local -a __NOAUTOV __NOAUTOVERBOSITY # options de verbosité qui ont été définies par l'utilisateur
+    local -a __NOAUTOI __NOAUTOINTERACTION # options d'interaction qui ont été définies par l'utilisateur
     local __ADVHELP # y a-t-il des options avancées?
     local __popt __sopts __lopts
     local -a __defs
@@ -165,10 +181,10 @@ function __parse_args() {
         +) __popt="$1"; shift; continue;;
         -) __popt="$1"; shift; continue;;
         -*) IFS=, read -a __defs <<<"$1"; shift;;
-        *) eerror "Invalid arg definition: expected option, got '$1'"; eval "$__DIE";;
+        *) die "Invalid arg definition: expected option, got '$1'" || return;;
         esac
         # est-ce que l'option prend un argument?
-        local __def __witharg __valdesc
+        local __def __longdef __witharg __valdesc
         __witharg=
         for __def in "${__defs[@]}"; do
             if [ "${__def%::*}" != "$__def" ]; then
@@ -180,23 +196,36 @@ function __parse_args() {
         # définitions __sopts et __lopts
         for __def in "${__defs[@]}"; do
             __def="${__def%%:*}"
+            __longdef=
             if [[ "$__def" == --* ]]; then
                 # --longopt
                 __def="${__def#--}"
                 __lopts="$__lopts${__lopts:+,}$__def$__witharg"
+                __longdef=1
             elif [[ "$__def" == -* ]] && [ ${#__def} -eq 2 ]; then
                 # -o
                 __def="${__def#-}"
                 __sopts="$__sopts$__def$__witharg"
-                [ "$__def" == h ] && __AUTOH=
-                [ "$__def" == D ] && __AUTOD=
+                case "$__def" in
+                h) __AUTOH=;;
+                L) __NOAUTOL+=("-$__def");;
+                Q|q|v|D) __NOAUTOV+=("-$__def");;
+                b|y|i) __NOAUTOI+=("-$__def");;
+                esac
             else
                 # -longopt ou longopt
                 __def="${__def#-}"
                 __lopts="$__lopts${__lopts:+,}$__def$__witharg"
+                __longdef=1
+            fi
+            if [ -n "$__longdef" ]; then
+                case "$__def" in
+                help|help++) __AUTOHELP=;;
+                log-to) __NOAUTOLOGTO+=("--$__def");;
+                very-quiet|quiet|verbose|debug) __NOAUTOVERBOSITY+=("--$__def");;
+                batch|automatic|interactive) __NOAUTOINTERACTION+=("--$__def");;
+                esac
             fi
-            [ "$__def" == help -o "$__def" == help++ ] && __AUTOHELP=
-            [ "$__def" == debug ] && __AUTODEBUG=
         done
         # sauter l'action
         shift
@@ -209,8 +238,12 @@ function __parse_args() {
 
     [ -n "$__AUTOH" ] && __sopts="${__sopts}h"
     [ -n "$__AUTOHELP" ] && __lopts="$__lopts${__lopts:+,}help,help++"
-    [ -n "$__AUTOD" ] && __sopts="${__sopts}D"
-    [ -n "$__AUTODEBUG" ] && __lopts="$__lopts${__lopts:+,}debug"
+    __nulib_args_add_sopt __NOAUTOL L
+    __nulib_args_add_lopt __NOAUTOLOGTO log-to
+    __nulib_args_add_sopt __NOAUTOV Q q v D
+    __nulib_args_add_lopt __NOAUTOVERBOSITY very-quiet quiet verbose debug
+    __nulib_args_add_sopt __NOAUTOI b y i
+    __nulib_args_add_lopt __NOAUTOINTERACTION batch automatic interactive
 
     __sopts="$__popt$__sopts"
     local -a __getopt_args
@@ -222,7 +255,7 @@ function __parse_args() {
     else
         # relancer pour avoir le message d'erreur
         LANG=C getopt "${__getopt_args[@]}" 2>&1 1>/dev/null
-        eval "$__DIE"
+        die || return
     fi
 
     ## puis traiter les options
@@ -373,7 +406,7 @@ $prefix$usage"
             fi
             [[ "$1" == -* ]] || break
             option_="$1"; shift
-            __parse_opt "$option_"
+            __nulib_args_parse_opt "$option_"
             if [ -n "$__witharg" ]; then
                 # l'option prend un argument
                 value_="$1"; shift
@@ -387,7 +420,7 @@ $prefix$usage"
     unset -f inc@ res@ add@ set@ showhelp@
     args=("$@")
 }
-function __parse_opt() {
+function __nulib_args_parse_opt() {
     # $1 est l'option spécifiée
     local option_="$1"
     set -- "${__DEFS[@]}"
@@ -460,27 +493,62 @@ function __parse_opt() {
 
         [ -n "$__found" ] && return 0
     done
-    if [ -n "$__AUTOH" -a "$option_" == -h ]; then
-        __action="showhelp@"
-        return 0
-    fi
-    if [ -n "$__AUTOHELP" ]; then
-        if [ "$option_" == --help ]; then
-            __action="showhelp@"
-            return 0
-        elif [ "$option_" == --help++ ]; then
-            __action="showhelp@ ++"
+    case "$option_" in
+    -h)
+        if [ -n "$__AUTOH" ]; then
+            __action='showhelp@'
             return 0
         fi
-    fi
-    if [ -n "$__AUTOD" -a "$option_" == -D ]; then
-        __action=set_debug
-        return 0
-    fi
-    if [ -n "$__AUTODEBUG" -a "$option_" == --debug ]; then
-        __action=set_debug
-        return 0
-    fi
+        ;;
+    --help)
+        if [ -n "$__AUTOHELP" ]; then
+            __action='showhelp@'
+            return 0
+        fi
+        ;;
+    --help++)
+        if [ -n "$__AUTOHELP" ]; then
+            __action='showhelp@ ++'
+            return 0
+        fi
+        ;;
+    -L)
+        if ! array_contains __NOAUTOL "$option_"; then
+            __action='elogto $value_'
+            return 0
+        fi
+        ;;
+    --log-to)
+        if ! array_contains __NOAUTOL "$option_"; then
+            __action='elogto $value_'
+            return 0
+        fi
+        ;;
+    -Q|-q|-v|-D)
+        if ! array_contains __NOAUTOV "$option_"; then
+            __action='set_verbosity $option_'
+            return 0
+        fi
+        ;;
+    --very-quiet|--quiet|--verbose|--debug)
+        if ! array_contains __NOAUTOVERBOSITY "$option_"; then
+            __action='set_verbosity $option_'
+            return 0
+        fi
+        ;;
+    -b|-y|-i)
+        if ! array_contains __NOAUTOI "$option_"; then
+            __action='set_interaction $option_'
+            return 0
+        fi
+        ;;
+    --batch|--automatic|--interactive)
+        if ! array_contains __NOAUTOINTERACTION "$option_"; then
+            __action='set_interaction $option_'
+            return 0
+        fi
+        ;;
+    esac
     # ici, l'option n'a pas été trouvée, on ne devrait pas arriver ici
-    eerror "Unexpected option '$option_'"; eval "$__DIE"
+    die "Unexpected option '$option_'" || return
 }
diff --git a/bash/src/base.input.sh b/bash/src/base.input.sh
index 9e9acf0..c81fbc8 100644
--- a/bash/src/base.input.sh
+++ b/bash/src/base.input.sh
@@ -62,7 +62,7 @@ function ask_yesno() {
         else
             __eecho_ "Voulez-vous continuer?" 1>&2
         fi
-        tooenc_ " $prompt " 1>&2
+        echo_ " $prompt " 1>&2
         uread r
         is_yes "${r:-$default}"
     else
@@ -198,17 +198,17 @@ function __rv_read() {
             __eecho_ "Entrez la valeur" 1>&2
         fi
         if [ -n "$__rv_readline" ]; then
-            tooenc_ ": " 1>&2
+            echo_ ": " 1>&2
             uread -e ${__rv_d:+-i"$__rv_d"} "${__rv_opts[@]}" __rv_r
         else
             if [ -n "$__rv_d" ]; then
                 if [ -n "$__rv_showdef" ]; then
-                    tooenc_ " [$__rv_d]" 1>&2
+                    echo_ " [$__rv_d]" 1>&2
                 else
-                    tooenc_ " [****]" 1>&2
+                    echo_ " [****]" 1>&2
                 fi
             fi
-            tooenc_ ": " 1>&2
+            echo_ ": " 1>&2
             uread "${__rv_opts[@]}" __rv_r
             [ -n "$__rv_nl" ] && echo
         fi
@@ -217,6 +217,7 @@ function __rv_read() {
             _setv "$__rv_v" "$__rv_r"
             return 0
         fi
+        echo
     done
 }
 
@@ -268,7 +269,7 @@ function simple_menu() {
         else
             __eecho_ "Entrez le numéro de l'option choisie" 1>&2
         fi
-        tooenc_ ": " 1>&2
+        echo_ ": " 1>&2
         uread __sm_choice
 
         # Valeur par défaut
@@ -291,7 +292,7 @@ function simple_menu() {
         let __sm_c=$__sm_c+1
         if [ "$__sm_c" -eq 5 ]; then
             # sauter une ligne toutes les 4 tentatives
-            tooenc "" 1>&2
+            echo 1>&2
             __sm_c=0
         fi
     done
@@ -438,7 +439,7 @@ function __void_actions_menu() {
         if [ $c -eq 0 ]; then
             [ -n "$title" ] && __etitle "$title" 1>&2
             __eecho_ "=== Actions disponibles: " 1>&2
-            tooenc "$action_title" 1>&2
+            recho "$action_title" 1>&2
         fi
         if [ -n "$actyc" ]; then
             __eecho_ "$actyc" 1>&2
@@ -447,7 +448,7 @@ function __void_actions_menu() {
         else
             __eecho_ "Entrez l'action à effectuer" 1>&2
         fi
-        tooenc_ ": " 1>&2
+        echo_ ": " 1>&2
         uread choice
         if [ -z "$choice" -a -n "$default_action" ]; then
             select_action="$default_action"
@@ -468,7 +469,7 @@ function __void_actions_menu() {
         let c=$c+1
         if [ $c -eq 5 ]; then
             # sauter une ligne toutes les 4 tentatives
-            tooenc "" 1>&2
+            echo 1>&2
             c=0
         fi
     done
@@ -484,21 +485,21 @@ function __options_actions_menu() {
             i=1
             for option in "${options[@]}"; do
                 if [ "$option" == "$select_option" ]; then
-                    tooenc "$i*- $option" 1>&2
+                    echo "$i*- $option" 1>&2
                 else
-                    tooenc "$i - $option" 1>&2
+                    echo "$i - $option" 1>&2
                 fi
                 let i=$i+1
             done
             __estepn_ "Actions disponibles: " 1>&2
-            tooenc "$action_title" 1>&2
+            recho "$action_title" 1>&2
         fi
         if [ -n "$optyc" ]; then
             __eecho_ "$optyc" 1>&2
         else
             __eecho_ "Entrez l'action et le numéro de l'option choisie" 1>&2
         fi
-        tooenc_ ": " 1>&2
+        echo_ ": " 1>&2
         uread choice
 
         # vérifier la saisie
@@ -572,7 +573,7 @@ function __options_actions_menu() {
         let c=$c+1
         if [ $c -eq 5 ]; then
             # sauter une ligne toutes les 4 tentatives
-            tooenc "" 1>&2
+            echo "" 1>&2
             c=0
         fi
     done
diff --git a/bash/src/base.output.sh b/bash/src/base.output.sh
index ae51afd..15f2040 100644
--- a/bash/src/base.output.sh
+++ b/bash/src/base.output.sh
@@ -83,7 +83,7 @@ function err_isatty() {
 
 ################################################################################
 
-function tooenc() {
+function uecho() {
 # $1 étant une chaine encodée en utf-8, l'afficher dans l'encoding de sortie $2
 # qui vaut par défaut $NULIB_OUTPUT_ENCODING
     local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
@@ -93,9 +93,8 @@ function tooenc() {
         iconv -f -utf-8 -t "$to" <<<"$value"
     fi
 }
-function uecho() { tooenc "$*"; }
 
-function tooenc_() {
+function uecho_() {
 # $1 étant une chaine encodée en utf-8, l'afficher sans passer à la ligne dans
 # l'encoding de sortie $2 qui vaut par défaut $NULIB_OUTPUT_ENCODING
     local value="$1" to="${2:-$NULIB_OUTPUT_ENCODING}"
@@ -105,7 +104,6 @@ function tooenc_() {
         recho_ "$value" | iconv -f utf-8 -t "$to"
     fi
 }
-function uecho_() { tooenc_ "$*"; }
 
 export NULIB_QUIETLOG
 export NULIB__TMPLOG
@@ -210,7 +208,7 @@ function __eindent() {
 # indenter les lignes de $1, sauf la première
     local -a lines; local line first=1
     local indent="$(__eindent0)$2"
-    setx -a lines=echo "$1"
+    setx -a lines=recho "$1"
     for line in "${lines[@]}"; do
         if [ -n "$first" ]; then
             recho "$line"
@@ -232,7 +230,11 @@ function __complete() {
 }
 
 PRETTYOPTS=()
-function set_verbosity() { :;}
+function set_verbosity() {
+    case "$1" in
+    -D|--debug) NULIB_DEBUG=1;;
+    esac
+}
 function check_verbosity() { return 0; }
 function get_verbosity_option() { :;}
 
diff --git a/bash/src/pman.sh b/bash/src/pman.sh
index dcd9dfb..f2f5d3d 100644
--- a/bash/src/pman.sh
+++ b/bash/src/pman.sh
@@ -159,6 +159,7 @@ function load_branches() {
     local what="${1:-all}"; shift
     case "$what" in
     all)
+        [ -n "$Origin" ] || Origin=origin
         setx CurrentBranch=git_get_branch
         setx -a LocalBranches=git_list_branches
         setx -a RemoteBranches=git_list_rbranches "$Origin"
@@ -195,12 +196,12 @@ function load_branches() {
         ReleaseBranch=
         HotfixBranch=
         MainBranch=
-        Dist=Branch
-        for branch in "${AllBranches[@]}"; do
+        DistBranch=
+        for branch in "${LocalBranches[@]}"; do
             if [ "$branch" == "$UPSTREAM" ]; then
                 UpstreamBranch="$branch"
             elif [[ "$branch" == "$FEATURE"* ]]; then
-                FeatureBranch+=("$branch")
+                FeatureBranches+=("$branch")
             elif [ "$branch" == "$DEVELOP" ]; then
                 DevelopBranch="$branch"
             elif [[ "$branch" == "$RELEASE"* ]]; then
@@ -224,13 +225,15 @@ function load_config() {
     if [ -n "$ConfigFile" ]; then
         source "$ConfigFile" || die || return
     elif [ -n "$ConfigBranch" ]; then
+        # c'est le seul cas où ConfigFile reste vide
         if ! array_contains LocalBranches "$ConfigBranch"; then
             die "$ConfigBranch: branche de configuration introuvable" || return
         else
-            ac_set_tmpfile ConfigFile
-            git show "$ConfigBranch:.pman.conf" >"$ConfigFile" 2>/dev/null
-            [ -s "$ConfigFile" ] || die "$ConfigBranch: aucune configuration trouvée sur cette branche" || return
-            source "$ConfigFile"
+            local config
+            ac_set_tmpfile config
+            git show "$ConfigBranch:.pman.conf" >"$config" 2>/dev/null
+            [ -s "$config" ] || die "$ConfigBranch: aucune configuration trouvée sur cette branche" || return
+            source "$config"
         fi
     elif [ -f .pman.conf ]; then
         ConfigFile="$(pwd)/.pman.conf"
@@ -250,6 +253,16 @@ function load_config() {
 ################################################################################
 # Divers
 
+function resolve_should_push() {
+    local quiet="$1"
+    ShouldPush=1
+    if ! git_have_remote "$Origin" && [ -n "$Push" ]; then
+        [ -n "$quiet" ] || enote "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
+        ShouldPush=
+    fi
+    [ -z "$ShouldPush" ] && Push=
+}
+
 function _push_branches() {
     [ ${#push_branches[*]} -gt 0 ] || return
     [ -n "$Origin" ] || Origin=origin
diff --git a/bash/src/pretty.sh b/bash/src/pretty.sh
index 2d4f32c..de252fc 100644
--- a/bash/src/pretty.sh
+++ b/bash/src/pretty.sh
@@ -108,11 +108,17 @@ function set_interaction() {
 # set_interaction en fonction des arguments de la ligne de commande. A utiliser
 # de cette manière:
 #   parse_opts ... "${PRETTYOPTS[@]}" @ args -- ...
-PRETTYOPTS=(
+# NB: ce n'est pas nécessaire, sauf si on veut afficher ces options dans l'aide
+LOGTOOPTS=(
     -L:,--log-to:LOGFILE '$elogto $value_' "++enregistrer les messages dans le fichier spécifié"
+)
+VERBOSITYOPTS=(
     -Q,--very-quiet,-q,--quiet,-v,--verbose,-D,--debug '$set_verbosity $option_' "++spécifier le niveau de verbiage"
+)
+INTERACTIONOPTS=(
     -b,--batch,-y,--automatic,-i,--interactive '$set_interaction $option_' "++spécifier le niveau d'interaction"
 )
+PRETTYOPTS=("${LOGTOOPTS[@]}" "${VERBOSITYOPTS[@]}" "${INTERACTIONOPTS[@]}")
 
 function show_error() { [ "$__verbosity" -ge 1 ]; }
 function show_warn() { [ "$__verbosity" -ge 2 ]; }
diff --git a/bash/tests/test-interaction.sh b/bash/tests/test-interaction.sh
new file mode 100755
index 0000000..db35861
--- /dev/null
+++ b/bash/tests/test-interaction.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+args=(
+    "tester diverses fonctions de saisie"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+estep "inter non auto non"
+ask_yesno "oui ou non?"   && echo oui || echo non
+estep "inter oui auto oui"
+ask_yesno "oui ou non?" O && echo oui || echo non
+estep "inter non auto non"
+ask_yesno "oui ou non?" N && echo oui || echo non
+estep "inter non auto oui"
+ask_yesno "oui ou non?" C && echo oui || echo non
+estep "inter oui auto non"
+ask_yesno "oui ou non?" X && echo oui || echo non
+
+estep "valeur par défaut vide"
+read_value "valeur" empty "" N; echo "valeur=$empty"
+
+estep "valeur par défaut non vide"
+read_value "valeur" default default N; echo "valeur=$default"
+
+estep "valeur requise"
+read_value "valeur" required; echo "valeur=$required"
diff --git a/bash/tests/test-output.sh b/bash/tests/test-output.sh
index 56a5812..3b369d7 100755
--- a/bash/tests/test-output.sh
+++ b/bash/tests/test-output.sh
@@ -7,7 +7,6 @@ Multiline=
 Banner=
 args=(
     "afficher divers messages avec les fonctions e*"
-    -D,--debug '$set_debug'
     -d,--date NULIB_ELOG_DATE=1
     -m,--myname NULIB_ELOG_MYNAME=1
     -n,--nc,--no-color '$__set_no_colors 1'
diff --git a/bash/tests/test-verbosity.sh b/bash/tests/test-verbosity.sh
new file mode 100755
index 0000000..4b53d8f
--- /dev/null
+++ b/bash/tests/test-verbosity.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../src/nulib.sh" || exit 1
+#NULIB_NO_DISABLE_SET_X=1
+
+args=(
+    "afficher divers messages avec les fonctions e*"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+eimportant "important (q)"
+eattention "attention (q)"
+eerror "error (q)"
+ewarn "warn (q)"
+enote "note (qv)"
+einfo "info (qv)"
+eecho "echo (qv)"
+edebug "debug (D)"
+
+estep "step (qv)"
+estepe "stepe (qv)"
+estepw "stepw (qv)"
+estepn "stepn (qv)"
+estepi "stepi (qv)"
diff --git a/bin/_merge82 b/bin/_merge82
index 3c89e35..8a4c2bc 100755
--- a/bin/_merge82
+++ b/bin/_merge82
@@ -1,4 +1,4 @@
 #!/bin/bash
 # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
 
-exec "$(dirname -- "$0")/pdev" --tech-merge -Bdev82 dev74 "$@"
+exec "$(dirname -- "$0")/pdev" --tech-merge -Bdev82 dev74 -a "git checkout dev74" "$@"
diff --git a/bin/_pman-composer_local_deps.php b/bin/_pman-composer_local_deps.php
new file mode 100755
index 0000000..badae7b
--- /dev/null
+++ b/bin/_pman-composer_local_deps.php
@@ -0,0 +1,14 @@
+#!/usr/bin/php
+<?php
+require __DIR__ . "/../vendor/autoload.php";
+
+use nulib\tools\pman\ComposerFile;
+use nulib\tools\pman\PmanYamlConfigFile;
+use nulib\ValueException;
+
+$composer = new ComposerFile();
+
+$deps = $composer->getLocalDeps();
+foreach ($deps as $dep => $path) {
+  echo "$path\n";
+}
\ No newline at end of file
diff --git a/bin/p b/bin/p
index 561d88a..aedfdfc 100755
--- a/bin/p
+++ b/bin/p
@@ -20,8 +20,18 @@ function git_status() {
     fi
 }
 
+function git_statuses() {
+    local cwd="$(pwd)" dir
+    for dir in "$@"; do
+        cd "$dir" || die
+        git_status --porcelain
+        cd "$cwd"
+    done
+}
+
 chdir=
 all=
+composer=
 args=(
     "afficher l'état du dépôt"
     "[-d chdir] [-a patterns...]
@@ -29,6 +39,7 @@ args=(
 Si l'option -a est utilisée, ce script accepte comme arguments une liste de patterns permettant de filtrer les répertoires concernés"
     -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
     -a,--all all=1 "faire l'opération sur tous les sous-répertoires de BASEDIR qui sont des dépôts git"
+    -r,--composer composer=1 "faire l'opération sur tous les projets composer dépendants"
 )
 parse_args "$@"; set -- "${args[@]}"
 
@@ -50,16 +61,23 @@ if [ -n "$all" ]; then
             dirs+=("${dir%/.git}")
         done
     fi
-    setx cwd=pwd
-    for dir in "${dirs[@]}"; do
-        cd "$dir" || die
-        git_status --porcelain
-        cd "$cwd"
-    done
+    git_statuses "${dirs[@]}"
+
+elif [ -n "$composer" ]; then
+    # projets dépendants
+    git_ensure_gitvcs
+    setx toplevel=git_get_toplevel
+    cd "$toplevel" || die
+    setx cwd=ppath2 . "$OrigCwd"
+    [ -f composer.json ] || die "$cwd: ce n'est pas un projet composer"
+
+    setx -a dirs="$MYDIR/_pman-composer_local_deps.php"
+    git_statuses "${dirs[@]}"
+
 else
     # répertoire courant uniquement
-    setx toplevel=git_get_toplevel
-    [ -n "$toplevel" ] && Cwd="$toplevel"
+    git_ensure_gitvcs
+    Cwd="$(git_get_toplevel)"
 
     args=()
     isatty || args+=(--porcelain)
diff --git a/bin/pdev b/bin/pdev
index 6a2f5d1..03e827c 100755
--- a/bin/pdev
+++ b/bin/pdev
@@ -25,6 +25,9 @@ function ensure_branches() {
 }
 
 function merge_action() {
+    [ -z "$ShouldPush" ] && enote "\
+L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
+
     enote "\
 Ce script va
 - fusionner la branche ${COULEUR_BLEUE}$SrcBranch${COULEUR_NORMALE} dans ${COULEUR_ROUGE}$DestBranch${COULEUR_NORMALE}${Push:+
@@ -33,7 +36,7 @@ Ce script va
 
     local script=".git/pman-merge.sh"
     local -a push_branches delete_branches
-    local after
+    local hook
     local comment=
     local or_die=" || exit 1"
 
@@ -44,17 +47,22 @@ Ce script va
 if [ -n "\$merge" ]; then
 esection "Fusionner la branche"
 EOF
-    _mscript_merge_branch
-    after="AFTER_MERGE_${SrcType^^}"; [ -n "${!after}" ] && _scripta <<EOF
+    hook="BEFORE_MERGE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
 (
-${!after}
+${!hook}
+)$or_die
+EOF
+    _mscript_merge_branch
+    hook="AFTER_MERGE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
+(
+${!hook}
 )$or_die
 EOF
     _scripta <<EOF
 fi
 EOF
 
-    if [ -z "$ForbidDelete" ]; then
+    if [ -n "$ShouldDelete" ]; then
         _scripta <<EOF
 ################################################################################
 # delete
@@ -62,9 +70,9 @@ if [ -n "\$delete" ]; then
 esection "Supprimer la branche"
 EOF
         _mscript_delete_branch
-        after="AFTER_DELETE_${SrcType^^}"; [ -n "${!after}" ] && _scripta <<EOF
+        hook="AFTER_DELETE_${SrcType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
 (
-${!after}
+${!hook}
 )$or_die
 EOF
         _scripta <<EOF
@@ -77,6 +85,11 @@ EOF
 # push
 if [ -n "\$push" ]; then
 esection "Pousser les branches"
+EOF
+    hook="BEFORE_PUSH_${DestType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
+(
+${!hook}
+)$or_die
 EOF
     _script_push_branches
     if [ ${#delete_branches[*]} -gt 0 ]; then
@@ -85,35 +98,42 @@ EOF
         _script_push_branches
         _scripta <<<fi
     fi
-    after="AFTER_PUSH_${DestType^^}"; [ -n "${!after}" ] && _scripta <<EOF
+    hook="AFTER_PUSH_${DestType^^}"; [ -n "${!hook}" ] && _scripta <<EOF
 (
-${!after}
+${!hook}
 )$or_die
 EOF
     _scripta <<EOF
 fi
 EOF
 
-    [ -n "$Delete" -o "$ForbidDelete" ] && Deleted=1 || Deleted=
-    [ -n "$Push" -o "$CantPush" ] && Pushed=1 || Pushed=
-    if [ -n "$_NoRunScript" ]; then
-        einfo "Veuillez consulter le script $script pour le détail des opérations à effectuer"
+    [ -n "$Delete" -o -z "$ShouldDelete" ] && Deleted=1 || Deleted=
+    [ -n "$ShouldDelete" -a -n "$Delete" ] && ShouldDelete=
+    [ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
+    if [ -n "$_Fake" ]; then
+        cat "$script"
     elif ! "$script" merge ${Delete:+delete} ${Push:+push}; then
         eimportant "\
 Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
-Veuillez le consulter pour le détail des opérations qui n'ont pas pu êtres effectuées"
+En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
+    git merge --continue
+Sinon, veuillez consulter le script et/ou le relancer
+    ./$script${Delete:+ delete}${Push:+ push}"
         die
-    elif [ -n "$Deleted" -a -n "$Pushed" ]; then
+    elif [ -n "$Deleted" -a -n "$Push" ]; then
         [ -n "$_KeepScript" ] || rm "$script"
+        [ -n "$AfterMerge" ] && eval "$AfterMerge"
     else
-        local cmd
-        [ -n "$Deleted" ] || cmd="$cmd
-    ./$script delete"
-        [ -n "$Pushed" ] || cmd="$cmd
-    ./$script push"
-        einfo "\
+        local msg="\
 Le script $script a été lancé avec les arguments 'merge${Delete:+ delete}${Push:+ push}'
-Veuillez le consulter pour le détail des autres opérations à effectuer$cmd"
+Vous pouvez consulter le script et/ou le relancer
+    ./$script${ShouldDelete:+ delete}${ShouldPush:+ push}"
+        [ -n "$AfterMerge" ] && msg="$msg
+Il y a aussi les commandes supplémentaires suivantes:
+    ${AfterMerge//
+/
+    }"
+        einfo "$msg"
     fi
 }
 
@@ -125,24 +145,27 @@ chdir=
 Origin=
 ConfigBranch=
 ConfigFile=
+_Fake=
 _KeepScript=
-_NoRunScript=
 action=merge
 TechMerge=
 SquashMsg=
 [ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
 [ -z "$PMAN_NO_DELETE" ] && Delete=1 || Delete=
+AfterMerge=
 args=(
     "fusionner la branche source dans la branche destination correspondante"
     " [source]
 
 CONFIGURATION
-Le fichier .pman.conf contient la configuration des branches.Les variables
+Le fichier .pman.conf contient la configuration des branches. Les variables
 supplémentaires suivantes peuvent être définies:
+    BEFORE_MERGE_<srcType>
     AFTER_MERGE_<srcType>
     AFTER_DELETE_<srcType>
+    BEFORE_PUSH_<destType>
     AFTER_PUSH_<destType>
-xxxType valant UPSTREAM, DEVELOP, FEATURE, RELEASE, MAIN, HOTFIX, DIST"
+srcType et destType pouvant valoir UPSTREAM, DEVELOP, FEATURE, RELEASE, MAIN, HOTFIX, DIST"
     -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
     -O:,--origin Origin= "++\
 origine à partir de laquelle les branches distantes sont considérées"
@@ -151,8 +174,8 @@ branche à partir de laquelle charger la configuration"
     -c:,--config-file:CONFIG ConfigFile= "++\
 fichier de configuration des branches. cette option est prioritaire sur --config-branch
 par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
+    --fake _Fake=1 "++option non documentée"
     --keep-script _KeepScript=1 "++option non documentée"
-    --no-run-script _NoRunScript=1 "++option non documentée"
     -w,--show action=show "\
 lister les modifications qui seraient fusionnées dans la branche destination"
     -b,--rebase action=rebase "\
@@ -175,6 +198,10 @@ ne pas supprimer la branche après la fusion dans la destination"
     --delete Delete=1 "++\
 supprimer la branche après la fusion dans la destination.
 c'est l'option par défaut"
+    -f,--force-merge ForceMerge=1 "++\
+forcer la fusion pour une branche qui devrait être traitée par prel"
+    -a:,--after-merge AfterMerge= "\
+évaluer le script spécifié après une fusion *réussie*"
 )
 parse_args "$@"; set -- "${args[@]}"
 
@@ -184,13 +211,7 @@ load_branches all
 load_config "$MYNAME"
 load_branches current "$1"
 
-CantPush=
-[ -n "$Origin" ] || Origin=origin
-if ! git_have_remote "$Origin" && [ -n "$Push" ]; then
-    ewarn "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
-    CantPush=1
-fi
-[ -n "$CantPush" ] && Push=
+resolve_should_push quiet
 
 # puis faire l'action que l'on nous demande
 case "$action" in
@@ -200,24 +221,26 @@ show)
     show_action "$@"
     ;;
 merge)
-    ForbidDelete=
-    case "$SrcType" in
-    develop|release|hotfix)
-        die "$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
-        ;;
-    *)
-        # n'autoriser la suppression que pour feature
-        [ "$SrcType" == feature ] || ForbidDelete=1
-        ;;
-    esac
-    [ -n "$ForbidDelete" ] && Delete=
-    git_ensure_cleancheckout
-    if ! array_contains LocalBranches "$SrcBranch"; then
-        # si la branche source n'existe pas, la créer
-        exec "$MYDIR/pman" "$FEATURE${SrcBranch#$FEATURE}"
-    else
+    ShouldDelete=1
+    no_merge_msg="$SrcBranch: cette branche doit être fusionnée dans $DestBranch avec prel"
+    if [ "$SrcType" == develop ]; then
+        [ -z "$ForceMerge" ] && die "$no_merge_msg"
+        [ -n "$AfterMerge" ] || setx AfterMerge=qvals git checkout -q "$SrcBranch"
+    elif [ "$SrcType" == release -o "$SrcType" == hotfix ]; then
+        die "$no_merge_msg"
+    fi
+    # n'autoriser la suppression que pour feature
+    [ "$SrcType" == feature ] || ShouldDelete=
+    [ -z "$ShouldDelete" ] && Delete=
+    [ -z "$_Fake" ] && git_ensure_cleancheckout
+    if array_contains LocalBranches "$SrcBranch"; then
         ensure_branches
         merge_action "$@"
+    elif array_contains AllBranches "$SrcBranch"; then
+        enote "$SrcBranch: une branche du même nom existe dans l'origine"
+        die "$SrcBranch: branche locale introuvable"
+    else
+        die "$SrcBranch: branche introuvable"
     fi
     ;;
 *)
diff --git a/bin/pman b/bin/pman
index 2c999e4..d4b91a9 100755
--- a/bin/pman
+++ b/bin/pman
@@ -59,9 +59,9 @@ function _init_config() {
 }
 
 function init_repo_action() {
-    [ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé"
+    local -a push_branches; local config
 
-    local -a push_branches
+    [ ${#LocalBranches[*]} -eq 0 ] || die "Ce dépôt a déjà été initialisé"
 
     _init_config || exit_with ewarn "Initialisation du dépôt annulée"
 
@@ -78,9 +78,11 @@ function init_repo_action() {
 }
 
 function init_config_action() {
-    [ -f .pman.conf ] && die "La configuration pman a déjà été initialisée"
+    local -a push_branches; config
 
-    local -a push_branches
+    [ -f .pman.conf -a -z "$ForceCreate" ] && die "La configuration pman a déjà été initialisée"
+
+    resolve_should_push
 
     _init_config || exit_with ewarn "Initialisation de la configuration annulée"
     git commit -m "configuration pman"
@@ -89,17 +91,41 @@ function init_config_action() {
     _push_branches
 }
 
+function _ensure_main_branch() {
+    [ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
+    [ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+}
+
+function checkout_main_action() {
+    if [ -z "$MainBranch" ]; then
+        array_contains AllBranches "$MAIN" && exit_with enote "\
+$MAIN: une branche du même nom existe dans l'origine
+    git checkout $MAIN"
+        _ensure_main_branch
+    fi
+    git checkout -q "$MAIN"
+}
+
+function _ensure_develop_branch() {
+    [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
+    [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+}
+
 function init_develop_action() {
+    local -a push_branches
+
     if [ -z "$DevelopBranch" ]; then
-        [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
-        [ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
-        [ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+        array_contains AllBranches "$DEVELOP" && exit_with enote "\
+$DEVELOP: une branche du même nom existe dans l'origine
+    git checkout $DEVELOP"
+        _ensure_main_branch
+        _ensure_develop_branch
+
+        resolve_should_push
 
         enote "Vous allez créer la branche ${COULEUR_VERTE}$DEVELOP${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}"
         ask_yesno "Voulez-vous continuer?" O || die
 
-        local -a push_branches
-
         einfo "Création de la branche $DEVELOP"
         git checkout -b "$DEVELOP" "$MAIN" || die
         push_branches+=("$DEVELOP")
@@ -109,17 +135,26 @@ function init_develop_action() {
     git checkout -q "$DEVELOP"
 }
 
+function _ensure_upstream_branch() {
+    [ -n "$UPSTREAM" ] || die "La branche UPSTREAM n'a pas été définie"
+    [ -n "$UpstreamBranch" ] || die "$UPSTREAM: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+}
+
 function init_upstream_action() {
+    local -a push_branches; local config
+
     if [ -z "$UpstreamBranch" ]; then
-        [ -n "$UPSTREAM" ] || die "La branche UPSTREAM n'a pas été définie"
-        [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
-        [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+        array_contains AllBranches "$UPSTREAM" && exit_with enote "\
+$UPSTREAM: une branche du même nom existe dans l'origine
+    git checkout $UPSTREAM"
+        _ensure_develop_branch
+        _ensure_upstream_branch
+
+        resolve_should_push
 
         enote "Vous allez créer la branche ${COULEUR_VERTE}$UPSTREAM${COULEUR_NORMALE}"
         ask_yesno "Voulez-vous continuer?" O || die
 
-        local -a push_branches; local config
-
         # faire une copie de la configuration actuelle
         ac_set_tmpfile config
         cp "$ConfigFile" "$config"
@@ -145,17 +180,26 @@ function init_upstream_action() {
     git checkout -q "$UPSTREAM"
 }
 
+function _ensure_dist_branch() {
+    [ -n "$DIST" ] || die "La branche DIST n'a pas été définie"
+    [ -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+}
+
 function init_dist_action() {
+    local -a push_branches
+
     if [ -z "$DistBranch" ]; then
-        [ -n "$DIST" ] || die "La branche DIST n'a pas été définie"
-        [ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie"
-        [ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+        array_contains AllBranches "$DIST" && exit_with enote "\
+$DIST: une branche du même nom existe dans l'origine
+    git checkout $DIST"
+        _ensure_main_branch
+        _ensure_dist_branch
+
+        resolve_should_push
 
         enote "Vous allez créer la branche ${COULEUR_VERTE}$DIST${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$MAIN${COULEUR_NORMALE}"
         ask_yesno "Voulez-vous continuer?" O || die
 
-        local -a push_branches
-
         einfo "Création de la branche $DIST"
         git checkout -b "$DIST" "$MAIN" || die
         push_branches+=("$DIST")
@@ -166,18 +210,24 @@ function init_dist_action() {
 }
 
 function init_feature_action() {
-    local branch="${1#$FEATURE}"
-    [ -n "$branch" ] || die "Vous devez définir la nom de la branche à créer"
+    local -a push_branches; local branch
+
+    [ -n "$FEATURE" ] || die "La branche FEATURE n'a pas été définie"
+    branch="${1#$FEATURE}"
+    [ -n "$branch" ] || die "Vous devez spécifier le nom de la branche"
     branch="$FEATURE$branch"
-    if ! array_contains AllBranches "$branch"; then
-        [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie"
-        [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)"
+
+    if ! array_contains LocalBranches "$branch"; then
+        array_contains AllBranches "$branch" && exit_with enote "\
+$branch: une branche du même nom existe dans l'origine
+    git checkout $branch"
+        _ensure_develop_branch
+
+        resolve_should_push
 
         enote "Vous allez créer la branche ${COULEUR_VERTE}$branch${COULEUR_NORMALE} <-- ${COULEUR_BLEUE}$DEVELOP${COULEUR_NORMALE}"
         ask_yesno "Voulez-vous continuer?" O || die
 
-        local -a push_branches
-
         einfo "Création de la branche $branch"
         git checkout -b "$branch" "$DEVELOP" || die
         push_branches+=("$branch")
@@ -192,7 +242,7 @@ function init_action() {
     case "$what" in
     init|repo|r) init_repo_action "$@";;
     config) init_config_action "$@";;
-    main|m) git checkout -q "$MAIN";;
+    main|m) checkout_main_action;;
     develop|dev|d) init_develop_action "$@";;
     upstream|up|u) init_upstream_action "$@";;
     dist|x) init_dist_action "$@";;
@@ -209,7 +259,7 @@ ConfigBranch=
 ConfigFile=
 action=init
 Origin=
-Push=1
+[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
 ForceCreate=
 args=(
     "gérer un projet git"
diff --git a/bin/prel b/bin/prel
index 30a0706..e1af6eb 100755
--- a/bin/prel
+++ b/bin/prel
@@ -44,6 +44,8 @@ function create_release_action() {
 
     [ -n "$ManualRelease" ] && ewarn "\
 L'option --no-merge a été forcée puisque ce dépôt ne supporte pas les releases automatiques"
+    [ -z "$ShouldPush" ] && enote "\
+L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
 
     if [ -z "$Version" -a -n "$CurrentVersion" -a -f VERSION.txt ]; then
         Version="$(<VERSION.txt)"
@@ -83,6 +85,11 @@ Vous devrez:
 # create
 if [ -n "\$create" ]; then
 esection "Création de la release"
+EOF
+    [ -n "$BEFORE_CREATE_RELEASE" ] && _scripta <<EOF
+(
+$BEFORE_CREATE_RELEASE
+)$or_die
 EOF
     _rscript_create_release_branch
     [ -n "$AFTER_CREATE_RELEASE" ] && _scripta <<EOF
@@ -99,6 +106,11 @@ EOF
 # merge
 if [ -n "\$merge" ]; then
 esection "Fusionner la release"
+EOF
+    [ -n "$BEFORE_MERGE_RELEASE" ] && _scripta <<EOF
+(
+$BEFORE_MERGE_RELEASE
+)$or_die
 EOF
     _rscript_merge_release_branch "$DestBranch" "$Tag"
     _rscript_merge_release_branch "$SrcBranch"
@@ -117,6 +129,11 @@ EOF
 # push
 if [ -n "\$push" ]; then
 esection "Pousser branches et tags"
+EOF
+    [ -n "$BEFORE_PUSH_RELEASE" ] && _scripta <<EOF
+(
+$BEFORE_PUSH_RELEASE
+)$or_die
 EOF
     _script_push_branches
     _script_push_tags
@@ -129,26 +146,25 @@ EOF
 fi
 EOF
 
-    [ -n "$Merge" ] && Merged=1 || Merged=
-    [ -n "$Push" -o "$CantPush" ] && Pushed=1 || Pushed=
-    if [ -n "$_NoRunScript" ]; then
-        einfo "Veuillez consulter le script $script pour le détail des opérations à effectuer"
+    [ -z "$ManualRelease" -a -n "$Merge" ] && ShouldMerge= || ShouldMerge=1
+    [ -n "$ShouldPush" -a -n "$Push" ] && ShouldPush=
+    if [ -n "$_Fake" ]; then
+        cat "$script"
     elif ! "$script" create ${Merge:+merge} ${Push:+push}; then
         eimportant "\
 Le script $script a été lancé avec les arguments 'create${Merge:+ merge}${Push:+ push}'
-Veuillez le consulter pour le détail des opérations qui n'ont pas pu êtres effectuées"
+En cas d'erreur de merge, veuillez corriger les erreurs puis continuer avec
+    git merge --continue
+Veuillez aussi consulter le script et/ou le relancer
+    ./$script${Push:+ push}"
         die
-    elif [ -n "$Merged" -a -n "$Pushed" ]; then
+    elif [ -n "$Merge" -a -n "$Push" ]; then
         [ -n "$_KeepScript" ] || rm "$script"
     else
-        local cmd
-        [ -n "$Merged" ] || cmd="$cmd
-    ./$script merge"
-        [ -n "$Pushed" ] || cmd="$cmd
-    ./$script push"
         einfo "\
 Le script $script a été lancé avec les arguments 'create${Merge:+ merge}${Push:+ push}'
-Veuillez le consulter pour le détail des autres opérations à effectuer$cmd"
+Vous pouvez consulter le script et/ou le relancer
+    ./$script${ShouldMerge:+ merge}${ShouldPush:+ push}"
     fi
 }
 
@@ -175,8 +191,8 @@ chdir=
 Origin=
 ConfigBranch=
 ConfigFile=
+_Fake=
 _KeepScript=
-_NoRunScript=
 action=release
 [ -z "$PMAN_NO_MERGE" ] && Merge=1 || Merge=
 [ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
@@ -188,11 +204,13 @@ args=(
     " -v VERSION [source]
 
 CONFIGURATION
-Le fichier .pman.conf contient la configuration des branches
-
-Les variables supplémentaires suivantes peuvent être définies:
+Le fichier .pman.conf contient la configuration des branches. Les variables
+supplémentaires suivantes peuvent être définies:
+    BEFORE_CREATE_RELEASE
     AFTER_CREATE_RELEASE
+    BEFORE_MERGE_RELEASE
     AFTER_MERGE_RELEASE
+    BEFORE_PUSH_RELEASE
     AFTER_PUSH_RELEASE"
     -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
     -O:,--origin Origin= "++\
@@ -202,8 +220,8 @@ branche à partir de laquelle charger la configuration"
     -c:,--config-file:CONFIG ConfigFile= "++\
 fichier de configuration des branches. cette option est prioritaire sur --config-branch
 par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
+    --fake _Fake=1 "++option non documentée"
     --keep-script _KeepScript=1 "++option non documentée"
-    --no-run-script _NoRunScript=1 "++option non documentée"
     -w,--show action=show "\
 lister les modifications qui seraient intégrées dans la release"
     --release action=release "++\
@@ -223,7 +241,7 @@ c'est l'option par défaut"
 spécifier la version de la release à créer"
     -C,--current-version CurrentVersion=1 "++\
 si aucune version n'est spécifiée, prendre la version présente dans le fichier VERSION.txt"
-    -f,--force-create ForceCreate= "\
+    -f,--force-create ForceCreate=1 "\
 forcer la création de la release même si le tag correspond à la version existe déjà"
 )
 parse_args "$@"; set -- "${args[@]}"
@@ -238,13 +256,7 @@ load_branches current "$1"; shift
 [ -n "$ManualRelease" ] && Merge=
 [ -z "$Merge" ] && Push=
 
-CantPush=
-[ -n "$Origin" ] || Origin=origin
-if ! git_have_remote "$Origin" && [ -n "$Push" ]; then
-    ewarn "L'option --no-push a été forcée puisque ce dépôt n'a pas d'origine"
-    CantPush=1
-fi
-[ -n "$CantPush" ] && Push=
+resolve_should_push quiet
 
 # puis faire l'action que l'on nous demande
 case "$action" in
@@ -254,7 +266,7 @@ show)
     show_action "$@"
     ;;
 release)
-    git_ensure_cleancheckout
+    [ -z "$_Fake" ] && git_ensure_cleancheckout
     ensure_branches
     case "$SrcType" in
     release) merge_release_action "$@";;
diff --git a/bin/pwip b/bin/pwip
new file mode 100755
index 0000000..787676b
--- /dev/null
+++ b/bin/pwip
@@ -0,0 +1,60 @@
+#!/bin/bash
+# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
+source "$(dirname -- "$0")/../load.sh" || exit 1
+require: git pman pman.conf
+
+git_cleancheckout_DIRTY="\
+Vous avez des modifications locales.
+Enregistrez ces modifications avant de créer une nouvelle branche"
+
+chdir=
+Origin=
+ConfigBranch=
+ConfigFile=
+[ -z "$PMAN_NO_PUSH" ] && Push=1 || Push=
+args=(
+    "créer une branche de feature"
+    "<feature>"
+    -d:,--chdir:BASEDIR chdir= "répertoire dans lequel se placer avant de lancer les opérations"
+    -O:,--origin Origin= "++\
+origine à partir de laquelle les branches distantes sont considérées"
+    -B:,--config-branch ConfigBranch= "++\
+branche à partir de laquelle charger la configuration"
+    -c:,--config-file:CONFIG ConfigFile= "++\
+fichier de configuration des branches. cette option est prioritaire sur --config-branch
+par défaut, utiliser le fichier .pman.conf dans le répertoire du dépôt s'il existe"
+    -n,--no-push Push= "\
+ne pas pousser les branches vers leur origine après la fusion"
+    --push Push=1 "++\
+pousser les branches vers leur origine après la fusion.
+c'est l'option par défaut"
+)
+parse_args "$@"; set -- "${args[@]}"
+
+# charger la configuration
+ensure_gitdir "$chdir"
+load_branches all
+load_config "$MYNAME"
+load_branches current
+
+branch="$1"
+if [ -z "$branch" -a ${#FeatureBranches[*]} -eq 1 ]; then
+    branch="${FeatureBranches[0]}"
+fi
+[ -n "$branch" ] || die "Vous devez spécifier la branche à créer"
+branch="$FEATURE${branch#$FEATURE}"
+
+resolve_should_push
+git_ensure_cleancheckout
+
+if array_contains AllBranches "$branch"; then
+    git checkout -q "$branch"
+else
+    # si la branche source n'existe pas, la créer
+    args=(--origin "$Origin")
+    if [ -n "$ConfigFile" ]; then args+=(--config-file "$ConfigFile")
+    elif [ -n "$ConfigBranch" ]; then args+=(--config-branch "$ConfigBranch")
+    fi
+    [ -z "$Push" ] && args+=(--no-push)
+    exec "$MYDIR/pman" "${args[@]}" "$branch"
+fi
diff --git a/dockerfiles/Dockerfile.php-apache b/dockerfiles/Dockerfile.php-apache
index 2a9e467..641de40 100644
--- a/dockerfiles/Dockerfile.php-apache
+++ b/dockerfiles/Dockerfile.php-apache
@@ -25,7 +25,7 @@ COPY --from=builder /src/su-exec/su-exec /g/
 RUN /g/build
 
 COPY --from=php /g/ /g/
-RUN /g/build -a @apache-php-cas php-utils
+RUN /g/build -a @php-apache-cas php-utils
 
 EXPOSE 80 443
 ENTRYPOINT ["/g/entrypoint"]
diff --git a/dockerfiles/Dockerfile.php-apache+ic b/dockerfiles/Dockerfile.php-apache+ic
index cfbbd61..8907e78 100644
--- a/dockerfiles/Dockerfile.php-apache+ic
+++ b/dockerfiles/Dockerfile.php-apache+ic
@@ -34,7 +34,7 @@ COPY --from=legacytools /g/ /g/
 RUN /g/build nutools
 
 COPY --from=php /g/ /g/
-RUN /g/build -a @apache-php-cas php-utils
+RUN /g/build -a @php-apache-cas php-utils
 
 COPY --from=instantclient /g/ /g/
 COPY --from=builder /opt/oracle/ /opt/oracle/
diff --git a/php/src/cl.php b/php/src/cl.php
index 8bb3b37..2397fe9 100644
--- a/php/src/cl.php
+++ b/php/src/cl.php
@@ -610,6 +610,42 @@ class cl {
 
   #############################################################################
 
+  /**
+   * tester si $array a en début de tableau les mêmes clés que $ref, et dans le
+   * même ordre. $array peut avoir d'autres clés, ça n'influe pas sur le résultat
+   *
+   * $keys obtient la liste des clés de $ref trouvées, dans l'ordre de $array
+   * $remainKeys obtient la liste des clés de $array qui ne sont pas dans $ref
+   * $missingKeys obtient la liste des clés de $ref qui ne sont pas dans $array
+   */
+  static function same_keys(?array $array, ?array $ref, ?array &$keys=null, ?array &$remainKeys=null, ?array &$missingKeys=null): bool {
+    $keys = [];
+    $remainKeys = [];
+    $missingKeys = [];
+    if ($array === null || $array === []) {
+      if ($ref === null || $ref === []) return true;
+      $missingKeys = array_keys($ref);
+      return false;
+    } elseif ($ref === null || $ref === []) {
+      $remainKeys = array_keys($array);
+      return true;
+    }
+    $refKeys = array_keys($ref);
+    $refCount = count($ref);
+    $index = 0;
+    $sameKeys = true;
+    foreach (array_keys($array) as $key) {
+      if ($index < $refCount) {
+        if ($key !== $refKeys[$index]) $sameKeys = false;
+        $index++;
+      }
+      if (array_key_exists($key, $ref)) $keys[] = $key;
+      else $remainKeys[] = $key;
+    }
+    $missingKeys = array_values(array_diff($refKeys, $keys));
+    return $sameKeys && $index == $refCount;
+  }
+
   /**
    * retourner le tableau $array en "renommant" les clés selon le tableau
    * $mappings qui contient des associations de la forme [$from => $to]
diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/schema/ref_schema.php
index 9cfa10b..cee4786 100644
--- a/php/src/ref/schema/ref_schema.php
+++ b/php/src/ref/schema/ref_schema.php
@@ -6,7 +6,7 @@ class ref_schema {
   const NATURE_METASCHEMA = [
     "nature" => ["string", null, "nature du schéma",
       "pkey" => 0,
-      "allowed_values" => ["assoc", "list", "scalar"],
+      "allowed_values" => ["scalar", "assoc", "list"],
     ],
     "title" => ["?string", null, "libellé de la valeur"],
     "required" => ["bool", false, "la valeur est-elle requise?"],
@@ -31,9 +31,10 @@ class ref_schema {
     "messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"],
     "formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"],
     "format" => [null, null, "format à utiliser pour l'affichage"],
-    "" => ["array", "scalar", "nature du schéma",
-      "" => ["assoc", "schema" => self::NATURE_METASCHEMA],
+    "" => ["array", ["scalar"], "nature du schéma",
+      "schema" => self::NATURE_METASCHEMA,
     ],
+    "schema" => ["?array", null, "schéma de la valeur si c'est un array"],
     "name" => ["?string", null, "identifiant de la valeur"],
     "pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"],
     "header" => ["?string", null, "nom de l'en-tête s'il faut présenter cette donnée dans un tableau"],
diff --git a/php/src/ref/schema/ref_types.php b/php/src/ref/schema/ref_types.php
index 24973d5..d7ce1d4 100644
--- a/php/src/ref/schema/ref_types.php
+++ b/php/src/ref/schema/ref_types.php
@@ -6,5 +6,6 @@ class ref_types {
     "boolean" => "bool",
     "integer" => "int",
     "flt" => "float", "double" => "float", "dbl" => "float",
+    "func" => "callable", "function" => "callable",
   ];
 }
diff --git a/php/src/tools/pman/ComposerFile.php b/php/src/tools/pman/ComposerFile.php
index 5a48a1c..d85a957 100644
--- a/php/src/tools/pman/ComposerFile.php
+++ b/php/src/tools/pman/ComposerFile.php
@@ -138,6 +138,28 @@ class ComposerFile {
     }
   }
 
+  function getLocalDeps(): array {
+    $deps = cl::merge(array_keys($this->getRequires()), array_keys($this->getRequireDevs()));
+    $paths = [];
+    foreach ($deps as $dep) {
+      $path = cl::get(self::PATHS, $dep, $dep);
+      $path = str_replace("/", "-", $path);
+      $path = "../$path";
+      $paths[$dep] = $path;
+    }
+    $repositories = $this->getRepositories();
+    $localDeps = [];
+    foreach ($deps as $dep) {
+      foreach ($repositories as $key => $repository) {
+        if ($repository["type"] === "path" && $repository["url"] === $paths[$dep]) {
+          $localDeps[$dep] = $repository["url"];
+          break;
+        }
+      }
+    }
+    return $localDeps;
+  }
+
   function print(): void {
     $contents = json::with($this->data, json::INDENT_TABS);
     if ($contents) echo "$contents\n";
diff --git a/php/tests/clTest.php b/php/tests/clTest.php
new file mode 100644
index 0000000..d2b834f
--- /dev/null
+++ b/php/tests/clTest.php
@@ -0,0 +1,37 @@
+<?php
+namespace nulib;
+
+use nulib\tests\TestCase;
+
+class clTest extends TestCase {
+  function checkKeys(?array $array, ?array $ref, bool $sameKeys, array $expectedKeys, array $expectedRemains, array $expectedMissings): void {
+    self::assertSame($sameKeys, cl::same_keys($array, $ref, $keys, $remains, $missings), "sameKeys");
+    self::assertSame($expectedKeys, $keys, "keys");
+    self::assertSame($expectedRemains, $remains, "remains");
+    self::assertSame($expectedMissings, $missings, "missings");
+  }
+  function test_same_keys() {
+    $array = ["a" => 42, "b" => "tesxt"]; $arrayKeys = array_keys($array);
+    $xarray = ["parasite0", "a" => 42, "parasite1", "b" => "tesxt"]; $xarrayKeys = array_keys($array);
+    $ref = ["a" => "int", "b" => "text"]; $refKeys = array_keys($ref);
+    $missingArray = ["c" => true]; $missingKeys = array_keys($missingArray);
+    $missingRef = ["c" => "bool"]; $missingKeys = array_keys($missingRef);
+
+    $this->checkKeys(null, null, true, [], [], []);
+    $this->checkKeys(null, [], true, [], [], []);
+    $this->checkKeys([], null, true, [], [], []);
+    $this->checkKeys([], [], true, [], [], []);
+
+    $this->checkKeys(null, $ref, false, [], [], $refKeys);
+    $this->checkKeys([], $ref, false, [], [], $refKeys);
+
+    $this->checkKeys($array, null, true, [], $arrayKeys, []);
+    $this->checkKeys($array, [], true, [], $arrayKeys, []);
+
+    $this->checkKeys($array, $ref, true, $arrayKeys, [], []);
+    $this->checkKeys(cl::merge($array, $missingArray), $ref, true, $arrayKeys, $missingKeys, []);
+    $this->checkKeys($array, cl::merge($ref, $missingRef), false, $arrayKeys, [], $missingKeys);
+
+    $this->checkKeys($xarray, $ref, false, $arrayKeys, [0, 1], []);
+  }
+}
diff --git a/php/tests/php/access/KeyAccessTest.php b/php/tests/php/access/KeyAccessTest.php
deleted file mode 100644
index dc5bd4c..0000000
--- a/php/tests/php/access/KeyAccessTest.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-namespace nulib\php\access;
-
-use nulib\tests\TestCase;
-use nulib\wip\php\access\KeyAccess;
-use stdClass;
-
-class KeyAccessTest extends TestCase {
-  function testAccess() {
-    $default = new stdClass();
-    $array = ["null" => null, "false" => false, "empty" => ""];
-
-    #
-    $a = new KeyAccess($array, "inexistant");
-    self::assertFalse($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $a = new KeyAccess($array, "null");
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame(null, $a->get($default));
-
-    $a = new KeyAccess($array, "false");
-    self::assertTrue($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $a = new KeyAccess($array, "empty");
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame("", $a->get($default));
-
-    #
-    $a = new KeyAccess($array, "null", ["allow_null" => false]);
-    self::assertTrue($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $a = new KeyAccess($array, "null", ["allow_null" => true]);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame(null, $a->get($default));
-
-    #
-    $a = new KeyAccess($array, "false", ["allow_false" => false]);
-    self::assertTrue($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $a = new KeyAccess($array, "false", ["allow_false" => true]);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame(false, $a->get($default));
-
-    #
-    $a = new KeyAccess($array, "empty", ["allow_empty" => false]);
-    self::assertTrue($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $a = new KeyAccess($array, "empty", ["allow_empty" => true]);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame("", $a->get($default));
-  }
-}
diff --git a/php/tests/php/access/ValueAccessTest.php b/php/tests/php/access/ValueAccessTest.php
deleted file mode 100644
index a7d08c9..0000000
--- a/php/tests/php/access/ValueAccessTest.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-namespace nulib\php\access;
-
-use nulib\tests\TestCase;
-use nulib\wip\php\access\ValueAccess;
-use stdClass;
-
-class ValueAccessTest extends TestCase {
-  function testAccess() {
-    $default = new stdClass();
-
-    #
-    $i = null;
-    $a = new ValueAccess($i);
-    self::assertFalse($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $i = false;
-    $a = new ValueAccess($i);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame(false, $a->get($default));
-
-    $i = "";
-    $a = new ValueAccess($i);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame("", $a->get($default));
-
-    #
-    $i = null;
-    $a = new ValueAccess($i, ["allow_null" => false]);
-    self::assertFalse($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $i = null;
-    $a = new ValueAccess($i, ["allow_null" => true]);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame(null, $a->get($default));
-
-    #
-    $i = false;
-    $a = new ValueAccess($i, ["allow_false" => false]);
-    self::assertTrue($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $i = false;
-    $a = new ValueAccess($i, ["allow_false" => true]);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame(false, $a->get($default));
-
-    #
-    $i = "";
-    $a = new ValueAccess($i, ["allow_empty" => false]);
-    self::assertTrue($a->exists());
-    self::assertFalse($a->available());
-    self::assertSame($default, $a->get($default));
-
-    $i = "";
-    $a = new ValueAccess($i, ["allow_empty" => true]);
-    self::assertTrue($a->exists());
-    self::assertTrue($a->available());
-    self::assertSame("", $a->get($default));
-  }
-}
diff --git a/php/tests/php/content/cTest.php b/php/tests/php/content/cTest.php
deleted file mode 100644
index 2f31b4a..0000000
--- a/php/tests/php/content/cTest.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-namespace nulib\php\content;
-
-use nulib\php\content\impl\html;
-use nulib\wip\web\content\v;
-use PHPUnit\Framework\TestCase;
-
-class cTest extends TestCase {
-  function testTo_string() {
-    self::assertSame("", c::to_string(null));
-    self::assertSame("", c::to_string(false));
-    self::assertSame("pouet&lt;q/&gt;", c::to_string("pouet<q/>"));
-    self::assertSame("pouet<q/>", c::to_string(["pouet<q/>"]));
-    self::assertSame("hello world", c::to_string(["hello", "world"]));
-    self::assertSame("hello1 world", c::to_string(["hello1", "world"]));
-    self::assertSame("hello<world>", c::to_string(["hello", "<world>"]));
-    self::assertSame("<hello>world", c::to_string(["<hello>", "world"]));
-    self::assertSame("hello,world", c::to_string(["hello,", "world"]));
-    self::assertSame("hello&nbsp;world", c::to_string(["hello&nbsp;", "world"]));
-    self::assertSame("hello. world", c::to_string(["hello.", "world"]));
-    self::assertSame("hello.<world>", c::to_string(["hello.", "<world>"]));
-
-    self::assertSame(
-      "<h1>title&lt;q/&gt;</h1><p>hello<nq/><span>brave&lt;q/&gt;</span><span>world<nq/></span></p>",
-      c::to_string([
-        [html::H1, "title<q/>"],
-        [html::P, [
-          "hello<nq/>",
-          [html::SPAN, "brave<q/>"],
-          [html::SPAN, ["world<nq/>"]],
-        ]],
-      ]));
-  }
-
-  function testXxx() {
-    $content = [[v::h1, "hello"]];
-    self::assertSame("<h1>hello</h1>", c::to_string($content));
-  }
-}
-
diff --git a/php/tests/php/content/impl/AContent.php b/php/tests/php/content/impl/AContent.php
deleted file mode 100644
index c53c376..0000000
--- a/php/tests/php/content/impl/AContent.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-namespace nulib\php\content\impl;
-
-use nulib\php\content\IContent;
-
-class AContent implements IContent {
-  function getContent(): iterable {
-    return ["<span>content</span>"];
-  }
-}
diff --git a/php/tests/php/content/impl/APrintable.php b/php/tests/php/content/impl/APrintable.php
deleted file mode 100644
index 7a75c2f..0000000
--- a/php/tests/php/content/impl/APrintable.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-namespace nulib\php\content\impl;
-
-use nulib\php\content\IPrintable;
-
-class APrintable implements IPrintable {
-  function print(): void {
-    echo "<p>printable</p>";
-  }
-}
diff --git a/php/tests/php/content/impl/ATag.php b/php/tests/php/content/impl/ATag.php
deleted file mode 100644
index b4cc2ed..0000000
--- a/php/tests/php/content/impl/ATag.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-namespace nulib\php\content\impl;
-
-use nulib\php\content\c;
-use nulib\php\content\IContent;
-
-class ATag implements IContent {
-  function __construct(string $tag, $content=null) {
-    $this->tag = $tag;
-    $this->content = $content;
-  }
-
-  protected $tag;
-  protected $content;
-
-  function getContent(): iterable {
-    return [
-      "<$this->tag>",
-      ...c::q($this->content),
-      "</$this->tag>",
-    ];
-  }
-}
diff --git a/php/tests/php/content/impl/html.php b/php/tests/php/content/impl/html.php
deleted file mode 100644
index 3567b2e..0000000
--- a/php/tests/php/content/impl/html.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-namespace nulib\php\content\impl;
-
-class html {
-  const H1 = [self::class, "h1"];
-  const DIV = [self::class, "div"];
-  const P = [self::class, "p"];
-  const SPAN = [self::class, "span"];
-
-  static function h1($content) { return new ATag("h1", $content); }
-  static function div($content) { return new ATag("div", $content); }
-  static function p($content) { return new ATag("p", $content); }
-  static function span($content) { return new ATag("span", $content); }
-}
diff --git a/php/tests/schema/_scalar/ScalarSchemaTest.php b/php/tests/schema/_scalar/ScalarSchemaTest.php
deleted file mode 100644
index e004168..0000000
--- a/php/tests/schema/_scalar/ScalarSchemaTest.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-namespace nulib\schema\_scalar;
-
-use nulib\tests\TestCase;
-use nulib\wip\schema\_scalar\ScalarSchema;
-use nulib\wip\schema\SchemaException;
-
-class ScalarSchemaTest extends TestCase {
-  const NULL_SCHEMA = [
-    "type" => [null],
-    "default" => null,
-    "title" => null,
-    "required" => false,
-    "nullable" => true,
-    "desc" => null,
-    "analyzer_func" => null,
-    "extractor_func" => null,
-    "parser_func" => null,
-    "normalizer_func" => null,
-    "messages" => null,
-    "formatter_func" => null,
-    "format" => null,
-    "" => ["scalar"],
-    "name" => null,
-    "pkey" => null,
-    "header" => null,
-    "composite" => null,
-  ];
-
-  static function schema(array $schema): array {
-    return array_merge(self::NULL_SCHEMA, $schema);
-  }
-
-  function testNormalize() {
-    self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize(null));
-    self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([]));
-    self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([null]));
-    self::assertException(SchemaException::class, function () {
-      ScalarSchema::normalize([[]]);
-    });
-    self::assertException(SchemaException::class, function () {
-      ScalarSchema::normalize([[null]]);
-    });
-
-    $string = self::schema(["type" => ["string"], "nullable" => false]);
-    self::assertSame($string, ScalarSchema::normalize("string"));
-    self::assertSame($string, ScalarSchema::normalize(["string"]));
-
-    $nstring = self::schema(["type" => ["string"]]);
-    self::assertSame($nstring, ScalarSchema::normalize(["?string"]));
-    self::assertSame($nstring, ScalarSchema::normalize(["?string|null"]));
-    self::assertSame($nstring, ScalarSchema::normalize(["string|null"]));
-    self::assertSame($nstring, ScalarSchema::normalize([["?string", "null"]]));
-    self::assertSame($nstring, ScalarSchema::normalize([["string", "null"]]));
-    self::assertSame($nstring, ScalarSchema::normalize([["string", null]]));
-
-    $key = self::schema(["type" => ["string", "int"], "nullable" => false]);
-    self::assertSame($key, ScalarSchema::normalize("string|int"));
-
-    $nkey = self::schema(["type" => ["string", "int"], "nullable" => true]);
-    self::assertSame($nkey, ScalarSchema::normalize("?string|int"));
-    self::assertSame($nkey, ScalarSchema::normalize("string|?int"));
-  }
-}
diff --git a/php/tests/schema/types/boolTest.php b/php/tests/schema/types/boolTest.php
deleted file mode 100644
index 8f990e3..0000000
--- a/php/tests/schema/types/boolTest.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-namespace nulib\schema\types;
-
-use Exception;
-use nulib\tests\TestCase;
-use nulib\schema\_scalar\ScalarValue;
-use nulib\schema\Schema;
-
-class boolTest extends TestCase {
-  function commonTests($destv, &$dest, callable $destvSetter): void {
-    $destv->set(true);
-    self::assertSame(true, $destv->get());
-    self::assertSame(true, $dest);
-    self::assertSame("Oui", $destv->format());
-    self::assertSame("Oui", $destv->format("OuiNonNull"));
-    self::assertSame("O", $destv->format("ON"));
-    self::assertSame("O", $destv->format("ONN"));
-
-    $destv->set(false);
-    self::assertSame(false, $destv->get());
-    self::assertSame(false, $dest);
-    self::assertSame("Non", $destv->format());
-    self::assertSame("Non", $destv->format("OuiNonNull"));
-    self::assertSame("N", $destv->format("ON"));
-    self::assertSame("N", $destv->format("ONN"));
-
-    $destv->set("yes");
-    self::assertSame(true, $destv->get());
-
-    $destv->set(" yes ");
-    self::assertSame(true, $destv->get());
-
-    $destv->set("12");
-    self::assertSame(true, $destv->get());
-
-    $destv->set(12);
-    self::assertSame(true, $destv->get());
-
-    $destv->set("no");
-    self::assertSame(false, $destv->get());
-
-    $destv->set(" no ");
-    self::assertSame(false, $destv->get());
-
-    $destv->set("0");
-    self::assertSame(false, $destv->get());
-
-    $destv->set(0);
-    self::assertSame(false, $destv->get());
-
-    $destv->set(12.34);
-    self::assertSame(true, $destv->get());
-
-    self::assertException(Exception::class, $destvSetter("a"));
-    self::assertException(Exception::class, $destvSetter([]));
-    self::assertException(Exception::class, $destvSetter(["a"]));
-
-  }
-
-  function testBool() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "bool");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-    self::assertException(Exception::class, $destvSetter(""));
-    self::assertException(Exception::class, $destvSetter(" "));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testNbool() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "?bool");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("Non", $destv->format());
-    self::assertSame("", $destv->format("OuiNonNull"));
-    self::assertSame("N", $destv->format("ON"));
-    self::assertSame("", $destv->format("ONN"));
-
-    $destv->set("");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("Non", $destv->format());
-    self::assertSame("", $destv->format("OuiNonNull"));
-    self::assertSame("N", $destv->format("ON"));
-    self::assertSame("", $destv->format("ONN"));
-
-    $destv->set(" ");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("Non", $destv->format());
-    self::assertSame("", $destv->format("OuiNonNull"));
-    self::assertSame("N", $destv->format("ON"));
-    self::assertSame("", $destv->format("ONN"));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-}
diff --git a/php/tests/schema/types/floatTest.php b/php/tests/schema/types/floatTest.php
deleted file mode 100644
index 193d407..0000000
--- a/php/tests/schema/types/floatTest.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-namespace nulib\schema\types;
-
-use Exception;
-use nulib\tests\TestCase;
-use nulib\schema\_scalar\ScalarValue;
-use nulib\schema\Schema;
-
-class floatTest extends TestCase {
-  function commonTests($destv, &$dest, callable $destvSetter): void {
-    $destv->set(12);
-    self::assertSame(12.0, $destv->get());
-    self::assertSame(12.0, $dest);
-    self::assertSame("12", $destv->format());
-    self::assertSame("0012", $destv->format("%04u"));
-
-    $destv->set("12");
-    self::assertSame(12.0, $destv->get());
-
-    $destv->set(" 12 ");
-    self::assertSame(12.0, $destv->get());
-
-    $destv->set(12.34);
-    self::assertSame(12.34, $destv->get());
-
-    $destv->set(true);
-    self::assertSame(1.0, $destv->get());
-
-    self::assertException(Exception::class, $destvSetter("a"));
-    self::assertException(Exception::class, $destvSetter([]));
-    self::assertException(Exception::class, $destvSetter(["a"]));
-  }
-
-  function testFloat() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "float");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-    self::assertException(Exception::class, $destvSetter(""));
-    self::assertException(Exception::class, $destvSetter(" "));
-
-    // valeur non requise donc retourne null
-    $destv->set(false);
-    self::assertNull($destv->get());
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testRequiredFloat() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, [
-      "float", null,
-      "required" => true,
-    ]);
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-    self::assertException(Exception::class, $destvSetter(""));
-    self::assertException(Exception::class, $destvSetter(" "));
-
-    // valeur requise donc lance une exception
-    self::assertException(Exception::class, $destvSetter(false));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testNfloat() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "?float");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set("");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set(" ");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    // valeur non requise donc retourne null
-    $destv->set(false);
-    self::assertNull($destv->get());
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testRequiredNfloat() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, [
-      "?float", null,
-      "required" => true,
-    ]);
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set("");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set(" ");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    // valeur requise donc lance une exception
-    self::assertException(Exception::class, $destvSetter(false));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-}
diff --git a/php/tests/schema/types/intTest.php b/php/tests/schema/types/intTest.php
deleted file mode 100644
index 29de7ce..0000000
--- a/php/tests/schema/types/intTest.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-namespace nulib\schema\types;
-
-use Exception;
-use nulib\tests\TestCase;
-use nulib\schema\_scalar\ScalarValue;
-use nulib\schema\Schema;
-
-class intTest extends TestCase {
-  function commonTests($destv, &$dest, callable $destvSetter): void {
-    $destv->set(12);
-    self::assertSame(12, $destv->get());
-    self::assertSame(12, $dest);
-    self::assertSame("12", $destv->format());
-    self::assertSame("0012", $destv->format("%04u"));
-
-    $destv->set("12");
-    self::assertSame(12, $destv->get());
-
-    $destv->set(" 12 ");
-    self::assertSame(12, $destv->get());
-
-    $destv->set(12.34);
-    self::assertSame(12, $destv->get());
-
-    $destv->set(true);
-    self::assertSame(1, $destv->get());
-
-    self::assertException(Exception::class, $destvSetter("a"));
-    self::assertException(Exception::class, $destvSetter([]));
-    self::assertException(Exception::class, $destvSetter(["a"]));
-  }
-
-  function testInt() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "int");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-    self::assertException(Exception::class, $destvSetter(""));
-    self::assertException(Exception::class, $destvSetter(" "));
-
-    // valeur non requise donc retourne null
-    $destv->set(false);
-    self::assertNull($destv->get());
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testRequiredInt() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, [
-      "int", null,
-      "required" => true,
-    ]);
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-    self::assertException(Exception::class, $destvSetter(""));
-    self::assertException(Exception::class, $destvSetter(" "));
-
-    // valeur requise donc lance une exception
-    self::assertException(Exception::class, $destvSetter(false));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testNint() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "?int");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set("");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set(" ");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    // valeur non requise donc retourne null
-    $destv->set(false);
-    self::assertNull($destv->get());
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testRequiredNint() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, [
-      "?int", null,
-      "required" => true,
-    ]);
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set("");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    $destv->set(" ");
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    // valeur requise donc lance une exception
-    self::assertException(Exception::class, $destvSetter(false));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-}
diff --git a/php/tests/schema/types/strTest.php b/php/tests/schema/types/strTest.php
deleted file mode 100644
index 45eda17..0000000
--- a/php/tests/schema/types/strTest.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-namespace nulib\schema\types;
-
-use Exception;
-use nulib\tests\TestCase;
-use nulib\wip\schema\_scalar\ScalarValue;
-use nulib\wip\schema\Schema;
-
-class strTest extends TestCase {
-  function commonTests($destv, &$dest, callable $destvSetter): void {
-    $destv->set("");
-    self::assertSame("", $destv->get());
-    self::assertSame("", $dest);
-
-    $destv->set(" ");
-    self::assertSame(" ", $destv->get());
-    self::assertSame(" ", $dest);
-
-    $destv->set("a");
-    self::assertSame("a", $destv->get());
-    self::assertSame("a", $dest);
-
-    $destv->set("12");
-    self::assertSame("12", $destv->get());
-
-    $destv->set(" 12 ");
-    self::assertSame(" 12 ", $destv->get());
-
-    $destv->set(12);
-    self::assertSame("12", $destv->get());
-
-    $destv->set(12.34);
-    self::assertSame("12.34", $destv->get());
-
-    $destv->set(true);
-    self::assertSame("1", $destv->get());
-
-    self::assertException(Exception::class, $destvSetter([]));
-    self::assertException(Exception::class, $destvSetter(["a"]));
-  }
-
-  function testStr() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "string");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-
-    // valeur non requise donc retourne null
-    $destv->set(false);
-    self::assertNull($destv->get());
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testRequiredStr() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, [
-      "string", null,
-      "required" => true,
-    ]);
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    self::assertException(Exception::class, $destvSetter(null));
-
-    // valeur requise donc lance une exception
-    self::assertException(Exception::class, $destvSetter(false));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testNstr() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, "?string");
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    // valeur non requise donc retourne null
-    $destv->set(false);
-    self::assertNull($destv->get());
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-
-  function testRequiredNstr() {
-    /** @var ScalarValue $destv */
-    Schema::nv($destv, $dest, null, $schema, [
-      "?string", null,
-      "required" => true,
-    ]);
-    $destvSetter = function($value) use($destv) {
-      return function() use($destv, $value) {
-        $destv->set($value);
-      };
-    };
-
-    $destv->set(null);
-    self::assertNull($destv->get());
-    self::assertNull($dest);
-    self::assertSame("", $destv->format());
-
-    // valeur requise donc lance une exception
-    self::assertException(Exception::class, $destvSetter(false));
-
-    $this->commonTests($destv, $dest, $destvSetter);
-  }
-}
diff --git a/php/tests/schema/types/unionTest.php b/php/tests/schema/types/unionTest.php
deleted file mode 100644
index c208087..0000000
--- a/php/tests/schema/types/unionTest.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-namespace nulib\schema\types;
-
-use nulib\tests\TestCase;
-use nulib\schema\_scalar\ScalarValue;
-use nulib\schema\Schema;
-
-class unionTest extends TestCase {
-  function testUnionTypes() {
-    ## l'ordre des types doit être respecté
-    
-    # string puis int
-    /** @var ScalarValue $siv */
-    Schema::nv($siv, $si, null, $sis, "string|int");
-
-    $siv->set("12");
-    self::assertSame("12", $si);
-    $siv->set(12);
-    self::assertSame(12, $si);
-
-    # int puis string
-    Schema::nv($isv, $is, null, $iss, "int|string");
-
-    $isv->set("12");
-    self::assertSame("12", $is);
-    $isv->set(12);
-    self::assertSame(12, $is);
-  }
-}
diff --git a/runphp/runphp b/runphp/runphp
index 1a03b5f..4bfe2ed 100755
--- a/runphp/runphp
+++ b/runphp/runphp
@@ -455,15 +455,18 @@ OPTIONS
 
 function host_docker_run() {
     # lancer une commande avec docker
-    SOPTS=+w:
-    LOPTS=help,chdir:,no-use-rslave
-    args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
+    if [ "$1" == composer ]; then
+        : # pas d'analyse d'argument pour composer
+    else
+        SOPTS=+w:
+        LOPTS=help,chdir:,no-use-rslave
+        args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
 
-    while [ $# -gt 0 ]; do
-        case "$1" in
-        --) shift; break;;
-        --help)
-            eecho "\
+        while [ $# -gt 0 ]; do
+            case "$1" in
+            --) shift; break;;
+            --help)
+                eecho "\
 runphp: lancer une commande dans un environnement PHP déterminé
 
 USAGE
@@ -490,14 +493,15 @@ OPTIONS
         aller dans le répertoire spécifié avant de lancer la commande
     --no-use-rslave
         paramètre montage des volumes"
-            exit 0
-            ;;
-        -w|--chdir) shift; Chdir="$1";;
-        --no-use-rslave) UseRslave=;;
-        *) die "$1: option non configurée";;
-        esac
-        shift
-    done
+                exit 0
+                ;;
+            -w|--chdir) shift; Chdir="$1";;
+            --no-use-rslave) UseRslave=;;
+            *) die "$1: option non configurée";;
+            esac
+            shift
+        done
+    fi
 
     args=(
         run -it --rm
@@ -573,20 +577,24 @@ function container_exec() {
         fi
     fi
 
-    SOPTS=+w:
-    LOPTS=chdir:
-    args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
+    if [ "$1" == composer ]; then
+        : # pas d'analyse d'argument pour composer
+    else
+        SOPTS=+w:
+        LOPTS=chdir:
+        args="$(getopt -n "$MYNAME" -o "$SOPTS" -l "$LOPTS" -- "$@")" || exit 1; eval "set -- $args"
 
-    chdir=
-    action=
-    while [ $# -gt 0 ]; do
-        case "$1" in
-        --) shift; break;;
-        -w|--chdir) shift; chdir="$1";;
-        *) die "$1: option non configurée";;
-        esac
-        shift
-    done
+        chdir=
+        action=
+        while [ $# -gt 0 ]; do
+            case "$1" in
+            --) shift; break;;
+            -w|--chdir) shift; chdir="$1";;
+            *) die "$1: option non configurée";;
+            esac
+            shift
+        done
+    fi
 
     if [ $# -eq 0 ]; then
         die "no command specified"