@@ -3,6 +3,31 @@ import { existsSync } from 'node:fs'
33import { log } from '@stacksjs/cli'
44import { path as p } from '@stacksjs/path'
55
6+ export interface BuddyPlugin {
7+ /**
8+ * Plugin name
9+ */
10+ name : string
11+
12+ /**
13+ * Plugin version
14+ */
15+ version ?: string
16+
17+ /**
18+ * Plugin setup function
19+ */
20+ setup : ( cli : CLI ) => void | Promise < void >
21+
22+ /**
23+ * Plugin hooks
24+ */
25+ hooks ?: {
26+ beforeCommand ?: ( context : any ) => void | Promise < void >
27+ afterCommand ?: ( context : any ) => void | Promise < void >
28+ }
29+ }
30+
631export interface BuddyConfig {
732 /**
833 * CLI theme to use (default, dracula, nord, solarized, monokai)
@@ -32,6 +57,192 @@ export interface BuddyConfig {
3257 * Aliases for commands
3358 */
3459 aliases ?: Record < string , string >
60+
61+ /**
62+ * Plugins to load
63+ */
64+ plugins ?: Array < BuddyPlugin | string >
65+ }
66+
67+ interface ValidationError {
68+ path : string
69+ message : string
70+ value ?: any
71+ }
72+
73+ /**
74+ * Validate the buddy config structure
75+ */
76+ export function validateConfig ( config : any ) : ValidationError [ ] {
77+ const errors : ValidationError [ ] = [ ]
78+
79+ if ( typeof config !== 'object' || config === null ) {
80+ errors . push ( {
81+ path : 'config' ,
82+ message : 'Config must be an object' ,
83+ value : config ,
84+ } )
85+ return errors
86+ }
87+
88+ // Validate theme
89+ if ( config . theme !== undefined ) {
90+ const validThemes = [ 'default' , 'dracula' , 'nord' , 'solarized' , 'monokai' ]
91+ if ( typeof config . theme !== 'string' || ! validThemes . includes ( config . theme ) ) {
92+ errors . push ( {
93+ path : 'theme' ,
94+ message : `Theme must be one of: ${ validThemes . join ( ', ' ) } ` ,
95+ value : config . theme ,
96+ } )
97+ }
98+ }
99+
100+ // Validate emoji
101+ if ( config . emoji !== undefined && typeof config . emoji !== 'boolean' ) {
102+ errors . push ( {
103+ path : 'emoji' ,
104+ message : 'Emoji must be a boolean' ,
105+ value : config . emoji ,
106+ } )
107+ }
108+
109+ // Validate commands
110+ if ( config . commands !== undefined ) {
111+ if ( ! Array . isArray ( config . commands ) ) {
112+ errors . push ( {
113+ path : 'commands' ,
114+ message : 'Commands must be an array' ,
115+ value : config . commands ,
116+ } )
117+ }
118+ else {
119+ config . commands . forEach ( ( cmd : any , index : number ) => {
120+ if ( typeof cmd !== 'function' ) {
121+ errors . push ( {
122+ path : `commands[${ index } ]` ,
123+ message : 'Each command must be a function' ,
124+ value : cmd ,
125+ } )
126+ }
127+ } )
128+ }
129+ }
130+
131+ // Validate defaultFlags
132+ if ( config . defaultFlags !== undefined ) {
133+ if ( typeof config . defaultFlags !== 'object' || config . defaultFlags === null ) {
134+ errors . push ( {
135+ path : 'defaultFlags' ,
136+ message : 'Default flags must be an object' ,
137+ value : config . defaultFlags ,
138+ } )
139+ }
140+ else {
141+ const flagKeys = [ 'verbose' , 'quiet' , 'debug' ]
142+ for ( const key of Object . keys ( config . defaultFlags ) ) {
143+ if ( ! flagKeys . includes ( key ) ) {
144+ errors . push ( {
145+ path : `defaultFlags.${ key } ` ,
146+ message : `Unknown flag. Valid flags are: ${ flagKeys . join ( ', ' ) } ` ,
147+ value : config . defaultFlags [ key ] ,
148+ } )
149+ }
150+ else if ( typeof config . defaultFlags [ key ] !== 'boolean' ) {
151+ errors . push ( {
152+ path : `defaultFlags.${ key } ` ,
153+ message : 'Flag value must be a boolean' ,
154+ value : config . defaultFlags [ key ] ,
155+ } )
156+ }
157+ }
158+ }
159+ }
160+
161+ // Validate aliases
162+ if ( config . aliases !== undefined ) {
163+ if ( typeof config . aliases !== 'object' || config . aliases === null || Array . isArray ( config . aliases ) ) {
164+ errors . push ( {
165+ path : 'aliases' ,
166+ message : 'Aliases must be an object mapping alias names to command names' ,
167+ value : config . aliases ,
168+ } )
169+ }
170+ else {
171+ for ( const [ alias , command ] of Object . entries ( config . aliases ) ) {
172+ if ( typeof command !== 'string' ) {
173+ errors . push ( {
174+ path : `aliases.${ alias } ` ,
175+ message : 'Alias target must be a string' ,
176+ value : command ,
177+ } )
178+ }
179+ }
180+ }
181+ }
182+
183+ // Validate plugins
184+ if ( config . plugins !== undefined ) {
185+ if ( ! Array . isArray ( config . plugins ) ) {
186+ errors . push ( {
187+ path : 'plugins' ,
188+ message : 'Plugins must be an array' ,
189+ value : config . plugins ,
190+ } )
191+ }
192+ else {
193+ config . plugins . forEach ( ( plugin : any , index : number ) => {
194+ if ( typeof plugin === 'string' ) {
195+ // String plugins are module paths, which is valid
196+ return
197+ }
198+ if ( typeof plugin !== 'object' || plugin === null ) {
199+ errors . push ( {
200+ path : `plugins[${ index } ]` ,
201+ message : 'Each plugin must be an object or a string (module path)' ,
202+ value : plugin ,
203+ } )
204+ return
205+ }
206+ if ( ! plugin . name || typeof plugin . name !== 'string' ) {
207+ errors . push ( {
208+ path : `plugins[${ index } ].name` ,
209+ message : 'Plugin must have a name property of type string' ,
210+ value : plugin . name ,
211+ } )
212+ }
213+ if ( ! plugin . setup || typeof plugin . setup !== 'function' ) {
214+ errors . push ( {
215+ path : `plugins[${ index } ].setup` ,
216+ message : 'Plugin must have a setup function' ,
217+ value : plugin . setup ,
218+ } )
219+ }
220+ if ( plugin . hooks !== undefined ) {
221+ if ( typeof plugin . hooks !== 'object' || plugin . hooks === null ) {
222+ errors . push ( {
223+ path : `plugins[${ index } ].hooks` ,
224+ message : 'Plugin hooks must be an object' ,
225+ value : plugin . hooks ,
226+ } )
227+ }
228+ }
229+ } )
230+ }
231+ }
232+
233+ // Check for unknown keys
234+ const validKeys = [ 'theme' , 'emoji' , 'commands' , 'defaultFlags' , 'aliases' , 'plugins' ]
235+ for ( const key of Object . keys ( config ) ) {
236+ if ( ! validKeys . includes ( key ) ) {
237+ errors . push ( {
238+ path : key ,
239+ message : `Unknown configuration key. Valid keys are: ${ validKeys . join ( ', ' ) } ` ,
240+ value : config [ key ] ,
241+ } )
242+ }
243+ }
244+
245+ return errors
35246}
36247
37248let cachedConfig : BuddyConfig | null = null
@@ -56,7 +267,19 @@ export async function loadBuddyConfig(): Promise<BuddyConfig> {
56267 try {
57268 log . debug ( `Loading buddy config from ${ configPath } ` )
58269 const configModule = await import ( configPath )
59- cachedConfig = configModule . default || configModule
270+ const config = configModule . default || configModule
271+
272+ // Validate config
273+ const validationErrors = validateConfig ( config )
274+ if ( validationErrors . length > 0 ) {
275+ log . error ( 'Invalid buddy.config.ts:' )
276+ for ( const error of validationErrors ) {
277+ log . error ( ` - ${ error . path } : ${ error . message } ` )
278+ }
279+ throw new Error ( 'Configuration validation failed' )
280+ }
281+
282+ cachedConfig = config
60283 return cachedConfig
61284 }
62285 catch ( error ) {
@@ -70,6 +293,65 @@ export async function loadBuddyConfig(): Promise<BuddyConfig> {
70293 return cachedConfig
71294}
72295
296+ /**
297+ * Load and initialize plugins
298+ */
299+ export async function loadPlugins ( cli : CLI , config : BuddyConfig ) : Promise < void > {
300+ if ( ! config . plugins || config . plugins . length === 0 ) {
301+ return
302+ }
303+
304+ log . debug ( `Loading ${ config . plugins . length } plugin(s)` )
305+
306+ for ( const pluginConfig of config . plugins ) {
307+ let plugin : BuddyPlugin
308+
309+ // If plugin is a string, it's a module path
310+ if ( typeof pluginConfig === 'string' ) {
311+ try {
312+ const pluginModule = await import ( pluginConfig )
313+ plugin = pluginModule . default || pluginModule
314+ }
315+ catch ( error ) {
316+ log . error ( `Failed to load plugin from ${ pluginConfig } :` , error )
317+ continue
318+ }
319+ }
320+ else {
321+ plugin = pluginConfig
322+ }
323+
324+ // Run plugin setup
325+ try {
326+ log . debug ( `Initializing plugin: ${ plugin . name } ${ plugin . version ? ` v${ plugin . version } ` : '' } ` )
327+ await plugin . setup ( cli )
328+
329+ // Register plugin hooks if provided
330+ if ( plugin . hooks ) {
331+ if ( plugin . hooks . beforeCommand ) {
332+ cli . on ( 'command:*' , async ( command ) => {
333+ if ( plugin . hooks ?. beforeCommand ) {
334+ await plugin . hooks . beforeCommand ( { command } )
335+ }
336+ } )
337+ }
338+ if ( plugin . hooks . afterCommand ) {
339+ cli . on ( 'command:*' , async ( command ) => {
340+ if ( plugin . hooks ?. afterCommand ) {
341+ await plugin . hooks . afterCommand ( { command } )
342+ }
343+ } )
344+ }
345+ }
346+
347+ log . debug ( `Plugin ${ plugin . name } initialized successfully` )
348+ }
349+ catch ( error ) {
350+ log . error ( `Failed to initialize plugin ${ plugin . name } :` , error )
351+ }
352+ }
353+ }
354+
73355/**
74356 * Apply buddy config to CLI instance
75357 */
0 commit comments