Skip to content

Commit 9b4711b

Browse files
committed
chore: wip
1 parent 4e9b431 commit 9b4711b

File tree

1 file changed

+283
-1
lines changed

1 file changed

+283
-1
lines changed

storage/framework/core/buddy/src/config.ts

Lines changed: 283 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,31 @@ import { existsSync } from 'node:fs'
33
import { log } from '@stacksjs/cli'
44
import { 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+
631
export 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

37248
let 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

Comments
 (0)