[{"data":1,"prerenderedAt":34},["ShallowReactive",2],{"doc:fr:plugins\u002Fsdk":3},{"title":4,"lede":5,"html":6,"headings":7},"Le SDK","@escouade-dev\u002Fplugin sert à écrire un plugin d’Escouade en TypeScript : les types du protocole, un client qui le parle, dans Escouade en WASM ou sous Node, un faux Escouade pour les tests du plugin, et la commande escouade-plugin. Il est sous licence MIT, comme le schéma du protocole, et ne dépend de rien.","\u003Ch2 id=\"un-premier-plugin\">Un premier plugin\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-sh\">npm create escouade-plugin@latest mon-plugin\ncd mon-plugin\nnpm install\nnpm test            # ses fonctions, contre le faux Escouade, en mémoire\nnpm run validate    # le manifeste, champ par champ\nnpm run bench       # le banc de conformité, sur le vrai programme\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>La commande demande le genre de plugin ; \u003Ccode>--kind program\u003C\u002Fcode>, \u003Ccode>--kind rust\u003C\u002Fcode> ou \u003Ccode>--kind declarative\u003C\u002Fcode> le choisit sans demander, \u003Ccode class=\"key\">--id\u003C\u002Fcode> et \u003Ccode class=\"key\">--name\u003C\u002Fcode> donnent son identifiant et son nom.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Un programme\u003C\u002Fstrong> (\u003Ccode class=\"key\">program\u003C\u002Fcode>) : un plugin en TypeScript, avec ses tests. \u003Ccode class=\"key\">src\u002Fplugin.ts\u003C\u002Fcode> dit quand un ticket est prêt à tester et refuse les push forcés ; \u003Ccode class=\"key\">src\u002Fmain.ts\u003C\u002Fcode> le démarre ; \u003Ccode>npm run build\u003C\u002Fcode> réunit les deux dans \u003Ccode class=\"key\">dist\u002Fplugin.mjs\u003C\u002Fcode>, que nomme \u003Ccode class=\"key\">run.js\u003C\u002Fcode> dans le manifeste : Escouade le fait tourner dans l’app, en WASM, sans Node.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Un module en Rust\u003C\u002Fstrong> (\u003Ccode class=\"key\">rust\u003C\u002Fcode>) : le même plugin, sur \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Frust\u002F\">le kit Rust\u003C\u002Fa>, avec ses tests et son \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode>. Ni \u003Ccode class=\"key\">package.json\u003C\u002Fcode>, ni \u003Ccode class=\"key\">node_modules\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Un plugin déclaratif\u003C\u002Fstrong> (\u003Ccode class=\"key\">declarative\u003C\u002Fcode>) : un thème, décrit dans le manifeste. Rien ne s’exécute.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Pour l’essayer dans Escouade, charge son dossier dans Réglages › Plugins, en mode développeur. Escouade lance un plugin tel que son dépôt le contient, sans \u003Ccode>npm install\u003C\u002Fcode> : commite \u003Ccode class=\"key\">dist\u002F\u003C\u002Fcode> avant d’étiqueter une version.\u003C\u002Fp>\n\u003Ch2 id=\"le-client\">Le client\u003C\u002Fh2>\n\u003Cp>Le gabarit \u003Ccode class=\"key\">program\u003C\u002Fcode>, tel que \u003Ccode>npm create escouade-plugin\u003C\u002Fcode> l’écrit : ce que fait le plugin, à part pour que ses tests le pilotent sans process,\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Fprogram\u002Fsrc\u002Fplugin.ts\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F What the plugin does: its handlers, kept apart from main.ts so that the tests drive them without a process.\nimport { createPlugin } from &#39;@escouade-dev\u002Fplugin&#39;;\n\nexport const plugin = createPlugin();\n\n\u002F\u002F A fact of permissions.events: a ticket changed columns (its data says from and to).\nplugin.onEvent(&#39;ticket.column&#39;, (fact, p) =&gt; {\n  \u002F\u002F A ticket&#39;s fact says its ticket; the fact&#39;s type, one for every fact, leaves it optional.\n  if (fact.data.to === &#39;review&#39;) p.log.info(`${fact.ticket?.key ?? &#39;&#39;} is ready to test (${fact.project?.name ?? &#39;&#39;})`);\n});\n\n\u002F\u002F A permission an agent asks for, before the user is asked (permissions.decide). Answer within 500 ms.\nplugin.decidePermission(({ request }) =&gt; {\n  const command = typeof request.input?.command === &#39;string&#39; ? request.input.command : &#39;&#39;;\n  if (request.kind === &#39;command&#39; &amp;&amp; \u002F\\bgit\\s+push\\b.*(--force\\b|\\s-f\\b)\u002F.test(command)) {\n    return { decision: &#39;deny&#39;, message: &#39;No force push in this team: push a new commit instead.&#39; };\n  }\n  \u002F\u002F Leaves it to the user, as without the plugin.\n  return { decision: &#39;ask&#39; };\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>et le programme, que \u003Ccode class=\"key\">run.js\u003C\u002Fcode> nomme une fois réuni dans \u003Ccode class=\"key\">dist\u002Fplugin.mjs\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Fprogram\u002Fsrc\u002Fmain.ts\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F The program (run.js in escouade-plugin.json, once bundled): speaks the protocol to Escouade, which runs it inside the\n\u002F\u002F app in WASM; under Node too (the bench of escouade-plugin, a test), on stdin and stdout. start() finds which.\nimport { plugin } from &#39;.\u002Fplugin.ts&#39;;\n\nvoid plugin.start();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Aide\u003C\u002Fth>\n\u003Cth>Ce qu’elle fait\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode>onEvent(nom, fonction)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Appelle \u003Ccode class=\"key\">fonction\u003C\u002Fcode> à chaque fait \u003Ccode class=\"key\">nom\u003C\u002Fcode>, ou d’un motif qui finit par \u003Ccode class=\"key\">*\u003C\u002Fcode> (\u003Ccode class=\"key\">ticket.*\u003C\u002Fcode>), ou d’une liste. Les faits doivent être dans \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>. Une fonction qui échoue va au journal du plugin ; les autres sont appelées quand même.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>decidePermission(fonction)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Répond à \u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode> : \u003Ccode class=\"key\">allow\u003C\u002Fcode>, \u003Ccode class=\"key\">deny\u003C\u002Fcode> avec un \u003Ccode class=\"key\">message\u003C\u002Fcode>, ou \u003Ccode class=\"key\">ask\u003C\u002Fcode>, qui laisse la main à l’utilisateur. Sans fonction, la réponse est \u003Ccode class=\"key\">ask\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">secret(clé)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Demande à Escouade l’un de ses secrets (\u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>) ; \u003Ccode class=\"key\">null\u003C\u002Fcode> si aucun n’est gardé. Une autre clé échoue par l’erreur \u003Ccode class=\"key\">-32001\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">settings(projet?)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Demande ses réglages (\u003Ccode class=\"key\">settings\u002Fget\u003C\u002Fcode>) ; avec l’id d’un projet, ceux que son \u003Ccode class=\"key\">.escouade.yml\u003C\u002Fcode> lui donne (\u003Ccode class=\"key\">plugins.&lt;id&gt;\u003C\u002Fcode>) passent devant.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>log.debug\u002Finfo\u002Fwarn\u002Ferror(message)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une ligne de son journal (\u003Ccode class=\"key\">log\u003C\u002Fcode>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">file(chemin)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Lit un fichier de son dossier en texte (\u003Ccode class=\"key\">files\u002Fread\u003C\u002Fcode>), un sous-dossier compris (\u003Ccode class=\"key\">data\u002Fwords.txt\u003C\u002Fcode>), 256 Ko au plus, sous Node comme en WASM : jamais son manifeste, un fichier caché ou un chemin qui en sort (\u003Ccode class=\"key\">RpcError\u003C\u002Fcode> \u003Ccode class=\"key\">ERRORS.refused\u003C\u002Fcode>) ; un fichier absent échoue par \u003Ccode class=\"key\">ERRORS.notFound\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>fs.read\u002Ftext\u002Fwrite\u002Fstat\u002Flist\u002Fcanon(fichier)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Les dossiers de \u003Ccode class=\"key\">permissions.files\u003C\u002Fcode>, en WASM (\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F#les-fichiers\">les fichiers\u003C\u002Fa>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>runProgram(params, onOut?)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Lance une ligne de \u003Ccode class=\"key\">permissions.commands\u003C\u002Fcode>, en WASM : \u003Ccode class=\"key\">exited\u003C\u002Fcode>, \u003Ccode class=\"key\">write\u003C\u002Fcode>, \u003Ccode class=\"key\">close\u003C\u002Fcode>, \u003Ccode class=\"key\">kill\u003C\u002Fcode> (\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F#les-programmes-proc\">les programmes\u003C\u002Fa>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>openStream(p, url, init?)\u003C\u002Fcode>, \u003Ccode>openSocket(p, url, headers?)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Importées à part : une réponse lue au fil de l’eau et un WebSocket, en WASM (\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F#le-reseau\">le réseau\u003C\u002Fa>).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>handle(méthode, fonction)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Sert une autre méthode (un point d’extension à venir). Lance \u003Ccode>new RpcError(code, message)\u003C\u002Fcode> pour répondre une erreur.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>onInitialize(fonction)\u003C\u002Fcode>, \u003Ccode class=\"key\">onShutdown(fonction)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Appelée à \u003Ccode class=\"key\">initialize\u003C\u002Fcode>, ou avant la réponse à \u003Ccode class=\"key\">shutdown\u003C\u002Fcode> (800 ms au plus : Escouade attend une seconde).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>call(méthode, params)\u003C\u002Fcode>, \u003Ccode>notify(méthode, params)\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un appel ou une notification à Escouade, typés pour ses méthodes.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">start()\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Parle le protocole à Escouade : à son hôte WASM quand l’app fait tourner le plugin, sur l’entrée et la sortie standard sous Node (le banc, un test) ; le même code des deux côtés. Termine le plugin après \u003Ccode class=\"key\">shutdown\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Le client répond à \u003Ccode class=\"key\">initialize\u003C\u002Fcode> par la version 1 du protocole, quelle que soit celle qu’on lui offre (Escouade arrête un plugin qui ne parle pas la sienne), sert les lots, répond \u003Ccode class=\"key\">-32601\u003C\u002Fcode> à une méthode qu’il ne connaît pas, et n’écrit rien d’autre que le protocole sur sa sortie standard : écris tes traces sur la sortie d’erreur, qui va au journal.\u003C\u002Fp>\n\u003Ch2 id=\"les-types\">Les types\u003C\u002Fh2>\n\u003Cp>Les types du protocole viennent d’un seul schéma, \u003Ca href=\"\u002Fschemas\u002Fplugin-protocol-1.json\">\u003Ccode>plugin-protocol-1.json\u003C\u002Fcode>\u003C\u002Fa> : ses méthodes, les paramètres et la réponse de chacune, leurs délais, les faits et les codes d’erreur. Le TypeScript du SDK en est généré, les types du manifeste de \u003Ca href=\"\u002Fschemas\u002Fplugin-1.json\">\u003Ccode class=\"key\">plugin-1.json\u003C\u002Fcode>\u003C\u002Fa>, et les tests d’Escouade tiennent le cœur au même schéma : un changement d’un côté sans l’autre fait échouer les tests.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Type\u003C\u002Fth>\n\u003Cth>Ce que c’est\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">Fact\u003C\u002Fcode>, \u003Ccode class=\"key\">FactName\u003C\u002Fcode>, \u003Ccode class=\"key\">FACT_NAMES\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un fait (\u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode>) : son nom, sa version, quand, et le projet, le ticket et l’agent par leurs ids et leurs noms.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">DecideParams\u003C\u002Fcode>, \u003Ccode class=\"key\">PermissionRequest\u003C\u002Fcode>, \u003Ccode class=\"key\">Decision\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une autorisation à décider, et la réponse.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">InitializeParams\u003C\u002Fcode>, \u003Ccode class=\"key\">InitializeResult\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>La poignée de main.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ToPlugin\u003C\u002Fcode>, \u003Ccode class=\"key\">ToEscouade\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Chaque méthode, ses paramètres et sa réponse, par sens.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">TIMEOUTS_MS\u003C\u002Fcode>, \u003Ccode class=\"key\">DEFAULT_TIMEOUT_MS\u003C\u002Fcode>, \u003Ccode class=\"key\">ERRORS\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Les délais des appels d’Escouade, et les codes d’erreur.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">Manifest\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">escouade-plugin.json\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>@escouade-dev\u002Fplugin\u002Fmanifest-check\u003C\u002Fcode> donne les contrôles du manifeste que font le store et Escouade : \u003Ccode class=\"key\">validate(manifeste)\u003C\u002Fcode> et \u003Ccode>message(erreur, 'fr')\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"les-tests\">Les tests\u003C\u002Fh2>\n\u003Cp>\u003Ccode>@escouade-dev\u002Fplugin\u002Ftesting\u003C\u002Fcode> donne un faux Escouade, en mémoire : il envoie au plugin les mêmes messages que l’app et le banc, sans lancer de process.\u003C\u002Fp>\n\u003Cp>Les tests du gabarit :\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Fprogram\u002Ftest\u002Fplugin.test.ts\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F The plugin&#39;s handlers, driven by the SDK&#39;s fake Escouade, in memory. `npm run bench` plays the real program.\nimport assert from &#39;node:assert\u002Fstrict&#39;;\nimport { test } from &#39;node:test&#39;;\nimport { connect } from &#39;@escouade-dev\u002Fplugin\u002Ftesting&#39;;\nimport { plugin } from &#39;..\u002Fsrc\u002Fplugin.ts&#39;;\n\ntest(&#39;it speaks version 1 of the protocol&#39;, async () =&gt; {\n  const escouade = connect(plugin);\n  const hello = await escouade.initialize();\n  assert.equal(hello.protocolVersion, 1);\n});\n\ntest(&#39;a ticket moved to review is said in the journal&#39;, async () =&gt; {\n  const escouade = connect(plugin);\n  await escouade.initialize();\n  await escouade.fact(&#39;ticket.column&#39;, { ticket: { key: &#39;APP-7&#39; }, data: { from: &#39;doing&#39;, to: &#39;review&#39; } });\n  assert.deepEqual(escouade.logs, [{ level: &#39;info&#39;, message: &#39;APP-7 is ready to test (Banc)&#39; }]);\n});\n\ntest(&#39;a force push is refused, the rest left to the user&#39;, async () =&gt; {\n  const escouade = connect(plugin);\n  await escouade.initialize();\n  const push = await escouade.decide({ request: { input: { command: &#39;git push --force origin main&#39; } } });\n  assert.equal(push.decision, &#39;deny&#39;);\n  assert.equal((await escouade.decide()).decision, &#39;ask&#39;);\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>fact(nom, champs)\u003C\u002Fcode> envoie un fait et attend ses fonctions ; \u003Ccode class=\"key\">decide(champs)\u003C\u002Fcode> demande une décision ; \u003Ccode class=\"key\">call\u003C\u002Fcode> et \u003Ccode class=\"key\">notify\u003C\u002Fcode> envoient le reste. Les champs donnés remplacent ceux du banc (le projet « Banc », le ticket \u003Ccode class=\"key\">BEN-1\u003C\u002Fcode>, une commande \u003Ccode>npm test\u003C\u002Fcode>). Le faux garde ce que le plugin a écrit au journal (\u003Ccode class=\"key\">logs\u003C\u002Fcode>), les secrets qu’il a demandés (\u003Ccode class=\"key\">secretsAsked\u003C\u002Fcode>) et ses traces ; \u003Ccode class=\"key\">secrets\u003C\u002Fcode> donne leurs valeurs, et une clé hors d’elles est refusée, comme dans Escouade.\u003C\u002Fp>\n\u003Ch2 id=\"la-commande\">La commande\u003C\u002Fh2>\n\u003Cp>Le SDK installe \u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> dans les \u003Ccode class=\"key\">node_modules\u003C\u002Fcode> du plugin, pour ses scripts npm :\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Commande\u003C\u002Fth>\n\u003Cth>Ce qu’elle fait\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode>escouade-plugin validate [dossier]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Lit le manifeste et dit ce qui ne va pas, champ par champ, et vérifie que les programmes de \u003Ccode class=\"key\">run\u003C\u002Fcode> sont dans le dossier.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>escouade-plugin test [dossier]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">Le banc de conformité\u003C\u002Fa> : celui d’Escouade.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>escouade-plugin dev [dossier]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F#le-faux-escouade\">Le faux Escouade\u003C\u002Fa> : des faits, des décisions et des appels tapés, à un programme qui tourne.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>escouade-plugin pack [dossier]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>L’archive du plugin et son empreinte SHA-256.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>escouade-plugin build [dossier]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Compile \u003Ccode class=\"key\">run.js\u003C\u002Fcode> ou \u003Ccode class=\"key\">run.wasm\u003C\u002Fcode> en WASM, comme le store : \u003Ccode>packs\u002F&lt;id&gt;-&lt;version&gt;.wasm\u003C\u002Fcode> et son \u003Ccode class=\"key\">.sha256\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>escouade-plugin fetch [dossier]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Télécharge les dépendances d’un \u003Ccode class=\"key\">run.wasm\u003C\u002Fcode> depuis crates.io, sans rien lancer du plugin.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode class=\"key\">test\u003C\u002Fcode> et \u003Ccode class=\"key\">dev\u003C\u002Fcode> lancent l’\u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> livré avec Escouade, pour qu’un plugin soit jugé par le banc de l’app : celui de \u003Ccode class=\"key\">ESCOUADE_PLUGIN_BIN\u003C\u002Fcode>, sinon celui du \u003Ccode class=\"key\">PATH\u003C\u002Fcode>, sinon celui de l’app (\u003Ccode>%LOCALAPPDATA%\\Escouade\u003C\u002Fcode> sous Windows, \u003Ccode class=\"key\">Escouade.app\u003C\u002Fcode> sous macOS, \u003Ccode class=\"key\">\u002Fusr\u002Fbin\u003C\u002Fcode> avec le \u003Ccode class=\"key\">.deb\u003C\u002Fcode>). Sans lui, la commande dit où le trouver.\u003C\u002Fp>\n\u003Ch3 id=\"l-archive\">L’archive\u003C\u002Fh3>\n\u003Cp>\u003Ccode>escouade-plugin pack\u003C\u002Fcode> écrit \u003Ccode>packs\u002F&lt;id&gt;-&lt;version&gt;.tar.gz\u003C\u002Fcode> et son \u003Ccode class=\"key\">.sha256\u003C\u002Fcode>, avec tous les fichiers du dossier sauf \u003Ccode class=\"key\">.git\u003C\u002Fcode>, \u003Ccode class=\"key\">node_modules\u003C\u002Fcode> et \u003Ccode class=\"key\">packs\u003C\u002Fcode>. L’archive est la même, à l’octet près, pour les mêmes fichiers, sur n’importe quel système : les dates sont mises à zéro, et les programmes de \u003Ccode class=\"key\">run\u003C\u002Fcode> et les scripts qui commencent par \u003Ccode class=\"key\">#!\u003C\u002Fcode> sont exécutables. Un lien symbolique, ou une archive de plus de 50 Mo, est refusé, comme par le store.\u003C\u002Fp>\n\u003Cp>\u003Ccode>--target windows-x64\u003C\u002Fcode> (ou \u003Ccode class=\"key\">macos-arm64\u003C\u002Fcode>, \u003Ccode class=\"key\">linux-x64\u003C\u002Fcode>…) nomme l’archive d’un système, \u003Ccode>&lt;id&gt;-&lt;version&gt;-windows-x64.tar.gz\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"sans-node\">Sans Node\u003C\u002Fh2>\n\u003Cp>Un plugin en JS ou en TypeScript se publie par son bundle, nommé par \u003Ccode class=\"key\">run.js\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Fprogram\u002Fescouade-plugin.json\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">  &quot;run&quot;: { &quot;js&quot;: &quot;dist\u002Fplugin.mjs&quot; },\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Escouade le fait tourner \u003Cstrong>dans l’app, en WASM\u003C\u002Fstrong>, sans Node chez l’utilisateur : une instance isolée par plugin, qui garde son état d’un appel à l’autre comme un process. Le store compile le bundle à la soumission, et l’app télécharge ce module et le vérifie contre le \u003Ccode class=\"key\">version.json\u003C\u002Fcode> de la version ; un plugin installé depuis un dossier ou un dépôt git est compilé par l’app. La fenêtre d’installation dit « Tourne dans Escouade, isolé », et \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode> y est appliquée : \u003Ccode class=\"key\">fetch\u003C\u002Fcode> passe par Escouade, en https, vers ces hôtes seulement (voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fapi\u002F#un-programme-en-wasm\">le protocole\u003C\u002Fa>).\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Le même code sur les deux transports\u003C\u002Fstrong> : \u003Ccode class=\"key\">start()\u003C\u002Fcode> parle à l’hôte WASM d’Escouade quand l’app fait tourner le plugin, sur l’entrée et la sortie standard sous Node (ses tests, un plugin lancé par une commande). Un plugin écrit pour le SDK tourne sans changement des deux côtés.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ses fichiers\u003C\u002Fstrong> : \u003Ccode>readFileSync(new URL('..\u002Frules.json', globalThis._importMeta_.url), 'utf8')\u003C\u002Fcode> lit toujours un JSON de la racine de son dossier ; \u003Ccode class=\"key\">p.file()\u003C\u002Fcode> lit les autres. Le \u003Ccode class=\"key\">fetch\u003C\u002Fcode> standard sert le réseau, par Escouade en WASM.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ce que le bundle peut importer\u003C\u002Fstrong> : \u003Ccode class=\"key\">node:fs\u003C\u002Fcode> et \u003Ccode class=\"key\">node:readline\u003C\u002Fcode>, que sert Escouade, et rien d’autre ; il tient en un fichier, sans \u003Ccode class=\"key\">export\u003C\u002Fcode>. Un autre \u003Ccode class=\"key\">import\u003C\u002Fcode> refuse sa compilation, et \u003Ccode>escouade-plugin validate\u003C\u002Fcode> le dit dans ta langue (« le bundle importe node:child_process, qu’un plugin en WASM n’a pas (seulement node:fs, node:readline) »). \u003Ccode class=\"key\">setTimeout\u003C\u002Fcode> et \u003Ccode class=\"key\">clearTimeout\u003C\u002Fcode> y existent.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Le SDK 0.3 ou plus récent\u003C\u002Fstrong> : un bundle fait avec le SDK 0.2 ne tourne pas en WASM ; reconstruis-le (\u003Ccode>npm run build\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Les outils, sans Node\u003C\u002Fstrong> : \u003Ccode>escouade-plugin build\u003C\u002Fcode> fait le module comme le store, pour vérifier qu’il compile ; \u003Ccode class=\"key\">test\u003C\u002Fcode> et \u003Ccode class=\"key\">dev\u003C\u002Fcode> jouent le bundle en WASM, dans l’hôte de l’app. La Javy qui compile (9.1.0) est téléchargée au premier besoin dans \u003Ccode class=\"key\">~\u002F.escouade\u002Ftools\u003C\u002Fcode> et vérifiée par son empreinte ; l’app et \u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> la partagent. Node ne sert qu’à l’auteur, pour \u003Ccode>npm run build\u003C\u002Fcode> et les tests du plugin ; celui qui l’installe n’en a jamais besoin. Plus besoin d’en faire un exécutable (\u003Ccode>deno compile\u003C\u002Fcode>, \u003Ccode>bun build --compile\u003C\u002Fcode>) pour qui n’a pas Node.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Ce que le plugin peut demander en plus\u003C\u002Fstrong> : \u003Ccode class=\"key\">p.fs\u003C\u002Fcode> pour les dossiers de \u003Ccode class=\"key\">permissions.files\u003C\u002Fcode>, \u003Ccode class=\"key\">p.runProgram\u003C\u002Fcode> pour les programmes de \u003Ccode class=\"key\">permissions.commands\u003C\u002Fcode>, \u003Ccode class=\"key\">openStream\u003C\u002Fcode> et \u003Ccode class=\"key\">openSocket\u003C\u002Fcode> pour le réseau (voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F\">les appels\u003C\u002Fa>). Le store ne publie plus de \u003Ccode class=\"key\">run.command\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"en-ci\">En CI\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yaml\">jobs:\n  plugin:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with: { node-version: 22 }\n      - run: npm ci\n      - run: npm run typecheck &amp;&amp; npm test &amp;&amp; npm run build\n      - run: npx escouade-plugin validate .\n      # Le banc vient avec Escouade : installe son .deb, ou donne le chemin de son escouade-plugin.\n      - run: npx escouade-plugin test .\n        env:\n          ESCOUADE_PLUGIN_BIN: \u002Fusr\u002Fbin\u002Fescouade-plugin\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>La CI d’Escouade fait de même sur les gabarits de \u003Ccode>npm create escouade-plugin\u003C\u002Fcode>, contre le banc de chaque version construite.\u003C\u002Fp>\n",[8,12,15,18,21,24,28,31],{"id":9,"text":10,"depth":11},"un-premier-plugin","Un premier plugin",2,{"id":13,"text":14,"depth":11},"le-client","Le client",{"id":16,"text":17,"depth":11},"les-types","Les types",{"id":19,"text":20,"depth":11},"les-tests","Les tests",{"id":22,"text":23,"depth":11},"la-commande","La commande",{"id":25,"text":26,"depth":27},"l-archive","L’archive",3,{"id":29,"text":30,"depth":11},"sans-node","Sans Node",{"id":32,"text":33,"depth":11},"en-ci","En CI",1793889379686]