[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"doc:fr:plugins\u002Fquick-start":3},{"title":4,"lede":5,"html":6,"headings":7},"Un premier plugin","Un plugin qui marche en dix minutes : d’abord un webhook Discord, sans une ligne de code, puis un programme en TypeScript ou en Rust qui réagit aux mêmes événements. Chaque étape se vérifie avant la suivante.","\u003Ch2 id=\"ce-qu-il-te-faut\">Ce qu’il te faut\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Escouade, qui livre la commande \u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> à côté de son programme (voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">tester un plugin\u003C\u002Fa>) ;\u003C\u002Fli>\n\u003Cli>pour Discord, un salon et l’adresse d’un de ses webhooks (Paramètres du salon › Intégrations › Webhooks) ;\u003C\u002Fli>\n\u003Cli>pour un programme, Node 22.18 ou plus récent en TypeScript, Rust en Rust, pour écrire et tester le plugin. Celui qui l’installe n’a besoin ni de l’un ni de l’autre.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"un-webhook-discord-sans-code\">Un webhook Discord, sans code\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>1. Le manifeste.\u003C\u002Fstrong> Crée un dossier \u003Ccode class=\"key\">mon-discord\u003C\u002Fcode> et copie-y le manifeste du plugin Discord, l’un des \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fexamples\u002F#discord\">plugins d’exemple\u003C\u002Fa>. Change son \u003Ccode class=\"key\">id\u003C\u002Fcode> pour un identifiant à toi (\u003Ccode>io.github.&lt;ton-login&gt;.discord\u003C\u002Fcode>) : \u003Ccode class=\"key\">dev.escouade.*\u003C\u002Fcode> est réservé aux plugins d’Escouade. Le cœur du plugin est là :\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Fdiscord\u002Fescouade-plugin.json\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">  &quot;permissions&quot;: {\n    &quot;events&quot;: [&quot;ticket.column&quot;, &quot;agent.status&quot;],\n    &quot;network&quot;: [&quot;discord.com&quot;],\n    &quot;secrets&quot;: [&quot;webhook&quot;]\n  },\n  &quot;contributes&quot;: {\n    &quot;secrets&quot;: [{ &quot;key&quot;: &quot;webhook&quot;, &quot;title&quot;: { &quot;fr&quot;: &quot;Adresse du webhook Discord&quot;, &quot;en&quot;: &quot;Discord webhook URL&quot; } }],\n    &quot;webhooks&quot;: [\n      {\n        &quot;url&quot;: &quot;{{secrets.webhook}}&quot;,\n        &quot;events&quot;: [&quot;ticket.column&quot;],\n        &quot;when&quot;: { &quot;data.to&quot;: &quot;review&quot; },\n        &quot;body&quot;: {\n          &quot;content&quot;: &quot;🧪 **{{ticket.key}}** to test: {{ticket.title}} ({{project.name}})&quot;,\n          &quot;allowed_mentions&quot;: { &quot;parse&quot;: [] }\n        }\n      },\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Il demande deux sortes de faits (\u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>), un hôte (\u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>) et un secret, l’adresse du webhook, qu’il ne verra jamais écrite dans un fichier. Le webhook part quand un ticket passe dans la colonne \u003Ccode class=\"key\">review\u003C\u002Fcode>, « À tester ».\u003C\u002Fp>\n\u003Cp>\u003Cstrong>2. Le vérifier.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin validate mon-discord\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Une erreur est dite champ par champ, avec son chemin (« \u003Ccode class=\"key\">escouade.min\u003C\u002Fcode> manque. »).\u003C\u002Fp>\n\u003Cp>\u003Cstrong>3. Le charger.\u003C\u002Fstrong> Dans la vue Plugins, « En développement », puis « Charger un dossier… » et ton dossier. La fenêtre dit ce qu’il pourra faire : recevoir ces événements, se connecter à \u003Ccode class=\"key\">discord.com\u003C\u002Fcode>, garder un secret. Il est relu à chaque changement de ses fichiers.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>4. Son secret.\u003C\u002Fstrong> Sur sa page, l’onglet « Réglages » : colle l’adresse du webhook. Elle va au trousseau du système.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>5. L’essayer.\u003C\u002Fstrong> Passe un ticket dans « À tester » : le message arrive dans le salon. L’onglet « Journal » de sa page note chaque appel, par son hôte et sa réponse, jamais l’adresse entière.\u003C\u002Fp>\n\u003Ch2 id=\"un-programme-en-typescript-ou-en-rust\">Un programme, en TypeScript ou en Rust\u003C\u002Fh2>\n\u003Cp>Les deux donnent un module WASM qu’Escouade fait tourner dans l’app, sur les mêmes méthodes. Le modèle de chaque langage dit quand un ticket est prêt à tester et refuse les push forcés :\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>TypeScript\u003C\u002Fth>\n\u003Cth>Rust\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Il te faut\u003C\u002Ftd>\n\u003Ctd>Node 22.18\u003C\u002Ftd>\n\u003Ctd>Rust, avec \u003Ccode>rustup target add wasm32-wasip1\u003C\u002Fcode>, et un compilateur C pour \u003Ccode>cargo test\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Le créer\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>npm create escouade-plugin@latest mon-plugin -- --kind program\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>npm create escouade-plugin@latest mon-plugin -- --kind rust\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Le manifeste dit\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>&quot;run&quot;: { &quot;js&quot;: &quot;dist\u002Fplugin.mjs&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>&quot;run&quot;: { &quot;wasm&quot;: &quot;Cargo.toml&quot; }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Ses tests\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>npm test\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>cargo test\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Lire son manifeste\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>npm run validate\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>escouade-plugin validate .\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Le banc\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>npm run bench\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>escouade-plugin test .\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Le module\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>npm run build\u003C\u002Fcode>, Escouade compile le reste\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>escouade-plugin build .\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Ce que tu commites\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">dist\u002Fplugin.mjs\u003C\u002Fcode> et ses sources\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode> et ses sources\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Choisis le TypeScript pour aller vite. Choisis le Rust pour un client d’API qui existe déjà, un travail sur des octets, ou \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Frust\u002F\">le kit\u003C\u002Fa>, qui écrit pour toi une source du Kanban ou un hébergeur.\u003C\u002Fp>\n\u003Ch3 id=\"en-typescript\">En TypeScript\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>1. Le gabarit.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">npm create escouade-plugin@latest mon-plugin -- --kind program\ncd mon-plugin\nnpm install\nnpm test\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le gabarit dit quand un ticket est prêt à tester et refuse les push forcés ; son code et ses tests sont dans \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fsdk\u002F#le-client\">le SDK\u003C\u002Fa>. Son manifeste nomme \u003Ccode>&quot;run&quot;: { &quot;js&quot;: &quot;dist\u002Fplugin.mjs&quot; }\u003C\u002Fcode> : Escouade le fait tourner dans l’app, en WASM, sans Node chez celui qui l’installe.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>2. Envoyer un message.\u003C\u002Fstrong> Un programme peut faire plus qu’un webhook : choisir le salon, mettre en forme, garder un état. Le plugin Slack, par exemple, demande son secret à chaque fait et poste par \u003Ccode class=\"key\">fetch\u003C\u002Fcode>, qui passe par Escouade, vers les hôtes de \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode> seulement :\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Fslack\u002Fsrc\u002Fplugin.ts\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">  return createPlugin().onEvent([&#39;ticket.column&#39;, &#39;agent.status&#39;], async (fact, p) =&gt; {\n    const text = textOf(fact);\n    if (!text || !fact.project) return;\n    const token = await p.secret(&#39;token&#39;);\n    if (!token) return p.log.warn(&#39;no Slack token kept: nothing sent (Plugins › Slack › Settings)&#39;);\n    \u002F\u002F A bot token, never anything else, goes to Slack (and never into the journal).\n    if (!token.startsWith(&#39;xoxb-&#39;)) return p.log.warn(&#39;the token kept is not a Slack bot token (xoxb-…): nothing sent&#39;);\n    const channel = channelOf(fact.project.name, channels);\n    try {\n      const res = await fetch(&#39;https:\u002F\u002Fslack.com\u002Fapi\u002Fchat.postMessage&#39;, {\n        method: &#39;POST&#39;,\n        headers: { authorization: `Bearer ${token}`, &#39;content-type&#39;: &#39;application\u002Fjson; charset=utf-8&#39; },\n        body: JSON.stringify({ channel, text }),\n      });\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Ajoute à ton manifeste ce que ton code demande : le fait dans \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>, l’hôte dans \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>, le secret dans \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode> et \u003Ccode class=\"key\">contributes.secrets\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>3. Le banc.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">npm run bench\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">banc de conformité\u003C\u002Fa> joue ton programme comme Escouade : la poignée de main, les faits, les décisions, les délais, l’arrêt. Il doit passer avant toute publication.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>4. Le charger\u003C\u002Fstrong> : \u003Ccode>npm run build\u003C\u002Fcode> réunit ton code dans \u003Ccode class=\"key\">dist\u002Fplugin.mjs\u003C\u002Fcode>, puis charge le dossier comme le premier, depuis « En développement ». Son journal montre ses lignes en direct.\u003C\u002Fp>\n\u003Ch3 id=\"en-rust\">En Rust\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>1. Le modèle.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">npm create escouade-plugin@latest mon-plugin -- --kind rust\ncd mon-plugin\ncargo test\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode class=\"key\">npm\u003C\u002Fcode> ne sert qu’à écrire le dossier : le plugin n’a ni \u003Ccode class=\"key\">package.json\u003C\u002Fcode> ni \u003Ccode class=\"key\">node_modules\u003C\u002Fcode>. Il dépend du \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Frust\u002F\">kit\u003C\u002Fa> par version, avec son \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode>, et son code tient en un fichier :\n\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (extrait)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">struct Template;\n\nimpl Plugin for Template {\n    \u002F\u002F Every method of Escouade comes here but `initialize` and `shutdown`, which the kit answers. A notification too: what\n    \u002F\u002F it returns goes nowhere. Nothing may block here: an `.await` gives the turn back to Escouade.\n    fn call&lt;&#39;a&gt;(\n        &amp;&#39;a self,\n        cx: &amp;&#39;a Cx,\n        method: &amp;&#39;a str,\n        params: Value,\n    ) -&gt; BoxFuture&lt;&#39;a, Result&lt;Value, Refusal&gt;&gt; {\n        Box::pin(async move {\n            match method {\n                &quot;events\u002Ffact&quot; =&gt; {\n                    fact(cx, &amp;params);\n                    Ok(Value::Null)\n                }\n                &quot;permissions\u002Fdecide&quot; =&gt; Ok(decide(&amp;params)),\n                other =&gt; Err(Refusal::not_found(other)),\n            }\n        })\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Ce que fait chaque appel de l’hôte est dans \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F\">les appels\u003C\u002Fa>, en TypeScript et en Rust.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>2. Le banc.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin validate .\nescouade-plugin test .\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> compile le module comme le store, avec un compilateur qu’il télécharge lui-même la première fois (650 Mo, une minute), puis le joue comme Escouade.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>3. Le charger.\u003C\u002Fstrong> \u003Ccode>escouade-plugin build .\u003C\u002Fcode> écrit le module dans \u003Ccode class=\"key\">packs\u002F\u003C\u002Fcode> : copie-le à côté du manifeste sous le nom \u003Ccode class=\"key\">plugin.wasm\u003C\u002Fcode>, que le modèle n’envoie pas à git, puis charge le dossier depuis « En développement ». Recopie-le après chaque changement : le store, lui, compile ses sources.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin build .\ncp packs\u002F*.wasm plugin.wasm\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"ensuite\">Ensuite\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fmanifest\u002F\">Le manifeste\u003C\u002Fa>, champ par champ, et \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fevents\u002F\">les événements\u003C\u002Fa> qu’un plugin peut recevoir.\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcapabilities\u002F\">Les capacités de l’hôte\u003C\u002Fa> et \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fcalls\u002F\">les appels\u003C\u002Fa>, avec un exemple en TS et en Rust pour chacun.\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fpermissions\u002F\">Les permissions\u003C\u002Fa> : ce que chacune ouvre.\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fpublishing\u002F\">Publier sur le store\u003C\u002Fa>, quand il est prêt.\u003C\u002Fli>\n\u003C\u002Ful>\n",[8,12,15,18,22,25],{"id":9,"text":10,"depth":11},"ce-qu-il-te-faut","Ce qu’il te faut",2,{"id":13,"text":14,"depth":11},"un-webhook-discord-sans-code","Un webhook Discord, sans code",{"id":16,"text":17,"depth":11},"un-programme-en-typescript-ou-en-rust","Un programme, en TypeScript ou en Rust",{"id":19,"text":20,"depth":21},"en-typescript","En TypeScript",3,{"id":23,"text":24,"depth":21},"en-rust","En Rust",{"id":26,"text":27,"depth":11},"ensuite","Ensuite",1793889379386]