From 8117f5fbf2e27ca088fb92bb6a85ae79cd00a8cf Mon Sep 17 00:00:00 2001 From: hetaoBackend Date: Tue, 10 Nov 2026 14:40:00 +0800 Subject: [PATCH] feat(minimax-code-badge): add powered by MiniMax Code badge plugin for GitHub PRs Adds plugins/hetaoBackend/minimax-code-badge, a Skill-first Agent Plugin that appends a "powered by MiniMax Code" badge to the end of GitHub pull request descriptions. The badge is one line of Markdown pointing at a Shields.io image, wrapped in HTML comment markers that GitHub does not render. The package ships scripts/badge.mjs, a dependency-free idempotent writer that adds, refreshes, checks, or removes the badge on a local body file or on an existing PR through the GitHub CLI, plus bundled SVG and PNG fallbacks and 17 local tests. --- .../hetaoBackend/minimax-code-badge/LICENSE | 192 +++++++++++++++ .../hetaoBackend/minimax-code-badge/README.md | 130 ++++++++++ .../minimax-code-badge/plugin.json | 21 ++ .../skills/minimax-code-badge/SKILL.md | 98 ++++++++ .../assets/minimax-code-badge.png | Bin 0 -> 18226 bytes .../assets/minimax-code-badge.svg | 12 + .../minimax-code-badge/scripts/badge.mjs | 196 +++++++++++++++ .../minimax-code-badge/scripts/badge.test.mjs | 223 ++++++++++++++++++ 8 files changed, 872 insertions(+) create mode 100644 plugins/hetaoBackend/minimax-code-badge/LICENSE create mode 100644 plugins/hetaoBackend/minimax-code-badge/README.md create mode 100644 plugins/hetaoBackend/minimax-code-badge/plugin.json create mode 100644 plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/SKILL.md create mode 100644 plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.png create mode 100644 plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.svg create mode 100644 plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.mjs create mode 100644 plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.test.mjs diff --git a/plugins/hetaoBackend/minimax-code-badge/LICENSE b/plugins/hetaoBackend/minimax-code-badge/LICENSE new file mode 100644 index 00000000..125be1b8 --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 MCode Plugins contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/hetaoBackend/minimax-code-badge/README.md b/plugins/hetaoBackend/minimax-code-badge/README.md new file mode 100644 index 00000000..cb3819cc --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/README.md @@ -0,0 +1,130 @@ +# MiniMax Code Badge + +Append a small "powered by MiniMax Code" badge to the end of a GitHub pull request description +created with MiniMax Code. + +[![powered by MiniMax Code](https://img.shields.io/badge/powered_by-MiniMax_Code-E73562?style=flat-square&logo=minimax&logoColor=white)](https://github.com/MiniMax-AI/minimax-code) + +The badge is one line of Markdown pointing at a Shields.io image, wrapped in two HTML comment +markers that GitHub does not render. The Skill tells the agent when to add it; a dependency-free +Node.js script writes it idempotently, so running it twice never produces two badges. + +## Included capability + +- `minimax-code-badge`: decides when a PR description should carry the badge, supplies the exact + Markdown block, and provides `scripts/badge.mjs` to add, refresh, check, or remove the badge on a + local body file or on an existing PR through the GitHub CLI. + +## Try it + +```text +Open a GitHub pull request for this branch and add the powered by MiniMax Code badge. +``` + +```text +Add the MiniMax Code badge to PR #42 in owner/repo. +``` + +```text +Remove the MiniMax Code badge from PR #42. +``` + +## Expected result + +The PR description keeps all of its original content and gains exactly one badge block at the very +end, separated by one blank line: + +```md + +[![powered by MiniMax Code](https://img.shields.io/badge/powered_by-MiniMax_Code-E73562?style=flat-square&logo=minimax&logoColor=white)](https://github.com/MiniMax-AI/minimax-code) + +``` + +The markers stay invisible on GitHub. Re-running the writer reports `unchanged` and performs no +write, so a retried or repeated run cannot duplicate the badge. + +## Requirements + +- Node.js 18 or newer for `scripts/badge.mjs`. No npm dependencies. +- The GitHub CLI (`gh`), already authenticated, **only** when writing to an existing PR. The local + body-file mode never calls `gh`. +- No account, API key, paid service, or extra runtime. + +## Usage + +`SKILL_DIR` is the directory containing `SKILL.md`. + +```bash +# New PR: write the body first, then create the PR +node "$SKILL_DIR/scripts/badge.mjs" --body-file pr-body.md +gh pr create --title "…" --body-file pr-body.md + +# Existing PR: preview, then apply +node "$SKILL_DIR/scripts/badge.mjs" --pr 42 --repo owner/name --dry-run +node "$SKILL_DIR/scripts/badge.mjs" --pr 42 --repo owner/name + +# Remove, verify, or print +node "$SKILL_DIR/scripts/badge.mjs" --pr 42 --remove +node "$SKILL_DIR/scripts/badge.mjs" --body-file pr-body.md --check +node "$SKILL_DIR/scripts/badge.mjs" --print +``` + +Status is printed on stderr: `added`, `replaced`, `unchanged`, `removed`, `absent`, or `disabled`. +`--check` exits 1 when the body is not up to date. Set `MINIMAX_CODE_BADGE_DISABLE=1` to turn the +script into a no-op, or `MINIMAX_CODE_BADGE_MARKDOWN` to change the wording. + +## Supported platforms + +macOS, Linux, and Windows. The script is plain Node.js with no platform-specific code, and the badge +is ordinary Markdown that GitHub renders on every platform. + +## Network access + +The Plugin itself makes no network requests and starts no server. `badge.mjs` reaches the network +only through the user's own `gh` CLI when the user asks it to update an existing PR. + +The rendered badge image is loaded by the **reader's browser** from +`https://img.shields.io` when the PR page is displayed, and links to +`https://github.com/MiniMax-AI/minimax-code`. GitHub serves such images through its own image proxy, +so a reader does not contact Shields.io directly. Nothing is sent anywhere at install time or at +agent runtime. + +## Data use + +The Plugin ships instructions, one Node.js script, and two image files. It adds no telemetry and +uploads nothing. When the user asks it to update a PR, it reads that PR's current description with +`gh pr view` and writes the new description with `gh pr edit`, using the user's existing GitHub +credentials and permissions. No credentials are stored, logged, or transmitted by the Plugin. + +The bundled `assets/minimax-code-badge.svg` and `assets/minimax-code-badge.png` are offline copies of +the same badge, for documents that cannot load remote images. + +## Safety + +- The Skill instructs the agent to badge only PRs created in the current session, or PRs the user + explicitly names. It never edits another person's PR, and never writes to commit messages, PR + titles, review comments, or issue comments. +- It skips the badge when the user opted out, or when the repository's `CONTRIBUTING.md` or PR + template forbids badges, images, promotional content, or AI attribution. +- Editing a public PR description is a visible change, so the agent follows the session's normal + approval rules before calling `gh pr edit`. +- GitLab merge requests are never badged automatically; the user has to ask for that specific MR. + +## Tests + +```bash +node --test "$SKILL_DIR/scripts/badge.test.mjs" +``` + +17 tests, all using local fake data. The `--pr` tests run against a fake `gh` executable that reads +and writes a JSON file, so the suite never contacts GitHub. + +## Design notes + +The mechanism follows the public DeepSeek Harness badge Skill (`@deepseek-ai/dsh-skill-badge`): one +Skill instruction, one fixed Markdown block, and bundled images, with no model tools, no platform +API, and no hooks. The idempotent writer is the part this Plugin adds on top. + +## License + +Apache-2.0. See [LICENSE](LICENSE). diff --git a/plugins/hetaoBackend/minimax-code-badge/plugin.json b/plugins/hetaoBackend/minimax-code-badge/plugin.json new file mode 100644 index 00000000..0663dd51 --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/plugin.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "minimax-code-badge", + "version": "0.1.0", + "description": "Append a \"powered by MiniMax Code\" badge to the end of GitHub pull request descriptions, with an idempotent gh-based writer and bundled SVG/PNG fallbacks.", + "author": { + "name": "hetaoBackend", + "url": "https://github.com/hetaoBackend" + }, + "homepage": "https://github.com/MiniMax-AI/MiniMax-Code-Plugins/tree/main/plugins/hetaoBackend/minimax-code-badge", + "repository": "https://github.com/MiniMax-AI/MiniMax-Code-Plugins", + "license": "Apache-2.0", + "keywords": [ + "minimax-code", + "plugin", + "github", + "pull-request", + "badge", + "shields-io" + ] +} diff --git a/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/SKILL.md b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/SKILL.md new file mode 100644 index 00000000..fb24003c --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/SKILL.md @@ -0,0 +1,98 @@ +--- +name: minimax-code-badge +description: Use whenever creating a GitHub pull request - append the "powered by MiniMax Code" badge to the end of the PR description. Also use when the user asks to add, refresh, or remove the MiniMax Code badge on a PR or Markdown deliverable. GitLab merge requests only when the user explicitly asks. Skip when the user has said not to add the badge. +--- + +# powered by MiniMax Code badge + +This Skill only provides the badge and the rules for placing it. It adds no +model tools and calls no platform API by itself; writing into a PR goes through +the user's own `gh` CLI and the normal approval rules of this session. + +## Assets + +- Remote badge (Shields.io, 179×20, brand colour `#E73562`, MiniMax logo from Simple Icons): + `https://img.shields.io/badge/powered_by-MiniMax_Code-E73562?style=flat-square&logo=minimax&logoColor=white` +- Link target: `https://github.com/MiniMax-AI/minimax-code` +- Bundled fallback images in this Skill directory: `assets/minimax-code-badge.svg` + (179×20) and `assets/minimax-code-badge.png` (1074×120 source, render at 179×20). +- Idempotent writer: `scripts/badge.mjs` (Node.js ≥ 18, no dependencies). + +The canonical block, always wrapped in the two invisible markers: + +```md + +[![powered by MiniMax Code](https://img.shields.io/badge/powered_by-MiniMax_Code-E73562?style=flat-square&logo=minimax&logoColor=white)](https://github.com/MiniMax-AI/minimax-code) + +``` + +## When to add it + +Add it when **all** of these hold: + +1. You are creating a GitHub pull request in this session (`gh pr create`, the GitHub + API, or a web form you fill in), or the user explicitly asks you to badge a PR or a + Markdown deliverable. +2. The change in the PR was produced with MiniMax Code in this session or the user says so. + Never badge someone else's PR unless the user asks. +3. The user has not opted out (see "Turning it off"). +4. The repository does not forbid it. Read `CONTRIBUTING.md` and + `.github/pull_request_template.md` when present; skip the badge if they forbid badges, + images, promotional content, or AI attribution, or require an exact template that would + break. Mention the skip to the user in one sentence. + +GitLab merge requests: the same Markdown renders on GitLab, but do **not** add it on your +own. Add it to a GitLab MR only when the user explicitly asks for that MR or that +repository. Never put the badge in commit messages, PR titles, review comments, or issue +comments. + +## Rules + +- Place the block at the **very end** of the PR description, after one blank line, as the + last thing in the body. Exactly one block per description. +- Use the block verbatim. Do not change the colour, logo, text, link, size, or markers, and + do not add surrounding prose such as "Generated by…". +- GitHub and GitLab Markdown use the Shields.io URL above. For systems that cannot reliably + load remote images (for example Feishu/Lark documents or offline exports), upload + `assets/minimax-code-badge.png` to that system and reference the uploaded file, rendered + at 179×20, linked to the same target when links are supported. +- Do not hand-edit an existing badge: let the script replace it so only one copy remains. + +## Procedure + +`SKILL_DIR` is the directory containing this `SKILL.md`. + +New PR (preferred, no extra write to GitHub): + +1. Write the PR body to a temporary file, e.g. `pr-body.md`. +2. `node "$SKILL_DIR/scripts/badge.mjs" --body-file pr-body.md` +3. `gh pr create --title "…" --body-file pr-body.md` + +Existing PR (only when the user asked to badge it; this edits a public description): + +1. Preview: `node "$SKILL_DIR/scripts/badge.mjs" --pr [--repo owner/name] --dry-run` +2. Apply: `node "$SKILL_DIR/scripts/badge.mjs" --pr [--repo owner/name]` + The script reads the body with `gh pr view --json body,url` and only calls + `gh pr edit --body-file` when the body actually changes. + +Status on stderr: `added`, `replaced` (an old or duplicated badge was normalised to one +block), `unchanged` (already correct; nothing written), `removed`, `absent`, `disabled`. +`--check` exits 1 when the body is not up to date. If Node.js is unavailable, append the +canonical block by hand at the end of the body after checking that the markers are not +already present. + +After badging, tell the user in one short sentence that the badge was added and how to +remove it. + +## Turning it off or changing the text + +- The user says not to add it (this conversation or a remembered preference): skip it. +- Remove from a PR: `node "$SKILL_DIR/scripts/badge.mjs" --pr --remove` + (or `--body-file --remove`). +- Environment `MINIMAX_CODE_BADGE_DISABLE=1`: the script becomes a no-op and prints + `disabled`; treat that as an opt-out and do not add the badge by hand. +- Different wording requested by the user: pass one line of Markdown with + `--snippet ''` (or set `MINIMAX_CODE_BADGE_MARKDOWN`). The markers stay the same, + so a later run still replaces it instead of adding a second copy. +- Turning the whole behaviour off: disable the "MiniMax Code Badge" plugin in MiniMax Code + Settings → Plugins. diff --git a/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.png b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..5fb7b220e289db935a010dba9de4e6ea49147bb0 GIT binary patch literal 18226 zcmd>`1ydYhv$k<}5AN>n7Tn!EI0Sds;4Z--xVyW%1b5fqt^v;EJ@uaNM|@QjELFR+ zGtbV`efQm0Z-kP9BqAIx90&*qqO_El3J3@UEAZF@1`PNeCn;_R0)he}EheJok#(L8 z?W3yxHlpaZ)+mpSH;zT^P2+8kf+C_+oENZX*cXvW(hZYeLrU9?BrKdv;|)8ymB~7u zh{%vF-H`N|B(Xom!!;(q7TrAal%y#1r;Uv3X^VX3ieHx3@;7l1F-#C~-`>R8EQAOO zgoqw0`hWfc598y5>i_RCO7Q>3kI5*8f@D%?`?fQ=?9u+aB$2q{x~U75+Ri7xe~pce zO(!HLCl5to8+QAwo;AaT<{S3-aab>usuaQf^?rYSSXfwSvRTGOfcnpZ18XjYf{KXC z9*!aZ0}*ej#olmByyx@dEs;W&-&VKApbOj3hlPbD0asC0_U-koW+t0Y^519Ddwuv| z`JY2nOPwxN$wm?rOYy^$uQl16J-o{PWV~U7B$`+#Q&p$M?0|tw;+bmCi>2SSRYnl;!JO6lpyY94It*_~N)Hxha+2yJH-TZyMO1I_z78Oh} z;XajnFgO@kD<^EnPuGKb)rAVpnHPMqsIN&NPnVnf);M;!0{>k^&>|@>+17e;I=ZM1 zY5{?4cMdAo(*;LCMQQ0>#=WI0QSJXeQ=|ZRW>y4|M#bWQb*J>j?5w|LK;5eC0%@1Q z6@GL~48{MuFq1h2R1uVrgD0~5BeH0T0*L|=5fWBbl`=IaNtc)ZUBNi1Dm0pd4OHFO zrDCo`G^lal8X}PicwKPV%n{s8fipGqeZYi;1BXJ$%b=;!Yr9=G^#9L$>=8a3ppH*Z zwUVvQ`_EOa%gW$T%l z6g7&tIQ1HPh&LhO-kf7lbDfqPw%v(4!SWS9l2bM@1#I%a1(0$SVMG_dZi$b>-7HZu zl@^;xO@u4jV8n`o5hX$G=%7w`U$e%ttrZ+4hx7@RDN4~Na`nC~LJNW*Kp`hKQ~%oD zkDkj(juugAr|!{9AN+Dk@SYmHrru$@suPq95@|}o`0pOSfwgLai+qfc4pc_G5{d5@ zrHsK{%ErK1tRWdv`4lhxsq78*x>lVY}$dG^*A{6UZtw zYZ!m7uj}H*3{U)IV$!PJd3`t|*Rxu#-dw5sMgV=@Ys5U6E98H*g|_?*1)OFUD5!8s zQ;b)$;!%Ym3Q>XvU5({@uh|S9ve>^xe$0Ym7Pq)>7VK~!)DarIDsk(w8i<& zR`7OtXfm(n0LLH*RL4K9D{;d&)@W16XYAWG4HM#q%-KCTFRyx5RfQ*Y?-<{evyzEL zvu-~YR28SPcp4P4c@@x!a>%KuW->V7u@Y(z+KzL){}laf&45s%#$f9C%SuoBZ-1;9 z@p~_N+=ID8EQH=C$P7xJH|20UUoX9V5ue>hrChlm?lf$1mK=1P-FYGw1cMta!u&B+ ztltSOe{V)QT+fc|Xy}75|Aj_0PRq2Dyi(=v zlmoDJu6#-OpT3M+wrp=!wj=>=FG#vMIeci4{WBv_WNuZXB0XmzFR%JR5ZJA7^vMgH7hYs zBq#DBULgzR#%kbTjpej|u)7S`e|vcRHC>bc{VTB7H&8D`{p(fZ!G6V8 z0?MS(TOaC|M)11RWkX z`4-C>ED~jEOTc}uTp$5tR*hryFJEAfskpeX6=oMJG!=6N5syoxlH~$y-bcye*sbOY zO@<<4pU?;hzBA)E-L?6?J*Kl+Oy>wx%vwtQQmN8u8X@$q1QgB9ixz$Ay}y*KY%q4{ zdNc;X_)n(oeDSE$#fkw7bU`Kuq2BEHgalA@{4gYfptoOxep+B*Zo9)P=}aGQJz5wX z<(iM|B8Ch3CsMTC)b{CD+9)Do7{8Pn?eZ+u4OIk{RcICPiN4MM5s}fp6kx|!V8Q3M zX>-gC0bMK7Ra11`JL;B%VG1$`q16{Sz)DE(!-337ShFT*UkdJkpOJe*M!_=Y9O`r{k6@NI$vscC(o zL=5iqF`(S|+-HFgGYc%BLuYiCaI##TQG99Fu!J7eCKv*$O|RRi56s$OrPf%13`(tU zr!QEy+4c*jxW9bBgKCqtIx}^y=TSPK-vS-Lg3~Y%JlX=mp_}bCacxztmBqxwOiW1m z12xarTbNeLh{IBu40Ey>?mO_1JhnPLlRrK_yI?d+&x~_xg|rmFagp27rPKCrK6ELnAiIhz50mm;^&1?r_IQ?8g%D?J$Co zNctE9nsMQs^V@K=^YOHDMc+FJ>mo@^Z)KSOZcbnn7#LWaQ)twy4(LK?Ipbbq)upM0A9@oUM z_B*;@f=I+WWsXXe%n=||wZQukUoTbbci3(6lI$HMDGHXUjZw&hneGA`N)ArA$M1c| zCWH^n>rd0N0h#!h_v!sqyr)*e#Bf3e_U(71$H3+9WVz?mk!rl3O;)ZtJ+@?wW^PmH zZ5lW2+h^)Ek0SRWRAuQ_Q;K*%%y#{t^ChRTmLoE&fKnquPLQEdh)!9mJQyYX(U9== zATwe36wWkysxF)MiRC`%^Pup0T{dV~X{D{U3L;%JH_kZqpo{FisiD!+UTG|U8E!5j zqEA75oJ^28j=L1UTt!=@vlYf_!0tt0?LI!d@#-SuP1R}mhLj-%3nJg23Fayp0gEI(xfQL`?ATP-4fNP#Tih` zNC#nAuEp#$H7+7%ZMj`I-9K>eiZTnCQOXMPbFc7b8z>h0#nCWHXhzC>|E7xfoOx`O z>#Kd}f(HH!2lP(;SM=t<==Ta@5k$oUd24Z`6A)F9AHF5dJ-XkzJ%U4^jY(vwRnK<^ z{Qaqn{E|ZuH@KvH)0Ie>Optk#=h^>p*? zFnc{+z7L`+2Jldr`f}4c_%wZAcY8QpJX@}TzKU6v->C*ENRJG!P%Vjd&<4AW_HH*C z?$T>>+6d=E4hSjO!Y#$!&457i2I{Ij2#9LrSLk#5)}XvGpk5Jy0RO?~Dyt-l6npTe zBp1psHH1bUd=93N?D;qU1X`;barMqM|Ax0-Zvhw+v=fF{|KFFpyW87v!+>yH4r>f< ztdL4QzFdo3-_%#5AIN6o$(|oC$0DI#`siYU8~hGWeh-V923^Abe{Z1GtqOpM-+$$d z)#}4cC#G-XfvK`kx5^&HS+R}xa0Qb_Kj3{ZX|QMZwM~tq72ciLen=s6I2Q>|Ps}O3 z#uLMB&}2>HqvR14=YDoS7bMd_>bsuV2<3P)VneYHlNiWf`15oxS1S>C11rBRj2jQU zv?bSAKxs;jDl7Xk$jcdn3xUw&=dj8oPOksg3;6LII4a11h`jjwvh7X-t}N7O#Tjj| z%9@(s1LLhPt zz>shd4$bdC1tl_a3DNcJ>Bz5P{PlO(izIFx^=;Yo?99{MhU%?-A=>j~|G;H$GKQ!| zD76G8M|+&L64kGK9eS0<@1>UWAY!hM(V-s?NVIyQdJ$0`vbvho80SW%Dq8|fu7n8c zg>*0he|UhJ>}G!iewG}m_c?%P78acn#(r@R!hB@g`%0iT3&I-h^2bhv=jZzqq@Bmn zAY4T_0KbQ+0C=VkC6kQlt#6UeYxp2%iAonT&byT>m1%V$7P6OR;Zk9jNe z5=`)2kKF&pZ;!d!(ezE@!5)Fqn_!)p%t@(4MYe>dL-6!#4^kPq+v`fWV3o8KsEu6D z9ge%0PFK)!P%ThE^rr5`Vr;0ZP>0Kol$8jU_*oRj!za8gI?a!%S)&w+Z$vwG>y)x2 z{gASm6wj|D#qE zi|ef@O*0p(P?}pRas1h*5y4@lC{?bGPtEtDgd8o9UFLb009#QZ9Hxt21P%tLzQJM! z%dW;5sQ#yb5be7L5v^aFnwXRn(bM~@pAg?zD91PjWHKDxIDlaqDd?A6NZ)uO@EW+4 z*!zq1rxZVOwzt0@ zY7eHtv61xk2Lj1ZP+?&qAW4FLJbkqeT8Fq7!s{J|WFN@&|Byy>JNeybZw7D2Vh!zl(M$F9XOF3iX zC0FePu9~Fl_w~v<8SXjjLv%l%nW8;6YM27Xt8o9`PS&f|Rom(D@j2@zVsaA`la2I} z%?@|yv(IZ|k!qhbR%pHmE*Zqh0#zWA3i%7Nv8fi!ma3JvwsI?>#^7;j(55Vm*Fs;X zcfLQaOB9qaZvfdb5INHOfY^LIm4S|2DQuZVrAP)dFjKW20*Q=*V(3gmk^g3JUQuW& zc7U$myX^s)GO5f*aObGputyND45l1;;i%qhBK#_V6qm=z^lGbnQOGRt8;I*5uk=nV zuj5|GU01IFRLR1PTd(SDDe8L+!cPPjTo~m*s|`RUm@s^!x{d_2YXCOn1KJa^QTN;K zJv`D>CYO5)jO}}LG(u|^f%aABSwQ~3x<>GM2YfGwLAP1S0zuH1dB65s*vtzuVEhE* zCGvqW?g6h$+7sP66)MQN7>IF=@Tn(D8*0|`+blX?P*aH@gJ;0HnAvsZ)bd`4khgEN zm|h9z<{ZySp49bW1tL+s=4EFJxl0<;Uo(Ceg%2(L<8Z(csI7~ZoW83@)*#cb^z4&B zQ$nDect?2K-}-tsV5=OWf`LvGt1L~I;-)#nknc`6kdyREFv5R@^O*W@*>ZDdP_jXm z*AYF!h>PUWaz%kDTd_?bjfktLH~#Wh6V2%S0c3nG>Flyv=XbcLvz8cBx0fAO#m&8$ zcNcVPczPRYs9a)R>QZH)?KaLOwYC1U-k|@34j>x@4-ESh=gxnY`cO?*=mNHg7NMyQ zoAY48qZpH8zD^>Ju2vG*?Qq3mT$PuI z#&Ntoke-3-1C@KN&w$|61L}96MqU-LU2BA`JP`DK4RFify8%jicH0%^8H$NsyAH1h zTFg{%m63{S5Y;Ag`44;OcsiRUk1`anX5vHAR-YGEOxgi3H~tiAx!WOKxpdZ@@w3GW>@)EY=Q%((0voJj{rFx zenp6eam5Ad;#Md>=j;q0wJf>3gxAJTYow#wx>dG{YIA8IwPQ>5;A_O9Ilr4o%R{{J z70o4kCpV+cY`Ne4gkqh*mD<;KOaJ$Qj7#0Rn0`nXaem3v_`e8A2wYIrfL*YGgE^7y z^3G|G|7{)!*L@NtSVNt!jegr-YB1NMN0@-jOZ!T( zOlU`%;C1T!=c`a-v1cb^Y%*F(l$DKdeEP#9m&hphPp(5)1LhnVieF!6s}ltLvqf6| z2w!esc5-=6R}L}1p|DxO99p2z96HXA6vyR=gfvm9#GF&ut3(mBY_!(AIClBXNN=?v z1E$#cWt_N+cOx;X9`hfRw^?qDq>~n+vGj&yr;$E-5Ptj_%O_K}j5w8pnK$`>iq>ve z2+{*eqs9R7h<@@}rCCt!CTBFP6}pmCCN&`DjAQN{s2g0ld>O@3pHce`Yxik>E&!!% z_xq#njP}INSFH{=hS5>3Esx+#DUc3aZik)wKl2fBwHXITQ<+A;2m1JZo`16lAb(VZ zhr^`dD$2-=PaT#iJo|q>`6o99Abvjed=l^-ncZGJ-|W|PztV1eOKv&686nzkbtGFx zu>g$&^+p2a=Gpdr$_~S|BTnKx0(ZZSC;7TVNwo`oqQc#eNvCn06Y}OlmP=piNx!2(tDYX!PE<(iFgLe8Rk4__mgRJ zJQX^Hs{#={^7F&K!3rD7L!;ZM7tlb4E#--nz5bwlcK@|*KjUh7?ZW%1AL^28imPG=)LW^CP32XJVvnzGepm&GD86_# z_wRXAVW#obpT@qcifwD8_4&YU2Sz4 z`n^!#Da;qiV(%f=#!%}L(GMXD{rw$G=?L}_L!(+;yJ+8bLF0MD)OAodLg?HAGs!X2 z_d3X=4R(n%9vjwzkS}T^6w~2;HJvMzi(3h#KV9i1Disx#oobJZre$I0e%$~~(0Qy4 z!SwWWhehFU0VWWtX=`vBNXeRrjfX(Bd9I$s4Kf22&QfljE>mP8ZvcnVWlJ~#^e~Pm z)5&V9?}MR{84Wt6`jM|Cx<6jeqOh4B=_hxB|E^l()-kd={PKW-0`CPMU@$rfShtXd z0s1RNCSyB3aGfXOFuKhWEyEOel7+F6_`PfcGHo&l9f}z>UZ2(Qu#d2}?%h*q*Ja0P z=nnLSaW5-QNte1s9t)p(ef>dG+F>-hZXDiq=8HYbeM@1ho}sgn8=#Y+D>xs6cw*OAuF%9G2u z==0PeYyq#waVPKJ4olO){blmKM00ZNFOygb6hMx+=~na zzqUOO3J$7oW59wR6+)h0r@uxs3Q`RgJs@S$slswWk`mc{NX`aC>`&YmAS3l+OG1E; zAGB=y6+m^kU7Q-DOGcPlqqtzyL^6u@e*YY!$_*$*&)<^nRyDiaXgk++YZl*wT-t7q zVMHEU?->QJj1IKN%w#Y{9S;23-~n$&*LzN^-Z8wjS5OoMPAg|EFq_7(Hk3IJ)m5?W zCpLQ8j9=w8h6Q{CB*aLLZmGL7=sTV}u`8La%P!gcsnUH-Lj<*&;%r__TtpC+f3!X6 z+?9j-9|*O=(vy13$-054oY?WBg(qH(`cUP9H@6d$&oz{4u9H+_UZAq_3&|M`O{AK{ zV-bVZ=9M>sH^j0ib6S1Lhj2@Ff{fOMB&eXe!{sbfm!18VeP3yX0tY6fQ;+r^)j;#s zC-yr{)!{<+CF8Huh$rpY)h6a1^#KaCPDgG+tvVCPOlUCY zB0^5%ffgf5LLa6QwJ$UMGGOyye)wegucUKJHl2fKG)mZ z;8O@eRkbyo@Kd0fk+=u*jOCK4&ubrv{*6rE0=nQwN4V*J!d~EYlL&hhX z4be|3bPe(pDVaCJ-jdqij3zQpF-|Y8;U}Asd|&M+rY3a1NVJsR;BWp~DQaxwn*3>J z?q8oqh#6s0A}IMr@vI^vSEAy4^j9<}TuXk&YsE=aqn;^W(dIN{g}ptiZ6bn6>r!xm zwE_9j6tk_w2YS3JqlZk^!HG>6N!q5v$9{k-#Vuk6G6S%Tthz5*X^5*d0^I~430<-e z{_6!qfsCVBR5~Y7%JWzL*h<_G7pbybs@LlCl9ZHk&M1AEh}44|I(LvVB?JAAnp6O zYSHZ$<=)@id0j({GYZRjJM~qD*G71t>{|T^3vb2N_1WvDa=4+PVU_a9!2+&QdAMCh zSSfHO21B(mic?vcmp0ACrJ0icXWsegZ47pCHCk=gq@V?-F40l#<}2J5+pnDyPNIAy zxz)Bd^0LY8&u}s2>YeBB;o%I7i>M<2O!$!~3! zm1&we`O(TYI;oqhXdA<)mGpH-A?J8x`BVbyFdtvtF(3KkpWf_FNrPOcH7cD*BV*Ov zY-JEnISCLHm~{`5Tu8b%?BDAI&Wl)M=lTZfUw>JFVIjz_-T6>?$%^(TkTs6<0JhYn z7Xi@aPeRi-@@()%0h`2E6+r2=z10&QE4fbu#D|?4j@I$(v5VD%H9XC(+BQ!?Z)E-hfS&Tx--N40uz<2h`YJS`k6;3gS#d3A=P4zvl z>s=H5ymOlTmHJSmX&}cT*xv|5uB>Qr5>z}>bT|i=iFDZvwiWuAh_&=61_=~Lq0`vO zv|-)WmxM-jIn>ha63d=DPe$oq@z#xgDab$neP9@on2QkKLf`qSvqs0t<`oy?ax3?E zK6Di2GFaJZ_1)ryn>mOq_w>Ae#dO-o#vl+k=?(NR_t0-ubCM(qxb)~hlx6EK0nbQT z5|0Cn_9u^;D+az%Z~={4!5YJnxpzajsG~N8e%dToD>pP7<<@m6;j@2VOFlaTKm+D*=~}zc5l3a*G3COv*02kD zn%Wyurb?PCRi=J0m0zNev??%{=sit=*H2)_@fi!tmh6NwdsyC+=67YPF8%qM+F-f# zoGDULd)~*>*)+%`)SkKZ5s{>}+6;@OTW;TE2{31uDe^gN^(p+mO%+XKhkbRoAO_k( zv~QgGnskzf8YwmiU3OD;)>FhJsKIZdK*PQBhRiMWTI{XV!dp@iH$Uf9Zvz{hLJq4n zd@STKXttVV#p1!Do}%7u2W|Etls>~(Jz92O%sOEQ zSPB6F0)E0|M`ZNRPSyQNuKk*x57G~?bxyl;71-GZde8=(XmYz{Hzg$psS?FpbIoE( z-|Su8>yS)AB*yT;K{0OFbHY-t%*4dKRYnW4aX8J?=j0|mVX$`tHQJAeujQ=_V>1{` zb=7FlU%uQo+7!ub*+>Qrm~ON>ZlFF_rqHTKwrod%KeN}RU;^zau}D57BA6EP(k^K6 zFiHSWXgds^TE2uUJ5X(`wX&?xB!@@-)@wBR=IXuB`tcbnMWH<4ed3P4pNdRrK3s#w z44Boo!%c;Ybfea4Aw6y_Dgl`-I+ukO@-Kpy^PkS1Bz4qoYg#s5Sc?Hn_KS>4mgLV` zm7FoLyf~bdblkNBt5eHY&)@ShRqW^9YV=nP&;|xo^#bMHxVBL?0_%(dn2RM=gz0x{ zU3&*vx*_ASv8ZKb7>zho+BPrgjhgfAL`-4%;Lk|r2l@cdskJ`j0PlAteZs`y2IKzf z>Z(FXJXvhi8Jy*8K4j(?q9hvjG3(czMaE-JJjZJw^T+oOWYZAAtUR%j5~)bFcJ%ei zevJvBC!xUO0T|3k8emwueX^Zoe1G+d1fGa>9)4pB^bp2pq8QBhrm*WTpWMn`x zLh<(X`7p&|JoQwD5Ald6y;GjUIJiv9#tddZ9>c6Xkd+U5RLueSVf<8Ww;E`f_r4M* zL7=6DX}DQi*L1J7YFtkwe|gLb!c#odA^ie@mh#n}*O}sYzKC2*tAPVeCUp?vYB>^q zePS;8)StQGg>_z7gEED@+*7wqrXsmWLRdX9kGM((yDcV%8;7tk9z`zOa|J3M1P&V($`98;TzAoYKUd6mOE zDfTQWCn)Cq(L-A%S?vFSd0#M5O2x@Zq6;M?)5oq=3*!?OjFBmlIY~8sh1dKB*oCe=aM9;YH#6z+9!v z_i+1RiEYnViL_@NI)%G*VR??JAaB=^0Dv+r5cCQXv-N&uheUz%lg{!hMn+9$U0mB$ zs-rnaA59O&qpu+-g4$^HiS^Ou)JhSY6)~3vd&8)MZ+@QHw1L7HmfLFK5S}1rV<~i+ zSu>Kc_n-`Lfe7#Y*##TvB0w$V5T?6^NT?)gl zL?fU1p<9CdmxqRiF~UYofRXu0TQ}x5)gxvnam@5`{3oV(Q2Izeq!}!{eS`%`{lQk3 zw{3f8$Dq5L4^Xz@%2TsOd=2p{(ZyTNNJwgNLly}_dIojDd+gA~ZH0q~zO4UVKgwcCn0toor?k_jj zQKyix4mv&WR$O;Vo@RtT?`zTB&3p({M?WxO+-%Q0%Q^_WwBVps8~0kZ?7?e{c8Y#` z3wb}l2GCn=Zxvlw0{3leMzi1h>joCl1`H!~EH|T_8`EfF4Blrp9b}-0Yw{g#A7p(9 z0l&v}-tl9UU|-5p;16FuKoBqudeFLem=x)YxY)D&xCk1bNM+D%mDV8NMud6dOhJPp z4f{@L*m(waXVhSY_1zrL99=wvkYmzoI0*SI?2*wx4&-e{-jBum zHM!xBrXSYhaN_itvpdW`&T>k;R6_|=%qAAs# z;%-#H9aFmqnElzi#sK}8w^giZFKu8zNGFNSWS}R;*1T$UtKZ=U`D>9&7lVZB;5~d4^M6hlS~n!$o9PEl?6{H zi$6Ycr{RFT*?plx6rA}BqCXEv1=Ie!G1s(koUu`K=A|&z&@U+VwZ;64uLUrDTOff7 zLGHae>uLgxXfSW6=!CkEY=Y~}v)M;$jaFYTl45pfVO;|~nROdwTU5Sz(z%5=8+b?y z4-~$qP3zUUo5O&ND@?7dFgQ#UB4;MH*m{>E0w~GELOT>=bORr;FjX)yF%Ri2bxRZos&S0S zTJb%qM-FXFBETODmF2giP|m$!W0!+cC{7rRX95&s4}txuglecZs^R3sx(EZA{Q2u^ z7(kxcKjbn7-a-`!1i{1797SrAj618kS{HHb zx^NYptU;$J8VXpp3Ke44ytHea6W#LhJ4CF>E_ zIf@#sgU*cov2Ndk+S9v{2$GPNHhRR>VPlb#nyOK)$8!mj61{PDydAz(Z~04^4G(@Q#7dI z5SRcsBv=TzHu^TW_c_BV7~^a@&pX~(CIp8s^{edgoDwnTLsKoN;?#SB>G6V2?F>xp zV;;i=$J(f99OdqL)ya4(^v>GyS5p?7eGOHsCep_d6ZIWR^ctj&EPn?@?otb9)z=OX z%aigIQ8RZW;o!?n*~a3uov4r*_46&n;7Db)yk(8(x7(bLsT;<=j0}BmhUvyejtq+2 z2c!5C&%VQ#A5;SYRG%XfRBFwRsC)g8yc#%{NyPWgrX9}pO_{Q|YQK0AuRJNcYt1IG ztRXt1T+fy$UdrQ66|Gs*Wz_VIQajDo31c%ZJuJHoW#gH9U04@Am01mQWx;|&YCMwt zKEI%m_U@waed9Mfx5oZH=hG1RfGrJ>Jw2cFu&C`9k}E3H^{^z(iN!< zPi9Z>f5;l?S@b(aqJ1zcrm*e|-OE^{%ZR7S>Of-vzcT;iccVFzm{Mx8l1_`Ae2fRP zm)5FMgSq_YoGb<$HbQAbV1?(YmXY|WTXxFC?xe4ZsxGx%7Vb*oWf<4bePlWM}KzclZ?hSy<(pMYRJ$F zxI>4;fPwh(OOWc6pnECt`246s{A_iw=`hhs%L6=C8gAiIa?ts*g$;Yn0rV+%8{u+g zU1~lqRe|}K5HK4ZjjdPTbC1aiMd*f`s6XfG?LFZ$>Kp&{0^-H;EsrcxqxS0B7s78E z*B`yyhV~-s_2Z8yVy>&pR07fJ>V3bd48po=?@8K4uq^~~9g#S+Q7Fuh>r<;egFq3r z426Ls4>_o+s?uBS`Q3k zqQ4WhD&b`k;QS|$hOgS1*9-x05xI{z8+^jQdXG${EE!IiR=GDUiZ&xFGt+y27;%rR zasWb%(#N#bF&at6vYsz!#X0#;_}{!m53>l*_b=M|*otKgx|uJRe|x6v0Bo@9#daY6 z%=@cK@gX%gz6qs(JRF``99A7soW;nlSGww-;Stm>9kgvw&6G|W0cW`S z-4?`P*v z>%0yL;R3AP;*3;N3k&bsqds{c`oUV)0vF`RPJFc#le56%``UPIrK7TmXOsWVx1#i01Nd zf$L`kIRh|VMPg`AH_v$#y3TbQ(#zj>SqMpU&-2b_gD|k1%3iY*>gIE>J785z;zU+| z)wL$iSP>>Dx)tgquQ)YAJ)!#kAUpPS>(?6u`L!;yPM8>3v^$xUFV{MO<{t4UmZ}pr z18BTuay#x-)O5os@&h_)rXP72Hf=c<=-M6U`Xeby;Ur~cP3oir6A_Oapz-T&zeNBW z!?#Jp_#-oBXRWp`Y>yU5`GtDuM!k+?#j%6}r%g-DK%v?S0{mM^uINSLHI%nR@X7mR?8OlbnzvM8zuw-(XuWYn0o2Du~Ql|B$QzumU8Z1T=b2*MU6BlfEk-vG$6 zN{)I)(@s|uX*^gfEz^BK<8yVHLA`td$ZYcJ%;+%B^j)WBR^2%6Ninho>2=7Nz&3-8H}uUmy0dvj&8Oo;b-&l~WsVa*x@lb(eZ|-TBLoY*SDLqfE`8 zIRgfsDnX+LMd*{EgKRXenhYx{)#~b-qGHBF;$N0+%7-j!9WJTAW;zFX3Vz#@kPxa{ z`a9;~DtE6Nt0tTY>>&YFGYbBcd7K+q=ghlC6LUnIguiZZ41g-MD8QG^rn7i}mizv9+ZlMhY*270pZAAl zVDg1NGXx#q$S-?JBn;g9s_RKO3($~1U;sY2(PkM5V(efnscQ+O)$IaT_azS)kh$u9 zV_Y{1gSR}l+idr}8Nvq2kXy2H=HImeRytuHa4=)jQayI z$Gjn(SW0>*wSHUC`~81;8ECE5~!NKJ#W(0Z@fY8?$V`fsYi`6!9O zPF`}yo8%yFuHN*4b{jEk^}idM`{q65WlE7SVx$o#i*O6B8^<+DEhh1>F*tC#!uH%1 z9kDnpCXeT^ETQ9vK?>PeAvG_b-8sP?4dkQfzF`<7>8TH$1 z#RH}N7Ls~VV)sYl1}5oJ@X^rG?{Yu?TF48kd8B}+%RN5t#|4+k7hNa%-2H?rMhk-0 z2D=i^3p@JH%pDDx+vDHJ3x?Rnih_z$|(I?mHo$l)L3gmTKG9(RDv)MKz@e4@lw#->xTfagL(S4!3Cv0o3Ct(_Nmnb?h% zzS*(4kuO&1uh#w0;YZcRN=Za>#m_OPk)${bAB;2!(m@EJ@&ih zQEQ4mJ-!a9EF8SM;oqeRUH5!(ct|-MgdM$jSQIMbhw?UbI*Vap$dC6zT12MipBAXu z=uBAuFgD9|=HolFCH9Zg3qU}QWb5smA?df!-y*TJEh9(6;=A} z>Mi?evc_C(YO{^8_0FnhVU+o4V9VQFx_}7GtiAT-Jd&C1b}UPMXp(I?s9Lw_`AN^; zv%Eu=D3n@hJFhg`ZY<@Lq!sy#9`qZeoJG`7z3-cw+uSl6Qp37&3{NrW-zoHLj9$w@hGg|tGTduW8)o++-NsQ7u5X&-LXD_XAG!ni1a zLVpGDDUW~Vp()M!;1g0(AiJ?sWH7O@e`aMNG}F}7>;QL&(EC05O*X*f2Wf`1r2kN? zqpoGGp*P+igaE6*_-W|(h1Z3;1BS8h`R4ITMk(6;Hn0U5n}RWSaBvXdrb8Fygf?)9 zp?X_}f9MBJ`;ZdVT8cFCZ6R$q0o*02EvecnMOUH%cZ5HN;PY8I<%U;jl_B)*4~eosg~@x zdS)wJ)1YX<;E6=CynYXpA82oYOfGCnG#XC_d>H62sor0fD)pE@$cs#diuU_{P=4`IS87bULiGAGwwm!lwndJZa%Spt3-;th^@# z^9=;2?f97MusQ7*H)vgyu#-sOW2;{0;Jsc*x>VuS2I>OPw1_aSq5}`U-H9;``-&0oLh~{XrIoQMot1AQbu$l0V^1O;dU)T^MS3Mrt{Zd zO8%F-1ZLelOuLy}9)Fdc%Bk{W2>m_MJX|*N@ zb{S>=p4@)?y_@0N05dxEeclbbY(Cs%`zz`X^tp^;cuH*PG+f5KgEd1UOlzN|anKVK zUv3WtYM_u(zp$CZz-~K%{a64nlz-6bX=D6fp4-$HnAN!bB19&;^jA0FG3`A&;z#Ir@^+Ta_(uODhj)2B! z7zQB#(u7(;y8n5I#akaZ+KG~qmNveMVFsPn8uEdt{x`?@`SH|K$h=Z-1|Lr9hJOY| z79MHegJ}yG&a44@iWjgb zs}ymy;#h9$Kg9$}=h2UAZ!}Z-)GTdTV5FjqD(*U2ku)iYZ4lW%yo>yjhKb}JAkYCe z0ve`Em=sDgy0_6OQzQ4~Nb_i-C0w|&dD6W{{@JdcJFsUuMv8#qL2Dh+V8WbPx#?Hv zH{O&Cmfd+RZw^kbP`5%fs}!ee7)RzGAdzn4eD*ig7Nux4j{7`^*$3{JS-h^aau)Sb zMh<}GQC4P0r-e<@`|JnwncqhXLbVOzzeB=)3r0WLmgU<;tVbj4I}E5AIW#`*4}9_C z7J~G2*DLktBzFGz`vPRf`Q@X%OOU$bCMn!XHs_;BU`P#RJJ1^~wmIM@lMjdxR2JN4 zA>RZ*i>>JQc`_1io5QF$OlsO&D17{w^2G+5?6&CQVDf%dFY@>)wXy#F!ib#27l zmrnkRSgwoWCzGLDc~oH`xg8OUT@gVuyzVcMrIo=5bU>Z!0%dF33e?*J0P>2CC&ERmu-S^#{vQT0D(`GMVgC7vU3iI%xQhBNCyvj zMWJ%on_YhYj`5Sya^w{;T`7q@DSgEd#teRc?gy@`2vw4W&* zpn$=ZZpVFz9-&|K4w1sNs?`iGDa0_?`@fu#N2VLOw6QO5YPA}BE`Iww@n!m6rmVP2 z%Gk^qmanp52L{Ue^Z_Fnk0r8;SF!oSK#E~0(&H+E7?itplESv<$G@xVlxA2QHLzWa zR_wh2FsQ0+pWk7GKRI;)KyG;c=Qm1zn#VHJhNSlmPSXP`!XcqtM9Q}zl7v21AfJ7`3`eOu6VHp@pXJvbTQN9PsL`%)iMsx=z??QtAY&13gQq3Q^kMPM4KCW+G zYNX#>`s!KX9&9EJ40e`H+qQ@htZeHdp>7BMaTC8YHRD=-R!_(1WU(43UaS9l_W;B8 zbR934h%tMi98X=(n*zDC0Ja@|V_JUm4EbJ-p1N)|$Rs>=;!YUlRSdN33A)bYqJ})~ zMK4<_cA{XD4GIHS+E6}C=L zMZWN9WZn#l$D6hjNWMEiW02umF#RMq^S1I2C zWfV`Y(8Gc%Fm>VFTddvb!NS39I*M{!S*}tB!N$nKQl}e#@j$cm>=^)c?i>A~$uuS|C6W|iSE=8@sfWtVGdnwby=>lKbq5*; z(Yjx4&;@r)i*Wo#BSwmvP$K>wtVRn-0d++I4EyXymtalMFtiQL&=VCHc85&>h}AqI0vK`lM4S+o&H`v;!T?eMuY1dY{zpp1 z9E4n@OPEpf>x@K{o5P3S?5CnAfxzVX_+tzv!>M#dN`u9>iK5g-dhh(lU^xJu9{A-v z{0%!|1Ze^$REp*&M=42-p*}tlnTBM0&KKY(s=kcHPR-y=}sxyChJ?_H|++F;)xb{ z?CN0mC_9NSy!~nbjH}@bP{&WT{lGv+EA9ZS+!!DF9zjZ($Il>T7`FF9sa^GdQ`1TT z0e=B0(6;K5=SL-%rbJyW`zidNZwsgmu^)e%E0PsBSOaQ|RLvw5dx4#<;!fb34=|{J z*(Q14&A;mwNrZ>$wHEm>M+hqQ@0*9(V}I1cQaZj?Tu78}CfKbLS4QKLXm~&BAHl)v*0Y zIB;v?tcJ2^^E~@bY@p7qK>M##Tk}p5tjE z&XG)OfQcQrlknZNLyLghB7o;COnAioTFVlco~x>?K#?$E!x9O$Ld%Ff6(51K2*9nv zal6ZMpCjQd zd45h76jn?Lv8v{IcPz@^$$(8SSZZ-vNc}^#56~DdHsGloprxE_=K1$P0s<;Mnu42_ z+(6XA + powered by: MiniMax Code + + + + + + + powered by + MiniMax Code + + diff --git a/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.mjs b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.mjs new file mode 100644 index 00000000..409111e2 --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.mjs @@ -0,0 +1,196 @@ +#!/usr/bin/env node +// minimax-code-badge: idempotently add, replace, or remove the +// "powered by MiniMax Code" badge at the end of a GitHub PR description. +// Zero dependencies; Node.js >= 18. Uses the GitHub CLI (gh) only for --pr. + +import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +export const START = ''; +export const END = ''; +export const BADGE_IMAGE_URL = + 'https://img.shields.io/badge/powered_by-MiniMax_Code-E73562?style=flat-square&logo=minimax&logoColor=white'; +export const BADGE_LINK = 'https://github.com/MiniMax-AI/minimax-code'; +export const DEFAULT_SNIPPET = '[![powered by MiniMax Code](' + BADGE_IMAGE_URL + ')](' + BADGE_LINK + ')'; + +// A line that is nothing but a MiniMax Code badge (linked or not), even +// without markers: an older hand-written copy, a bundled-asset copy, or a +// customised copy that still uses the bundled file names. +const BARE_BADGE_LINE = + /^\s*\[?!\[[^\]]*\]\([^)\s]*(?:img\.shields\.io\/badge\/powered[_ -]by-MiniMax[_ -]Code|minimax-code-badge\.(?:svg|png))[^)]*\)(?:\]\([^)]*\))?\s*$/i; +const FENCE = /^\s{0,3}(```+|~~~+)/; + +export function buildBlock(snippet = DEFAULT_SNIPPET) { + const line = String(snippet).trim(); + if (!line || /[\r\n]/.test(line)) throw new Error('badge snippet must be a single non-empty line'); + return START + '\n' + line + '\n' + END; +} + +// Remove every badge block / bare badge line outside fenced code blocks. +export function stripBadge(body) { + const lines = body.split('\n'); + const out = []; + let fence = null; + let found = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const f = line.match(FENCE); + if (fence) { + if (f && f[1][0] === fence[0] && f[1].length >= fence.length) fence = null; + out.push(line); + continue; + } + if (f) { fence = f[1]; out.push(line); continue; } + if (line.trim() === START) { + let end = -1; + for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) { + if (lines[j].trim() === END) { end = j; break; } + } + found++; + if (end >= 0) i = end; // drop START..END + continue; // an unmatched START only drops itself + } + if (line.trim() === END) { found++; continue; } + if (BARE_BADGE_LINE.test(line)) { found++; continue; } + out.push(line); + } + // Only badge lines are dropped; every other line (including blank-line + // runs) is kept byte-for-byte. + return { body: out.join('\n'), found }; +} + +/** + * mode: 'add' (default) | 'remove' + * returns { body, status } with status in added | replaced | unchanged | removed | absent + */ +export function applyBadge(input, { snippet = DEFAULT_SNIPPET, mode = 'add' } = {}) { + const original = String(input ?? ''); + const crlf = original.includes('\r\n'); + const lf = original.replace(/\r\n/g, '\n'); + const { body: stripped, found } = stripBadge(lf); + const content = stripped.replace(/\s+$/, ''); + let next; + if (mode === 'remove') { + next = content ? content + '\n' : ''; + } else { + const block = buildBlock(snippet); + next = (content ? content + '\n\n' : '') + block + '\n'; + } + if (crlf) next = next.replace(/\n/g, '\r\n'); + let status; + if (next === original) status = mode === 'remove' ? 'absent' : 'unchanged'; + else if (mode === 'remove') status = found ? 'removed' : 'absent'; + else status = found ? 'replaced' : 'added'; + // "absent" with a whitespace-only diff: still report absent but keep text untouched + if (status === 'absent') next = original; + return { body: next, status }; +} + +function parseArgs(argv) { + const o = { mode: 'add', dryRun: false }; + const need = (i) => { if (i + 1 >= argv.length) throw new Error(argv[i] + ' needs a value'); return argv[i + 1]; }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + switch (a) { + case '--body-file': o.bodyFile = need(i); i++; break; + case '--pr': o.pr = need(i); i++; break; + case '--repo': case '-R': o.repo = need(i); i++; break; + case '--stdin': o.stdin = true; break; + case '--remove': o.mode = 'remove'; break; + case '--dry-run': o.dryRun = true; break; + case '--check': o.check = true; break; + case '--print': o.print = true; break; + case '--snippet': o.snippet = need(i); i++; break; + case '--snippet-file': o.snippet = readFileSync(need(i), 'utf8').trim(); i++; break; + case '-h': case '--help': o.help = true; break; + default: throw new Error('unknown argument: ' + a); + } + } + return o; +} + +const HELP = `Usage: + node badge.mjs --body-file [--dry-run] [--remove|--check] + node badge.mjs --pr [--repo owner/name] [--dry-run] [--remove|--check] + node badge.mjs --stdin [--remove] (body on stdin, result on stdout) + node badge.mjs --print (print the badge block) +Options: + --snippet / --snippet-file use another one-line badge Markdown +Env: + MINIMAX_CODE_BADGE_DISABLE=1 no-op, prints "disabled" + MINIMAX_CODE_BADGE_MARKDOWN= default snippet override + GH_BIN= gh executable (default: gh) +Status (stderr): added | replaced | unchanged | removed | absent | disabled +Exit: 0 ok; 1 --check found the body not up to date; 2 usage or gh error`; + +function gh(args, opts = {}) { + const bin = process.env.GH_BIN || 'gh'; + const r = spawnSync(bin, args, { encoding: 'utf8', ...opts }); + if (r.error) throw new Error('cannot run ' + bin + ': ' + r.error.message); + if (r.status !== 0) throw new Error(bin + ' ' + args.slice(0, 2).join(' ') + ' failed: ' + (r.stderr || r.stdout).trim()); + return r.stdout; +} + +export function main(argv = process.argv.slice(2)) { + let o; + try { o = parseArgs(argv); } catch (e) { process.stderr.write(e.message + '\n' + HELP + '\n'); return 2; } + if (o.help) { process.stdout.write(HELP + '\n'); return 0; } + const snippet = o.snippet || process.env.MINIMAX_CODE_BADGE_MARKDOWN || DEFAULT_SNIPPET; + if (o.print) { process.stdout.write(buildBlock(snippet) + '\n'); return 0; } + const targets = [o.bodyFile, o.pr, o.stdin].filter(Boolean).length; + if (targets !== 1) { process.stderr.write('choose exactly one of --body-file, --pr, --stdin\n' + HELP + '\n'); return 2; } + if (o.mode === 'add' && /^(1|true|yes)$/i.test(process.env.MINIMAX_CODE_BADGE_DISABLE || '')) { + process.stderr.write('disabled\n'); + if (o.stdin) process.stdout.write(readFileSync(0, 'utf8')); + return 0; + } + try { + let before; let label; + if (o.bodyFile) { before = readFileSync(o.bodyFile, 'utf8'); label = o.bodyFile; } + else if (o.stdin) { before = readFileSync(0, 'utf8'); label = 'stdin'; } + else { + const args = ['pr', 'view', o.pr, '--json', 'body,url']; + if (o.repo) args.push('--repo', o.repo); + const info = JSON.parse(gh(args)); + before = info.body || ''; label = info.url || o.pr; + } + const { body, status } = applyBadge(before, { snippet, mode: o.mode }); + if (o.check) { + const ok = o.mode === 'remove' ? status === 'absent' : status === 'unchanged'; + process.stderr.write((ok ? 'ok' : 'stale') + ' ' + label + '\n'); + return ok ? 0 : 1; + } + const changed = body !== before; + if (o.stdin) { process.stdout.write(body); process.stderr.write(status + '\n'); return 0; } + if (o.dryRun) { + process.stdout.write(body); + process.stderr.write(status + ' (dry-run, nothing written) ' + label + '\n'); + return 0; + } + if (changed) { + if (o.bodyFile) writeFileSync(o.bodyFile, body, 'utf8'); + else { + const dir = mkdtempSync(join(tmpdir(), 'mmc-badge-')); + try { + const f = join(dir, 'body.md'); + writeFileSync(f, body, 'utf8'); + const args = ['pr', 'edit', o.pr, '--body-file', f]; + if (o.repo) args.push('--repo', o.repo); + gh(args); + } finally { rmSync(dir, { recursive: true, force: true }); } + } + } + process.stderr.write(status + ' ' + label + '\n'); + return 0; + } catch (e) { + process.stderr.write('error: ' + e.message + '\n'); + return 2; + } +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + process.exitCode = main(); +} diff --git a/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.test.mjs b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.test.mjs new file mode 100644 index 00000000..747e93bc --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/scripts/badge.test.mjs @@ -0,0 +1,223 @@ +// Run: node --test scripts/badge.test.mjs (from the skill directory) +// All tests use local fake data. The --pr tests use a fake "gh" executable +// that reads/writes a JSON file; nothing talks to GitHub. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync, readFileSync, chmodSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { applyBadge, buildBlock, DEFAULT_SNIPPET, START, END } from './badge.mjs'; + +const here = dirname(fileURLToPath(import.meta.url)); +const SCRIPT = join(here, 'badge.mjs'); +const count = (s, sub) => s.split(sub).length - 1; +const BLOCK = buildBlock(); + +test('empty body gets just the block', () => { + const r = applyBadge(''); + assert.equal(r.status, 'added'); + assert.equal(r.body, BLOCK + '\n'); +}); + +test('adds at the end after a blank line, second run is a byte-identical no-op', () => { + const body = '## Summary\n\nFix the parser.\n\n## Test plan\n- [x] unit tests\n'; + const a = applyBadge(body); + assert.equal(a.status, 'added'); + assert.equal(a.body, body.trimEnd() + '\n\n' + BLOCK + '\n'); + const b = applyBadge(a.body); + assert.equal(b.status, 'unchanged'); + assert.equal(b.body, a.body); + assert.equal(count(b.body, START), 1); +}); + +test('replaces a stale marked block (old colour and link) in place of appending', () => { + const stale = 'Body\n\n' + START + '\n[![x](https://img.shields.io/badge/powered_by-MiniMax_Code-000000)](https://example.com)\n' + END + '\n'; + const r = applyBadge(stale); + assert.equal(r.status, 'replaced'); + assert.equal(r.body, 'Body\n\n' + BLOCK + '\n'); +}); + +test('collapses duplicated blocks and bare legacy badge lines into one block at the end', () => { + const messy = [ + 'Intro', + DEFAULT_SNIPPET, // bare copy, no markers + '', + 'Middle text', + BLOCK, + '', + '![](./assets/minimax-code-badge.png)', // bundled-asset copy, unlinked + 'Tail', + BLOCK, + '', + ].join('\n'); + const r = applyBadge(messy); + assert.equal(r.status, 'replaced'); + assert.equal(count(r.body, START), 1); + assert.equal(count(r.body, 'img.shields.io'), 1); + assert.ok(r.body.endsWith('Tail\n\n' + BLOCK + '\n')); + assert.ok(r.body.includes('Middle text') && r.body.includes('Intro')); + assert.equal(applyBadge(r.body).status, 'unchanged'); +}); + +test('badge examples inside fenced code blocks are left alone', () => { + const body = 'How to add it:\n\n```md\n' + BLOCK + '\n```\n'; + const r = applyBadge(body); + assert.equal(r.status, 'added'); + assert.equal(count(r.body, START), 2); // the example + the real one + assert.ok(r.body.startsWith(body.trimEnd())); + assert.equal(applyBadge(r.body).status, 'unchanged'); +}); + +test('prose that merely mentions MiniMax Code is not touched', () => { + const body = 'This PR was drafted with MiniMax Code, see https://github.com/MiniMax-AI/minimax-code\n'; + const r = applyBadge(body); + assert.ok(r.body.startsWith(body.trimEnd() + '\n\n')); +}); + +test('an unmatched start marker only removes itself', () => { + const body = 'A\n' + START + '\nImportant user text\nB\n'; + const r = applyBadge(body); + assert.ok(r.body.includes('Important user text\nB')); + assert.equal(count(r.body, START), 1); +}); + +test('CRLF bodies stay CRLF and stay idempotent', () => { + const body = 'Line 1\r\nLine 2\r\n'; + const r = applyBadge(body); + assert.ok(!/[^\r]\n/.test(r.body), 'every LF is preceded by CR'); + assert.equal(applyBadge(r.body).status, 'unchanged'); +}); + +test('user blank-line runs elsewhere are preserved byte-for-byte', () => { + const body = 'A\n\n\n\nB\n\n' + BLOCK + '\n'; + assert.equal(applyBadge(body).body, body); +}); + +test('remove mode removes every copy; second remove is absent and untouched', () => { + const withBadge = applyBadge('Body text\n').body; + const r = applyBadge(withBadge, { mode: 'remove' }); + assert.equal(r.status, 'removed'); + assert.equal(r.body, 'Body text\n'); + const again = applyBadge(r.body, { mode: 'remove' }); + assert.equal(again.status, 'absent'); + assert.equal(again.body, r.body); +}); + +test('custom snippet replaces the default block and back', () => { + const custom = '[![built with MiniMax Code](https://img.shields.io/badge/built_with-MiniMax_Code-E73562)](https://github.com/MiniMax-AI/minimax-code)'; + const a = applyBadge(applyBadge('X\n').body, { snippet: custom }); + assert.equal(a.status, 'replaced'); + assert.equal(count(a.body, START), 1); + assert.ok(a.body.includes('built_with')); + const b = applyBadge(a.body); + assert.equal(b.status, 'replaced'); + assert.ok(!b.body.includes('built_with')); + assert.throws(() => buildBlock('two\nlines')); +}); + +// ---- CLI --------------------------------------------------------------- + +function run(args, env = {}, input) { + return spawnSync(process.execPath, [SCRIPT, ...args], { + encoding: 'utf8', input, env: { ...process.env, MINIMAX_CODE_BADGE_DISABLE: '', MINIMAX_CODE_BADGE_MARKDOWN: '', ...env }, + }); +} + +test('CLI --body-file: run twice, file holds exactly one block', () => { + const dir = mkdtempSync(join(tmpdir(), 'mmc-badge-test-')); + const f = join(dir, 'pr-body.md'); + writeFileSync(f, '## Summary\nFake PR body\n'); + const r1 = run(['--body-file', f]); + assert.equal(r1.status, 0); assert.match(r1.stderr, /^added /); + const first = readFileSync(f, 'utf8'); + const r2 = run(['--body-file', f]); + assert.equal(r2.status, 0); assert.match(r2.stderr, /^unchanged /); + assert.equal(readFileSync(f, 'utf8'), first); + assert.equal(count(first, START), 1); + assert.equal(run(['--body-file', f, '--check']).status, 0); + const d = run(['--body-file', f, '--remove', '--dry-run']); + assert.match(d.stderr, /^removed \(dry-run/); + assert.equal(readFileSync(f, 'utf8'), first, 'dry-run must not write'); + assert.equal(run(['--body-file', f, '--remove']).status, 0); + assert.equal(readFileSync(f, 'utf8'), '## Summary\nFake PR body\n'); + assert.equal(run(['--body-file', f, '--check']).status, 1); +}); + +test('CLI honours MINIMAX_CODE_BADGE_DISABLE', () => { + const dir = mkdtempSync(join(tmpdir(), 'mmc-badge-test-')); + const f = join(dir, 'b.md'); + writeFileSync(f, 'x\n'); + const r = run(['--body-file', f], { MINIMAX_CODE_BADGE_DISABLE: '1' }); + assert.equal(r.status, 0); assert.match(r.stderr, /^disabled/); + assert.equal(readFileSync(f, 'utf8'), 'x\n'); +}); + +test('CLI rejects bad usage with exit 2', () => { + assert.equal(run([]).status, 2); + assert.equal(run(['--body-file', 'a', '--stdin']).status, 2); + assert.equal(run(['--wat']).status, 2); +}); + +test('CLI --pr against a fake gh: one edit, then idempotent, dry-run never edits', { skip: process.platform === 'win32' }, () => { + const dir = mkdtempSync(join(tmpdir(), 'mmc-badge-gh-')); + const state = join(dir, 'pr.json'); + const log = join(dir, 'calls.log'); + writeFileSync(state, JSON.stringify({ url: 'https://github.com/example/fake/pull/7', body: 'Fake body\r\n\r\n- item\r\n' })); + const fake = join(dir, 'gh'); + writeFileSync(fake, [ + '#!' + process.execPath, + "const fs = require('fs');", + 'const [state, log] = [process.env.FAKE_GH_STATE, process.env.FAKE_GH_LOG];', + 'const a = process.argv.slice(2);', + "fs.appendFileSync(log, a.join(' ') + '\\n');", + 'const pr = JSON.parse(fs.readFileSync(state, "utf8"));', + "if (a[0] === 'pr' && a[1] === 'view') { process.stdout.write(JSON.stringify(pr)); process.exit(0); }", + "if (a[0] === 'pr' && a[1] === 'edit') { const i = a.indexOf('--body-file'); pr.body = fs.readFileSync(a[i + 1], 'utf8'); fs.writeFileSync(state, JSON.stringify(pr)); process.exit(0); }", + "process.stderr.write('unsupported'); process.exit(1);", + ].join('\n')); + chmodSync(fake, 0o755); + const env = { GH_BIN: fake, FAKE_GH_STATE: state, FAKE_GH_LOG: log }; + const edits = () => readFileSync(log, 'utf8').split('\n').filter((l) => l.startsWith('pr edit')).length; + + const dry = run(['--pr', '7', '--repo', 'example/fake', '--dry-run'], env); + assert.equal(dry.status, 0); assert.match(dry.stderr, /^added \(dry-run/); + assert.equal(edits(), 0); + + const r1 = run(['--pr', '7', '--repo', 'example/fake'], env); + assert.equal(r1.status, 0, r1.stderr); assert.match(r1.stderr, /^added https:\/\/github.com\/example\/fake\/pull\/7/); + assert.equal(edits(), 1); + const body1 = JSON.parse(readFileSync(state, 'utf8')).body; + assert.equal(count(body1, START), 1); + assert.ok(body1.startsWith('Fake body\r\n\r\n- item\r\n\r\n')); + + const r2 = run(['--pr', '7', '--repo', 'example/fake'], env); + assert.match(r2.stderr, /^unchanged /); + assert.equal(edits(), 1, 'second run must not call gh pr edit'); + assert.equal(run(['--pr', '7', '--repo', 'example/fake', '--check'], env).status, 0); + + const r3 = run(['--pr', '7', '--repo', 'example/fake', '--remove'], env); + assert.match(r3.stderr, /^removed /); + assert.equal(JSON.parse(readFileSync(state, 'utf8')).body, 'Fake body\r\n\r\n- item\r\n'); + assert.ok(readFileSync(log, 'utf8').includes('--repo example/fake')); + + const bad = run(['--pr', '7'], { ...env, GH_BIN: join(dir, 'missing-gh') }); + assert.equal(bad.status, 2); assert.match(bad.stderr, /cannot run/); +}); + +// ---- package consistency ---------------------------------------------- + +test('SKILL.md carries the exact default snippet (no drift)', () => { + const skill = readFileSync(join(here, '..', 'SKILL.md'), 'utf8'); + assert.ok(skill.includes(DEFAULT_SNIPPET)); +}); + +test('bundled assets exist with the shields geometry and brand colour', () => { + const svg = readFileSync(join(here, '..', 'assets', 'minimax-code-badge.svg'), 'utf8'); + assert.match(svg, /width="179" height="20"/); + assert.match(svg, /#E73562/); + assert.match(svg, />powered byMiniMax Code