src/Ctx.ps1
|
# --------------------------------------------------------------------------- # ctx -- point d'entree unique, et repartiteur de sous-commandes # --------------------------------------------------------------------------- # # THE PROBLEM # # The module shipped twelve commands spelled `ctx-doctor`, `ctx-list`, # `ctx-new`. That hyphen is a PowerShell habit; every other CLI a developer # touches -- git, docker, gh, npm, cargo, supabase, vercel -- separates the # subcommand with a SPACE. So `ctx doctor` is what fingers type first. # # Until 1.5.0 it answered: # # Test-DevContext: A positional parameter cannot be found that accepts # argument 'doctor'. # # Measured on 17 Aug 2026. Three things wrong with that sentence, and the # third is the one that matters: it names `Test-DevContext`, an internal # function the user has never seen, never typed, and cannot find in any # document. A first contact that names your internals teaches the reader that # the tool is not for them. # # THE RULE THIS FILE ESTABLISHES # # ctx-<nom> et ctx <nom> sont la MEME chose. # # Two spellings, one implementation, no second code path to keep in sync -- # the table below is the only place the mapping exists, and the hyphenated # aliases are built FROM it in the psm1. # # WHY A WRAPPER RATHER THAN A PARAMETER ON Test-DevContext # # Adding `[Parameter(Position=0)][string]$Command` to an advanced function # does not work: `ctx doctor -Live` would then fail on -Live, because an # advanced function rejects a named parameter it does not declare, and # ValueFromRemainingArguments collects only POSITIONAL leftovers. The subcommand # would work and every one of its switches would break -- worse than today, # because it fails halfway. # # So `ctx` becomes a function with no param() block at all, reading $args. Same # doctrine as Invoke-DevGh / Invoke-DevVercel / Invoke-DevSupabase since 1.4.0, # and pinned by the same AST test: a param() block here would silently claim # -Verbose, -Debug and the other common parameters, making short options # ambiguous. See tests/Alias.Tests.ps1. # La table EST la definition. Les alias ctx-<nom> du psm1 en decoulent, donc # ajouter une entree ici suffit pour que les deux orthographes existent. $script:CtxSousCommandes = [ordered]@{ 'check' = 'Assert-DevContext' 'doctor' = 'Get-DevContextDoctor' 'editors' = 'Get-DevEditorList' 'end' = 'Close-DevContext' 'init' = 'Invoke-DevContextInit' 'list' = 'Get-DevContextList' 'mcp' = 'New-DevProjectMcp' 'new' = 'New-DevContext' 'off' = 'Clear-DevContext' 'root' = 'Set-DevContextRoot' 'sb' = 'Get-DevSupabaseMap' 'shortcut' = 'New-DevShortcut' 'who' = 'Resolve-DevContextForPath' } function Get-CtxSousCommandes { <# PURE. La table, copiee. Rendue en tant que fonction pour que les tests et les messages d'aide lisent UNE source, jamais une liste recopiee a la main qui se perimerait a la premiere sous-commande ajoutee. #> [CmdletBinding()] param() $copie = [ordered]@{} foreach ($k in $script:CtxSousCommandes.Keys) { $copie[$k] = $script:CtxSousCommandes[$k] } $copie } function Resolve-CtxSousCommande { <# PURE. Le premier argument de `ctx` designe-t-il une sous-commande ? Rend le nom de la fonction cible, ou $null -- et $null veut dire "ce n'est pas une sous-commande", pas "erreur". Trois cas rendent $null : - aucun argument -> `ctx` tout court, le verdict - un argument commencant par - -> `ctx -Quiet`, un switch du verdict - un mot inconnu -> l'appelant affiche l'aide Le test sur le tiret vient EN PREMIER : sans lui, `ctx -Quiet` serait lu comme la sous-commande "-quiet", introuvable, et le verdict -- la commande la plus utilisee du module -- afficherait une aide au lieu de repondre. #> [CmdletBinding()] param([AllowNull()][AllowEmptyString()][string]$Mot) if ([string]::IsNullOrWhiteSpace($Mot)) { return $null } if ($Mot.StartsWith('-')) { return $null } $cle = $Mot.Trim().ToLowerInvariant() if ($script:CtxSousCommandes.Contains($cle)) { return $script:CtxSousCommandes[$cle] } $null } function Show-CtxAide { <# L'ecran que voit quelqu'un qui s'est trompe de mot, ou qui tape `ctx help`. Ecrit avec Write-Host et non `throw` : un throw affiche une trace d'exception au-dessus du message utile, et c'est l'inverse de ce qu'il faut a quelqu'un dont la seule faute est d'avoir mal orthographie un mot. Meme parti pris que l'ecran "aucun contexte" de Test-DevContext. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Affiche une aide ; ne modifie aucun etat.')] [CmdletBinding()] param([AllowNull()][AllowEmptyString()][string]$Inconnu) Write-Host '' if ($Inconnu) { Write-Host " $(T 'ctx.sc.inconnue' $Inconnu)" -ForegroundColor Red Write-Host '' } Write-Host " $(T 'ctx.sc.titre')" -ForegroundColor Cyan Write-Host '' Write-Host " ctx" -ForegroundColor Green -NoNewline Write-Host " $(T 'ctx.sc.verdict')" -ForegroundColor DarkGray foreach ($k in $script:CtxSousCommandes.Keys) { Write-Host (" ctx {0}" -f $k) -ForegroundColor Green -NoNewline $pad = ' ' * [Math]::Max(1, 20 - $k.Length) Write-Host ("{0}{1}" -f $pad, (T ("ctx.sc.aide.$k"))) -ForegroundColor DarkGray } Write-Host '' Write-Host " $(T 'ctx.sc.tiret')" -ForegroundColor DarkGray Write-Host '' } function Invoke-DevCtx { <# Le point d'entree de l'alias `ctx`. AUCUN param() ni [CmdletBinding()] -- volontaire, et verifie par un test AST. Un bloc param ferait de cette fonction une fonction avancee : elle reclamerait alors -Verbose, -Debug, -ErrorAction et consorts, et toute option courte des sous-commandes deviendrait ambigue. C'est exactement la panne mesuree le 16 aout 2026 sur `gh api -i user`. Consequence : la repartition se fait a la main sur $args. #> $tous = @($args) $premier = if ($tous.Count -gt 0) { "$($tous[0])" } else { '' } # `ctx help` et `ctx -?` : l'aide, demandee explicitement. if ($premier -and ($premier.ToLowerInvariant() -in @('help', '-?', '--help'))) { Show-CtxAide return } $cible = Resolve-CtxSousCommande -Mot $premier if ($cible) { # Splat du reste : PowerShell relit alors -Live, -Force, etc. comme des # noms de parametres de la fonction cible. # # Le @() explicite n'est PAS decoratif. Ecrit # $reste = if ($n -gt 1) { ... } else { @() } # la branche vide est ENUMEREE par le pipeline, ne produit aucun objet, # et $reste vaut $null -- pas un tableau vide. Le splat passe alors un # argument positionnel $null, et `ctx list` echoue sur # "A positional parameter cannot be found that accepts argument $null". # Mesure le 17 aout 2026. Meme classe de defaut que Get-CtxVercelMots en # 1.4.0 : en PowerShell, un tableau qui traverse une expression se # deplie, et il faut le redemander a chaque fois. $reste = @() if ($tous.Count -gt 1) { $reste = @($tous[1..($tous.Count - 1)]) } # L'appel et le `return` sont deux instructions distinctes a dessein : en # "return & $cible @reste", une erreur de liaison dans l'appel empeche # le return de s'executer, et le flot tombait dans l'aide -- affichant # "sous-commande inconnue : list" juste apres avoir trouve `list`. & $cible @reste return } # Pas un mot connu. Deux cas seulement, et ils ne doivent PAS etre confondus. if ($premier -and -not $premier.StartsWith('-')) { # Un mot, mais pas des notres : l'utilisateur visait une sous-commande. Show-CtxAide -Inconnu $premier return } # Rien, ou uniquement des switches : c'est le verdict, comportement historique. Test-DevContext @tous } |