55// the directory, the icon and every referenced SKILL.md exist, every skill
66// directory is listed in the manifest, and every SKILL.md has frontmatter
77// with a `name` equal to its directory and a non-empty `description`.
8- // 2. Official MiniMax Code plugin validator: if MINIMAX_CODE_DIR points at a
8+ // 2. Marketplace: .claude-plugin/marketplace.json has exactly one entry per
9+ // stack under stacks/ and community/, named after its directory, with the
10+ // same description as its plugin.json and no component fields (the stack's
11+ // own plugin.json defines its skills).
12+ // 3. Version bump: if CHECK_BASE names a commit, every stack whose files
13+ // changed since its merge base with HEAD (uncommitted changes included)
14+ // must have a higher plugin.json "version" than at that merge base.
15+ // Marketplace installs look at the version to decide whether an update is
16+ // available, so a change without a bump never reaches installed copies.
17+ // 4. Official MiniMax Code plugin validator: if MINIMAX_CODE_DIR points at a
918// checkout of https://github.com/MiniMax-AI/minimax-code (only
1019// tools/plugins and schemas/plugins are needed), each stack is also run
1120// through tools/plugins/validate-plugin.mjs.
1221//
13- // Usage: node scripts/check.mjs (lint only)
22+ // Usage: node scripts/check.mjs (lint and marketplace)
23+ // CHECK_BASE=origin/main node scripts/check.mjs (plus the version bump check)
1424// MINIMAX_CODE_DIR=../minimax-code node scripts/check.mjs
1525
1626import { existsSync , readdirSync , readFileSync , statSync } from "node:fs" ;
@@ -20,9 +30,15 @@ import { fileURLToPath } from "node:url";
2030
2131const ROOT = fileURLToPath ( new URL ( ".." , import . meta. url ) ) ;
2232const STACK_PARENTS = [ "stacks" , "community" , "templates" ] ;
33+ /** Stacks under these parents are published in the marketplace. */
34+ const PUBLISHED_PARENTS = [ "stacks" , "community" ] ;
35+ const MARKETPLACE = ".claude-plugin/marketplace.json" ;
36+ const COMPONENT_FIELDS = [ "skills" , "commands" , "agents" , "hooks" , "mcpServers" ] ;
2337const ID = / ^ [ a - z ] [ a - z 0 - 9 ] * (?: [ . _ - ] [ a - z 0 - 9 ] + ) * $ / u;
2438const errors = [ ] ;
2539const err = ( where , msg ) => errors . push ( `${ where } : ${ msg } ` ) ;
40+ /** Parsed plugin.json of each stack root that has a valid one. */
41+ const manifests = new Map ( ) ;
2642
2743function dirs ( path ) {
2844 if ( ! existsSync ( path ) ) return [ ] ;
@@ -80,6 +96,7 @@ function checkStack(root) {
8096 } catch ( e ) {
8197 return err ( where , `plugin.json does not parse: ${ e . message } ` ) ;
8298 }
99+ manifests . set ( root , manifest ) ;
83100 if ( manifest . name !== name ) err ( where , `plugin name "${ manifest . name } " != directory "${ name } "` ) ;
84101 if ( ! ID . test ( name ) ) err ( where , `stack id "${ name } " should be lowercase letters, digits and - . _` ) ;
85102 if ( manifest . icon && ! existsSync ( join ( root , manifest . icon ) ) ) err ( where , `icon ${ manifest . icon } does not exist` ) ;
@@ -102,6 +119,85 @@ for (const parent of STACK_PARENTS)
102119
103120for ( const root of stacks ) checkStack ( root ) ;
104121
122+ function checkMarketplace ( ) {
123+ const file = join ( ROOT , MARKETPLACE ) ;
124+ if ( ! existsSync ( file ) ) return err ( MARKETPLACE , "missing" ) ;
125+ let catalog ;
126+ try {
127+ catalog = JSON . parse ( readFileSync ( file , "utf8" ) ) ;
128+ } catch ( e ) {
129+ return err ( MARKETPLACE , `does not parse: ${ e . message } ` ) ;
130+ }
131+ if ( ! catalog . name ) err ( MARKETPLACE , '"name" is required' ) ;
132+ if ( ! Array . isArray ( catalog . plugins ) ) return err ( MARKETPLACE , '"plugins" must be a list' ) ;
133+ const published = new Map (
134+ stacks
135+ . map ( ( root ) => [ relative ( ROOT , root ) , root ] )
136+ . filter ( ( [ rel ] ) => PUBLISHED_PARENTS . includes ( rel . split ( "/" ) [ 0 ] ) ) ,
137+ ) ;
138+ const listed = new Set ( ) ;
139+ for ( const entry of catalog . plugins ) {
140+ const where = `${ MARKETPLACE } entry "${ entry ?. name } "` ;
141+ const rel = typeof entry ?. source === "string" ? entry . source . replace ( / ^ \. \/ / u, "" ) . replace ( / \/ $ / u, "" ) : "" ;
142+ if ( ! published . has ( rel ) ) {
143+ err ( where , `"source" must be "./<stacks|community>/<id>" of an existing stack, got ${ JSON . stringify ( entry ?. source ) } ` ) ;
144+ continue ;
145+ }
146+ if ( listed . has ( rel ) ) err ( where , `${ rel } is listed twice` ) ;
147+ listed . add ( rel ) ;
148+ if ( entry . name !== rel . split ( "/" ) . pop ( ) ) err ( where , `"name" must equal the stack directory name` ) ;
149+ for ( const field of COMPONENT_FIELDS )
150+ if ( field in entry ) err ( where , `do not set "${ field } "; the stack's plugin.json defines its components` ) ;
151+ const manifest = manifests . get ( published . get ( rel ) ) ;
152+ if ( manifest && entry . description !== manifest . description )
153+ err ( where , '"description" must equal the description in the stack\'s plugin.json' ) ;
154+ }
155+ for ( const rel of published . keys ( ) ) if ( ! listed . has ( rel ) ) err ( MARKETPLACE , `${ rel } has no entry` ) ;
156+ }
157+
158+ function git ( args ) {
159+ const r = spawnSync ( "git" , args , { cwd : ROOT , encoding : "utf8" } ) ;
160+ return r . status === 0 ? r . stdout . trim ( ) : undefined ;
161+ }
162+
163+ function semver ( value ) {
164+ const m = / ^ ( \d + ) \. ( \d + ) \. ( \d + ) / u. exec ( typeof value === "string" ? value : "" ) ;
165+ return m ? m . slice ( 1 ) . map ( Number ) : undefined ;
166+ }
167+
168+ function isHigher ( next , previous ) {
169+ for ( let i = 0 ; i < 3 ; i += 1 ) if ( next [ i ] !== previous [ i ] ) return next [ i ] > previous [ i ] ;
170+ return false ;
171+ }
172+
173+ function checkVersionBumps ( ) {
174+ const base = process . env . CHECK_BASE ;
175+ if ( ! base ) return console . log ( "note: CHECK_BASE not set, skipping the version bump check." ) ;
176+ const mergeBase = git ( [ "merge-base" , base , "HEAD" ] ) ;
177+ if ( ! mergeBase ) return err ( "CHECK_BASE" , `no merge base between ${ base } and HEAD (fetch full history)` ) ;
178+ for ( const root of stacks ) {
179+ const rel = relative ( ROOT , root ) ;
180+ if ( ! PUBLISHED_PARENTS . includes ( rel . split ( "/" ) [ 0 ] ) ) continue ;
181+ if ( ! git ( [ "diff" , "--name-only" , mergeBase , "--" , rel ] ) ) continue ;
182+ const before = git ( [ "show" , `${ mergeBase } :${ rel } /.minimax-plugin/plugin.json` ] ) ;
183+ if ( before === undefined ) continue ; // a new stack has nothing to bump from
184+ let previous ;
185+ try {
186+ previous = JSON . parse ( before ) . version ;
187+ } catch {
188+ continue ;
189+ }
190+ const current = manifests . get ( root ) ?. version ;
191+ const next = semver ( current ) ;
192+ const prev = semver ( previous ) ;
193+ if ( ! next || ! prev || ! isHigher ( next , prev ) )
194+ err ( rel , `files changed since ${ base } , but version ${ current } is not higher than ${ previous } ; bump "version" in .minimax-plugin/plugin.json so marketplace installs see the update` ) ;
195+ }
196+ }
197+
198+ checkMarketplace ( ) ;
199+ checkVersionBumps ( ) ;
200+
105201const upstream = process . env . MINIMAX_CODE_DIR ;
106202if ( upstream ) {
107203 const validator = join ( upstream , "tools" , "plugins" , "validate-plugin.mjs" ) ;
0 commit comments