1+ import { spawnSync } from "node:child_process" ;
12import fs from "node:fs" ;
23import path from "node:path" ;
34
45const readJson = ( file ) => JSON . parse ( fs . readFileSync ( file , "utf8" ) ) ;
56
6- function loadManifestEntries ( ) {
7- const explicit = ( process . env . OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "" )
8- . split ( / [ , \s ] + / u)
9- . map ( ( entry ) => entry . trim ( ) )
10- . filter ( Boolean ) ;
11- const extensionRoot = path . join ( process . cwd ( ) , "dist" , "extensions" ) ;
12- const manifestEntries = fs
13- . readdirSync ( extensionRoot , { withFileTypes : true } )
14- . filter ( ( entry ) => entry . isDirectory ( ) )
15- . map ( ( entry ) => {
16- const manifestPath = path . join ( extensionRoot , entry . name , "openclaw.plugin.json" ) ;
17- if ( ! fs . existsSync ( manifestPath ) ) {
7+ function resolveOpenClawEntry ( ) {
8+ if ( process . env . OPENCLAW_ENTRY ) {
9+ return process . env . OPENCLAW_ENTRY ;
10+ }
11+ for ( const entry of [ "dist/index.mjs" , "dist/index.js" ] ) {
12+ if ( fs . existsSync ( entry ) ) {
13+ return entry ;
14+ }
15+ }
16+ throw new Error ( "Missing OPENCLAW_ENTRY and dist/index.(m)js" ) ;
17+ }
18+
19+ function readPluginsList ( ) {
20+ const entry = resolveOpenClawEntry ( ) ;
21+ const result = spawnSync ( process . execPath , [ entry , "plugins" , "list" , "--json" ] , {
22+ cwd : process . cwd ( ) ,
23+ encoding : "utf8" ,
24+ env : process . env ,
25+ } ) ;
26+ if ( result . status !== 0 ) {
27+ throw new Error (
28+ `Unable to list packaged bundled plugins: ${ result . stderr || result . stdout || `exit ${ result . status } ` } ` ,
29+ ) ;
30+ }
31+ const payload = JSON . parse ( result . stdout ) ;
32+ return Array . isArray ( payload . plugins ) ? payload . plugins : [ ] ;
33+ }
34+
35+ function pluginRequiresConfig ( pluginDir ) {
36+ const manifestPath = path . join ( pluginDir , "openclaw.plugin.json" ) ;
37+ if ( ! fs . existsSync ( manifestPath ) ) {
38+ throw new Error ( `missing bundled plugin manifest: ${ manifestPath } ` ) ;
39+ }
40+ const manifest = readJson ( manifestPath ) ;
41+ const required = manifest . configSchema ?. required ;
42+ return Array . isArray ( required ) && required . some ( ( value ) => typeof value === "string" ) ;
43+ }
44+
45+ async function loadPackagedBundledEntries ( ) {
46+ return readPluginsList ( )
47+ . filter ( ( plugin ) => plugin ?. origin === "bundled" )
48+ . map ( ( plugin ) => {
49+ const id = typeof plugin . id === "string" ? plugin . id . trim ( ) : "" ;
50+ const rootDir = typeof plugin . rootDir === "string" ? plugin . rootDir . trim ( ) : "" ;
51+ const source = typeof plugin . source === "string" ? plugin . source . trim ( ) : "" ;
52+ const pluginDir = rootDir || ( source ? path . dirname ( source ) : "" ) ;
53+ if ( ! id || ! pluginDir ) {
1854 return null ;
1955 }
20- const manifest = readJson ( manifestPath ) ;
21- const id = typeof manifest . id === "string" ? manifest . id . trim ( ) : "" ;
22- if ( ! id ) {
23- throw new Error ( `Bundled plugin manifest is missing id: ${ manifestPath } ` ) ;
24- }
25- const required = manifest . configSchema ?. required ;
2656 return {
2757 id,
28- dir : entry . name ,
29- requiresConfig :
30- Array . isArray ( required ) && required . some ( ( value ) => typeof value === "string" ) ,
58+ dir : path . basename ( pluginDir ) ,
59+ rootDir : pluginDir ,
60+ requiresConfig : pluginRequiresConfig ( pluginDir ) ,
3161 } ;
3262 } )
3363 . filter ( Boolean )
3464 . toSorted ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
65+ }
66+
67+ async function loadManifestEntries ( ) {
68+ const explicit = ( process . env . OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "" )
69+ . split ( / [ , \s ] + / u)
70+ . map ( ( entry ) => entry . trim ( ) )
71+ . filter ( Boolean ) ;
72+ const manifestEntries = await loadPackagedBundledEntries ( ) ;
3573
3674 if ( explicit . length === 0 ) {
3775 return manifestEntries ;
3876 }
39- return explicit . map (
40- ( lookup ) =>
41- manifestEntries . find ( ( entry ) => entry . id === lookup || entry . dir === lookup ) || {
42- id : lookup ,
43- dir : lookup ,
44- requiresConfig : false ,
45- } ,
46- ) ;
77+ const available = manifestEntries . map ( ( entry ) => entry . id ) . join ( ", " ) ;
78+ return explicit . map ( ( lookup ) => {
79+ const found = manifestEntries . find ( ( entry ) => entry . id === lookup || entry . dir === lookup ) ;
80+ if ( ! found ) {
81+ throw new Error (
82+ `OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS entry is not an installable bundled plugin in this package: ${ lookup } . Available: ${ available } ` ,
83+ ) ;
84+ }
85+ return found ;
86+ } ) ;
4787}
4888
49- function selectedManifestEntries ( ) {
50- const allEntries = loadManifestEntries ( ) ;
89+ async function selectedManifestEntries ( ) {
90+ const allEntries = await loadManifestEntries ( ) ;
5191 const total = Number . parseInt ( process . env . OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL || "1" , 10 ) ;
5292 const index = Number . parseInt ( process . env . OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX || "0" , 10 ) ;
5393 if ( ! Number . isInteger ( total ) || total < 1 ) {
@@ -85,15 +125,23 @@ function assertInstalled(pluginId, pluginDir, requiresConfig) {
85125 }
86126 if (
87127 typeof record . sourcePath !== "string" ||
88- ! record . sourcePath . includes ( `/dist/extensions/${ pluginDir } ` )
128+ ! [ `/dist/extensions/${ pluginDir } ` , `/dist-runtime/extensions/${ pluginDir } ` ] . some ( ( fragment ) =>
129+ record . sourcePath . includes ( fragment ) ,
130+ )
89131 ) {
90132 throw new Error ( `unexpected bundled source path for ${ pluginId } : ${ record . sourcePath } ` ) ;
91133 }
92134 if ( record . installPath !== record . sourcePath ) {
93135 throw new Error ( `bundled install path should equal source path for ${ pluginId } ` ) ;
94136 }
95137 const paths = config . plugins ?. load ?. paths || [ ] ;
96- if ( paths . some ( ( entry ) => String ( entry ) . includes ( `/dist/extensions/${ pluginDir } ` ) ) ) {
138+ if (
139+ paths . some ( ( entry ) =>
140+ [ `/dist/extensions/${ pluginDir } ` , `/dist-runtime/extensions/${ pluginDir } ` ] . some (
141+ ( fragment ) => String ( entry ) . includes ( fragment ) ,
142+ ) ,
143+ )
144+ ) {
97145 throw new Error ( `config load paths should not include bundled install path for ${ pluginId } ` ) ;
98146 }
99147 if ( requiresConfig && config . plugins ?. entries ?. [ pluginId ] ?. enabled === true ) {
@@ -123,7 +171,13 @@ function assertUninstalled(pluginId, pluginDir) {
123171 throw new Error ( `install record still present after uninstall for ${ pluginId } ` ) ;
124172 }
125173 const paths = config . plugins ?. load ?. paths || [ ] ;
126- if ( paths . some ( ( entry ) => String ( entry ) . includes ( `/dist/extensions/${ pluginDir } ` ) ) ) {
174+ if (
175+ paths . some ( ( entry ) =>
176+ [ `/dist/extensions/${ pluginDir } ` , `/dist-runtime/extensions/${ pluginDir } ` ] . some (
177+ ( fragment ) => String ( entry ) . includes ( fragment ) ,
178+ ) ,
179+ )
180+ ) {
127181 throw new Error ( `load path still present after uninstall for ${ pluginId } ` ) ;
128182 }
129183 if ( config . plugins ?. entries ?. [ pluginId ] ) {
@@ -145,8 +199,8 @@ function assertUninstalled(pluginId, pluginDir) {
145199
146200const [ command , pluginId , pluginDir , requiresConfig ] = process . argv . slice ( 2 ) ;
147201if ( command === "select" ) {
148- for ( const entry of selectedManifestEntries ( ) ) {
149- console . log ( `${ entry . id } \t${ entry . dir } \t${ entry . requiresConfig ? "1" : "0" } ` ) ;
202+ for ( const entry of await selectedManifestEntries ( ) ) {
203+ console . log ( `${ entry . id } \t${ entry . dir } \t${ entry . requiresConfig ? "1" : "0" } \t ${ entry . rootDir } ` ) ;
150204 }
151205} else if ( command === "assert-installed" ) {
152206 assertInstalled ( pluginId , pluginDir , requiresConfig === "1" ) ;
0 commit comments