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 00000000..5fb7b220 Binary files /dev/null and b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.png differ diff --git a/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.svg b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.svg new file mode 100644 index 00000000..22e3362a --- /dev/null +++ b/plugins/hetaoBackend/minimax-code-badge/skills/minimax-code-badge/assets/minimax-code-badge.svg @@ -0,0 +1,12 @@ + + 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