Pourquoi HagiCode a choisi execa pour gérer l'exécution de commandes CLI
Pourquoi HagiCode a choisi execa pour gérer l’exécution de commandes CLI
Dans les projets Node.js, l’utilisation directe de child_process pour exécuter des commandes externes présente des problèmes tels que des différences importantes entre plateformes et une gestion des erreurs incohérente. Cet article partage l’expérience pratique du projet HagiCode dans l’adoption d’execa, y compris les décisions de conception fondamentales et des exemples de code réels.
Contexte
Dans les projets Node.js, l’utilisation directe du module child_process pour exécuter des commandes externes est une pratique courante, mais cette méthode présente assez de problèmes :
- Grandes différences entre plateformes : les fichiers
.cmd/.batde Windows nécessitent un traitement spécial, les chemins contenant des espaces doivent être enveloppés de guillemets - Gestion des erreurs incohérente : les formats de message d’erreur de
execFile,spawn,execFileSyncvarient, rendant difficile un traitement unifié - Traitement des flux fastidieux : nécessite une gestion manuelle de la collecte et du tampon des flux stdout/stderr
- Gestion complexe des délais d’attente et des signaux : nécessite du code supplémentaire pour implémenter l’annulation de commande par timeout et le traitement des signaux de processus
Les applications Hagiscript et Desktop du projet HagiCode doivent toutes deux exécuter un grand nombre de commandes CLI externes (npm, node, PowerShell, etc.), et l’utilisation directe de child_process a entraîné une duplication de code et des coûts de maintenance élevés.
Pour résoudre ces problèmes, nous avons pris une décision : introduire execa comme solution unifiée d’exécution de commandes. Les changements apportés par cette décision sont en réalité plus importants que vous ne l’imaginez — j’en parlerai plus en détail tout à l’heure.
À propos de HagiCode
La solution partagée dans cet article provient de notre expérience pratique dans le projet HagiCode. HagiCode est un projet d’assistant de code IA qui doit exécuter un grand nombre de commandes externes dans plusieurs sous-projets (le moteur de script Hagiscript et l’application de bureau Desktop). Cette complexité multi-langage et multi-plateforme est peut-être la raison directe pour laquelle nous avons introduit execa.
Si vous trouvez que la solution partagée dans cet article a de la valeur, cela montre que nos capacités d’ingénierie sont assez bonnes — alors HagiCode lui-même mérite également qu’on s’y intéresse.
Pourquoi choisir execa ?
execa est une bibliothèque mature d’exécution de processus qui résout les problèmes fondamentaux de child_process :
- Cohérence multi-plateforme : gère automatiquement les commandes shim Windows, sans nécessiter de détection manuelle des fichiers
.cmd - Gestion unifiée des erreurs : objets d’erreur standardisés, incluant exitCode, signal, timedOut, stdout, stderr
- Meilleure conception d’API : prend en charge Promise API, AbortSignal pour l’annulation, le traitement des flux
- Sécurité : maintient les limites des paramètres, évite les risques d’injection de commandes
Ces caractéristiques sont exactement ce dont nous avions besoin lors du développement de HagiCode. Hagiscript doit exécuter des commandes npm sur différentes plateformes, Desktop doit appeler PowerShell et divers outils de développement, et la cohérence multi-plateforme d’execa a considérablement réduit notre code d’adaptation de plateforme. Après tout, qui veut écrire du code de traitement spécial pour chaque plateforme ?
Décisions de conception fondamentales
Les implémentations des deux projets adoptent une couche d’encapsulage interne plutôt que d’appeler execa directement :
// Exécuteur unifié de Hagiscriptexport const runCommand: CommandRunner = async (command, args, options) => { const result = await execa(command, args, { /* normalized options */ }); return { /* normalized result */ };};Raisons :
- Maintenir des types d’erreur spécifiques au domaine (comme
NpmCommandError) - Faciliter l’injection d’exécuteurs simulés lors des tests
- Unifier la gestion des erreurs et la journalisation
- Permettre un remplacement facile de l’implémentation sous-jacente à l’avenir
Protection des limites de paramètres
Les deux implémentations mettent l’accent sur les tableaux de paramètres plutôt que sur les chaînes shell :
// Correct : limites de paramètres clairesawait runCommand('npm', ['install', '@scope/package@1.0.0']);
// Incorrect : risque d'injection facileawait execa(`npm install @scope/package@1.0.0`, { shell: true });Cela évite les problèmes de sécurité liés à la citation, à l’échappement et à l’injection des paramètres. Dans HagiCode, nous devons souvent traiter des paramètres saisis par l’utilisateur tels que des noms de paquets et des noms de scripts, et l’utilisation de tableaux de paramètres peut prévenir efficacement les injections de commandes. Après tout, la sécurité, une fois qu’elle pose problème, c’est un gros problème.
Solution de Hagiscript
Le sous-projet Hagiscript de HagiCode a créé le module runtime/command-launch.ts, fournissant :
- Exécuteur unifié : fonction
runCommandencapsulant execa - Résultats normalisés : interface
CommandResult - Erreurs normalisées : classe
CommandExecutionError - Fonctions auxiliaires de compatibilité :
normalizeCommandPath,requiresShellLaunch
export interface CommandResult { command: string; args: string[]; stdout: string; stderr: string; exitCode?: number; signal?: string; timedOut?: boolean;}
export class CommandExecutionError extends Error { readonly context: CommandFailureContext;}Cette abstraction permet à Hagiscript de traiter uniformément toutes les commandes externes, qu’il s’agisse d’installer des dépendances npm ou d’exécuter des scripts node. Disons simplement qu’avec une interface unifiée, le code est beaucoup plus agréable à écrire.
Solution de Desktop
Le sous-projet Desktop de HagiCode a créé le module utils/cli-executor.ts, fournissant :
- Options d’exécution :
CliExecutorOptionsprend en charge le timeout, l’annulation, les variables d’environnement - Classification des résultats :
CliExecutionResultcontient les états de succès/échec - Traitement des flux :
executeCliStreamingprend en charge les rappels de sortie en temps réel - Classification des erreurs :
CliFailureKinddistingue les types d’échec tels que sortie, timeout, annulation
export async function executeCli(options: CliExecutorOptions): Promise<CliExecutionResult>export async function executeCliStreaming(options: CliExecutorOptions): Promise<CliExecutionResult>Desktop doit afficher la progression de l’exécution des commandes dans l’interface utilisateur, la fonctionnalité de traitement en flux devient donc utile. Les utilisateurs peuvent voir la sortie de npm install en temps réel au lieu d’attendre la fin de l’exécution de la commande pour voir le résultat. Cette expérience, comment dire, une fois qu’on l’a utilisée, on ne peut plus s’en passer.
Exemples d’utilisation
Exécution de commandes dans Hagiscript
import { runCommand } from '../runtime/command-launch.js';
// Exécution simpleconst result = await runCommand('node', ['--version']);console.log(result.stdout); // 'v20.0.0'
// Exécution avec optionsconst installResult = await runCommand('npm', ['install', 'express'], { cwd: '/project/path', env: { NODE_ENV: 'development' }, timeoutMs: 30000});Exécution de commandes dans Desktop
import { executeCli, executeCliStreaming } from './utils/cli-executor.js';
// Exécution tamponnéeconst result = await executeCli({ command: 'npm', args: ['list', '--json'], cwd: projectPath, timeoutMs: 5000,});
if (result.success) { console.log(result.stdout);} else { console.error(result.error?.message);}
// Exécution en fluxawait executeCliStreaming({ command: 'npm', args: ['install'], onOutput: (type, data) => { console.log(`[${type}]`, data); }});Gestion des erreurs
try { await runCommand('npm', ['install', 'invalid-package']);} catch (error) { if (error instanceof CommandExecutionError) { console.error('Command failed:', error.context.command); console.error('Exit code:', error.context.exitCode); console.error('Stderr:', error.context.stderr); }}La gestion unifiée des erreurs nous permet de fournir une meilleure expérience utilisateur dans HagiCode. Par exemple, lorsque l’installation npm échoue, nous pouvons extraire le message d’erreur spécifique à afficher à l’utilisateur, plutôt que d’afficher un message générique “échec de l’exécution de la commande”. Après tout, lorsque les utilisateurs voient le message d’erreur spécifique, ils savent au moins où se trouve le problème.
Stratégie de test
Les deux projets prennent en charge l’injection de dépendances, facilitant les tests :
// Code de productionasync function installPackage(pkg: string, runCommand = defaultRunCommand) { return runCommand('npm', ['install', pkg]);}
// Code de testit('installs package', async () => { const mockRunCommand = vi.fn().mockResolvedValue({ stdout: 'installed', stderr: '', exitCode: 0 }); await installPackage('test-pkg', mockRunCommand); expect(mockRunCommand).toHaveBeenCalledWith('npm', ['install', 'test-pkg']);});Cette conception rend les tests de HagiCode plus fiables et plus rapides. Nous n’avons pas besoin d’exécuter réellement des commandes npm dans les tests, il suffit de simuler l’exécuteur pour qu’il retourne le résultat attendu. Les tests tournent vite, l’humeur de développement est naturellement meilleure.
Précautions
Dans la pratique de HagiCode, nous avons résumé les précautions suivantes :
- Maintenir la séparation des paramètres : passez toujours la commande et les paramètres comme des éléments de tableau distincts
- Utiliser avec prudence le mode shell : utilisez
shell: trueuniquement lorsque nécessaire, comme pour les tuyaux ou les redirections - Gérer les timeouts : définissez
timeoutMspour les commandes qui peuvent se bloquer - Taille du tampon : pour les sorties volumineuses, envisagez de définir
maxBuffer - Chemins Windows : execa gère automatiquement les shims
.cmd, aucune détection manuelle nécessaire - Annulation d’opération : utilisez
AbortSignalplutôt quekill()manuel - Classification des erreurs : distinguez les scénarios d’échec de démarrage du processus, échec d’exécution, timeout, annulation, etc.
Ce sont tous des pières que nous avons rencontrés dans le développement réel, cela peut peut-être vous aider à éviter quelques détours.
Pièges courants
// Incorrect : la concaténation de chaînes peut injecterawait execa(`npm install ${userInput}`, { shell: true });
// Correct : tableau de paramètresawait execa('npm', ['install', userInput]);
// Incorrect : ignorer le timeoutawait execa('npm', ['install', 'heavy-package']);
// Correct : définir le timeoutawait execa('npm', ['install', 'heavy-package'], { timeout: 60000 });
// Incorrect : supposer que le code de sortie est 0const result = await execa('npm', ['install']);
// Correct : vérifier l'échectry { await execa('npm', ['install']);} catch (error) { // Gérer l'échec}Ces pièges, disons-le, sont des larmes. Après tout, qui n’a pas rencontré quelques pièges en environnement de production ?
Conclusion
Après l’introduction d’execa, la qualité du code et la maintenabilité du projet HagiCode en matière d’exécution de commandes se sont considérablement améliorées :
- Cohérence multi-plateforme : plus besoin d’écrire du code de traitement spécial pour Windows
- Gestion unifiée des erreurs : messages d’erreur structurés, faciles à afficher et à analyser
- Meilleure testabilité : grâce à l’injection de dépendances, il est facile de simuler l’exécution de commandes
- Traitement plus sûr des paramètres : utilise des tableaux de paramètres pour éviter les risques d’injection
Si vous avez également besoin d’exécuter des commandes externes dans des projets Node.js, je vous recommande fortement d’essayer execa. La solution partagée dans cet article est le résultat de nos pièges réels et de nos optimisations réelles lors du développement de HagiCode, j’espère que cela vous sera utile.
Après tout, les bons outils méritent d’être connus de plus de monde…
Références
- Documentation officielle d’execa
- Documentation Node.js child_process
- Dépôt GitHub HagiCode
- Site officiel HagiCode
Si cet article vous aide :
- Venez mettre une étoile sur GitHub : github.com/HagiCode-org/site
- Visitez le site officiel pour en savoir plus : hagicode.com
- Regardez la démonstration pratique de 30 minutes : www.bilibili.com/video/BV1pirZBuEzq/
- Installation en un clic : docs.hagicode.com/installation/docker-compose
- Installation rapide du bureau Desktop : hagicode.com/desktop/
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。