@@ -22,6 +22,11 @@ type PackageJson = {
2222 } ;
2323 } ;
2424} ;
25+ type BundledExtension = { id : string ; packageJson : PackageJson } ;
26+ type BundledExtensionMetadata = BundledExtension & {
27+ npmSpec ?: string ;
28+ rootDependencyMirrorAllowlist : string [ ] ;
29+ } ;
2530
2631const requiredPathGroups = [
2732 [ "dist/index.js" , "dist/index.mjs" ] ,
@@ -133,25 +138,23 @@ function normalizePluginSyncVersion(version: string): string {
133138
134139export function collectBundledExtensionRootDependencyGapErrors ( params : {
135140 rootPackage : PackageJson ;
136- extensions: Array < { id : string ; packageJson: PackageJson } > ;
141+ extensions : BundledExtension [ ] ;
137142} ) : string [ ] {
138143 const rootDeps = {
139144 ...params . rootPackage . dependencies ,
140145 ...params . rootPackage . optionalDependencies ,
141146 } ;
142147 const errors : string [ ] = [ ] ;
143148
144- for ( const extension of params . extensions ) {
145- if ( ! extension . packageJson . openclaw ?. install ?. npmSpec ) {
149+ for ( const extension of normalizeBundledExtensionMetadata ( params . extensions ) ) {
150+ if ( ! extension . npmSpec ) {
146151 continue ;
147152 }
148153
149154 const missing = Object . keys ( extension . packageJson . dependencies ?? { } )
150155 . filter ( ( dep ) => dep !== "openclaw" && ! rootDeps [ dep ] )
151156 . toSorted ( ) ;
152- const allowlisted = [
153- ...( extension . packageJson . openclaw ?. releaseChecks ?. rootDependencyMirrorAllowlist ?? [ ] ) ,
154- ] . toSorted ( ) ;
157+ const allowlisted = extension . rootDependencyMirrorAllowlist . toSorted ( ) ;
155158 if ( missing . join ( "\n" ) !== allowlisted . join ( "\n" ) ) {
156159 const unexpected = missing . filter ( ( dep ) => ! allowlisted . includes ( dep ) ) ;
157160 const resolved = allowlisted . filter ( ( dep ) => ! missing . includes ( dep ) ) ;
@@ -172,7 +175,56 @@ export function collectBundledExtensionRootDependencyGapErrors(params: {
172175 return errors ;
173176}
174177
175- function collectBundledExtensions ( ) : Array < { id: string ; packageJson: PackageJson } > {
178+ function normalizeBundledExtensionMetadata (
179+ extensions : BundledExtension [ ] ,
180+ ) : BundledExtensionMetadata [ ] {
181+ return extensions . map ( ( extension ) => ( {
182+ ...extension ,
183+ npmSpec :
184+ typeof extension . packageJson . openclaw ?. install ?. npmSpec === "string"
185+ ? extension . packageJson . openclaw . install . npmSpec . trim ( )
186+ : undefined ,
187+ rootDependencyMirrorAllowlist :
188+ extension . packageJson . openclaw ?. releaseChecks ?. rootDependencyMirrorAllowlist ?. filter (
189+ ( entry ) : entry is string => typeof entry === "string" && entry . trim ( ) . length > 0 ,
190+ ) ?? [ ] ,
191+ } ) ) ;
192+ }
193+
194+ export function collectBundledExtensionManifestErrors ( extensions : BundledExtension [ ] ) : string [ ] {
195+ const errors : string [ ] = [ ] ;
196+ for ( const extension of extensions ) {
197+ const install = extension . packageJson . openclaw ?. install ;
198+ if (
199+ install &&
200+ ( ! install . npmSpec || typeof install . npmSpec !== "string" || ! install . npmSpec . trim ( ) )
201+ ) {
202+ errors . push (
203+ `bundled extension '${ extension . id } ' manifest invalid | openclaw.install.npmSpec must be a non-empty string` ,
204+ ) ;
205+ }
206+
207+ const allowlist = extension . packageJson . openclaw ?. releaseChecks ?. rootDependencyMirrorAllowlist ;
208+ if ( allowlist === undefined ) {
209+ continue ;
210+ }
211+ if ( ! Array . isArray ( allowlist ) ) {
212+ errors . push (
213+ `bundled extension '${ extension . id } ' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array of non-empty strings` ,
214+ ) ;
215+ continue ;
216+ }
217+ const invalidEntries = allowlist . filter ( ( entry ) => typeof entry !== "string" || ! entry . trim ( ) ) ;
218+ if ( invalidEntries . length > 0 ) {
219+ errors . push (
220+ `bundled extension '${ extension . id } ' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must contain only non-empty strings` ,
221+ ) ;
222+ }
223+ }
224+ return errors ;
225+ }
226+
227+ function collectBundledExtensions ( ) : BundledExtension [ ] {
176228 const extensionsDir = resolve ( "extensions" ) ;
177229 const entries = readdirSync ( extensionsDir , { withFileTypes : true } ) . filter ( ( entry ) =>
178230 entry . isDirectory ( ) ,
@@ -195,9 +247,18 @@ function collectBundledExtensions(): Array<{ id: string; packageJson: PackageJso
195247
196248function checkBundledExtensionRootDependencyMirrors ( ) {
197249 const rootPackage = JSON . parse ( readFileSync ( resolve ( "package.json" ) , "utf8" ) ) as PackageJson ;
250+ const extensions = collectBundledExtensions ( ) ;
251+ const manifestErrors = collectBundledExtensionManifestErrors ( extensions ) ;
252+ if ( manifestErrors . length > 0 ) {
253+ console . error ( "release-check: bundled extension manifest validation failed:" ) ;
254+ for ( const error of manifestErrors ) {
255+ console . error ( ` - ${ error } ` ) ;
256+ }
257+ process . exit ( 1 ) ;
258+ }
198259 const errors = collectBundledExtensionRootDependencyGapErrors ( {
199260 rootPackage,
200- extensions : collectBundledExtensions ( ) ,
261+ extensions,
201262 } ) ;
202263 if ( errors . length > 0 ) {
203264 console . error ( "release-check: bundled extension root dependency mirror validation failed:" ) ;
0 commit comments