1
1
import type { Command } from '../cli/types'
2
2
import { config , defaultConfig } from '../config'
3
+ import { validateConfig , getEffectiveConfig , applyProfile , type ValidationResult } from '../config-validation'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { homedir } from 'node:os'
3
7
4
- function parseArgs ( argv : string [ ] ) : { get ?: string , json ?: boolean , help ?: boolean } {
5
- const opts : { get ?: string , json ?: boolean , help ?: boolean } = { }
8
+ interface ConfigArgs {
9
+ get ?: string
10
+ set ?: string
11
+ value ?: string
12
+ unset ?: string
13
+ profile ?: string
14
+ validate ?: boolean
15
+ list ?: boolean
16
+ reset ?: boolean
17
+ json ?: boolean
18
+ help ?: boolean
19
+ action ?: 'get' | 'set' | 'unset' | 'validate' | 'list' | 'reset' | 'profiles'
20
+ }
21
+
22
+ function parseArgs ( argv : string [ ] ) : ConfigArgs {
23
+ const opts : ConfigArgs = { }
6
24
for ( let i = 0 ; i < argv . length ; i ++ ) {
7
25
const a = argv [ i ]
8
26
if ( a === '--help' || a === '-h' ) opts . help = true
9
27
else if ( a === '--json' ) opts . json = true
28
+ else if ( a === '--validate' ) opts . validate = true
29
+ else if ( a === '--list' ) opts . list = true
30
+ else if ( a === '--reset' ) opts . reset = true
10
31
else if ( a . startsWith ( '--get=' ) ) opts . get = a . slice ( '--get=' . length )
11
32
else if ( a === '--get' && i + 1 < argv . length ) opts . get = argv [ ++ i ]
33
+ else if ( a . startsWith ( '--set=' ) ) {
34
+ const parts = a . slice ( '--set=' . length ) . split ( '=' , 2 )
35
+ opts . set = parts [ 0 ]
36
+ opts . value = parts [ 1 ] || ''
37
+ }
38
+ else if ( a === '--set' && i + 2 < argv . length ) {
39
+ opts . set = argv [ ++ i ]
40
+ opts . value = argv [ ++ i ]
41
+ }
42
+ else if ( a . startsWith ( '--unset=' ) ) opts . unset = a . slice ( '--unset=' . length )
43
+ else if ( a === '--unset' && i + 1 < argv . length ) opts . unset = argv [ ++ i ]
44
+ else if ( a . startsWith ( '--profile=' ) ) opts . profile = a . slice ( '--profile=' . length )
45
+ else if ( a === '--profile' && i + 1 < argv . length ) opts . profile = argv [ ++ i ]
46
+ else if ( a === 'get' && ! opts . action ) opts . action = 'get'
47
+ else if ( a === 'set' && ! opts . action ) opts . action = 'set'
48
+ else if ( a === 'unset' && ! opts . action ) opts . action = 'unset'
49
+ else if ( a === 'validate' && ! opts . action ) opts . action = 'validate'
50
+ else if ( a === 'list' && ! opts . action ) opts . action = 'list'
51
+ else if ( a === 'reset' && ! opts . action ) opts . action = 'reset'
52
+ else if ( a === 'profiles' && ! opts . action ) opts . action = 'profiles'
53
+ else if ( ! opts . action && ( opts . get || opts . set || opts . validate || opts . list || opts . reset ) ) {
54
+ // Legacy support - action inferred from flags
55
+ }
56
+ else if ( ! opts . action && ! opts . get && ! opts . set && ! opts . unset ) {
57
+ // Positional argument for get
58
+ if ( a && ! a . startsWith ( '-' ) ) opts . get = a
59
+ }
12
60
}
13
61
return opts
14
62
}
@@ -23,40 +71,268 @@ function getByPath(obj: any, path: string): any {
23
71
return cur
24
72
}
25
73
74
+ function printValidationResult ( result : ValidationResult , json : boolean ) : void {
75
+ if ( json ) {
76
+ console . log ( JSON . stringify ( result , null , 2 ) )
77
+ } else {
78
+ if ( result . valid ) {
79
+ console . log ( '✅ Configuration is valid' )
80
+ } else {
81
+ console . log ( '❌ Configuration has errors:' )
82
+ for ( const error of result . errors ) {
83
+ console . log ( ` • ${ error } ` )
84
+ }
85
+ }
86
+
87
+ if ( result . warnings . length > 0 ) {
88
+ console . log ( '\n⚠️ Warnings:' )
89
+ for ( const warning of result . warnings ) {
90
+ console . log ( ` • ${ warning } ` )
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ function setByPath ( obj : any , path : string , value : any ) : boolean {
97
+ const parts = path . split ( '.' ) . filter ( Boolean )
98
+ let current = obj
99
+
100
+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
101
+ const part = parts [ i ]
102
+ if ( ! ( part in current ) || typeof current [ part ] !== 'object' ) {
103
+ current [ part ] = { }
104
+ }
105
+ current = current [ part ]
106
+ }
107
+
108
+ const lastPart = parts [ parts . length - 1 ]
109
+ if ( ! lastPart ) return false
110
+
111
+ // Try to parse the value appropriately
112
+ let parsedValue = value
113
+ if ( value === 'true' ) parsedValue = true
114
+ else if ( value === 'false' ) parsedValue = false
115
+ else if ( value === 'null' ) parsedValue = null
116
+ else if ( value === 'undefined' ) parsedValue = undefined
117
+ else if ( ! isNaN ( Number ( value ) ) && value !== '' ) parsedValue = Number ( value )
118
+ else if ( value . startsWith ( '[' ) || value . startsWith ( '{' ) ) {
119
+ try {
120
+ parsedValue = JSON . parse ( value )
121
+ } catch {
122
+ // Keep as string if JSON parsing fails
123
+ }
124
+ }
125
+
126
+ current [ lastPart ] = parsedValue
127
+ return true
128
+ }
129
+
130
+ function unsetByPath ( obj : any , path : string ) : boolean {
131
+ const parts = path . split ( '.' ) . filter ( Boolean )
132
+ let current = obj
133
+
134
+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
135
+ const part = parts [ i ]
136
+ if ( ! ( part in current ) ) return false
137
+ current = current [ part ]
138
+ }
139
+
140
+ const lastPart = parts [ parts . length - 1 ]
141
+ if ( ! lastPart || ! ( lastPart in current ) ) return false
142
+
143
+ delete current [ lastPart ]
144
+ return true
145
+ }
146
+
147
+ function getConfigPath ( ) : string {
148
+ return path . join ( homedir ( ) , '.config' , 'launchpad' , 'config.json' )
149
+ }
150
+
151
+ function loadUserConfig ( ) : any {
152
+ const configPath = getConfigPath ( )
153
+ try {
154
+ if ( fs . existsSync ( configPath ) ) {
155
+ return JSON . parse ( fs . readFileSync ( configPath , 'utf8' ) )
156
+ }
157
+ } catch ( error ) {
158
+ console . error ( `Warning: Failed to load user config: ${ ( error as Error ) . message } ` )
159
+ }
160
+ return { }
161
+ }
162
+
163
+ function saveUserConfig ( config : any ) : boolean {
164
+ const configPath = getConfigPath ( )
165
+ try {
166
+ fs . mkdirSync ( path . dirname ( configPath ) , { recursive : true } )
167
+ fs . writeFileSync ( configPath , JSON . stringify ( config , null , 2 ) )
168
+ return true
169
+ } catch ( error ) {
170
+ console . error ( `Error saving config: ${ ( error as Error ) . message } ` )
171
+ return false
172
+ }
173
+ }
174
+
26
175
const command : Command = {
27
176
name : 'config' ,
28
- description : 'Inspect Launchpad configuration (read-only) ' ,
177
+ description : 'Manage Launchpad configuration' ,
29
178
async run ( ctx ) {
30
- const { get, json, help } = parseArgs ( ctx . argv )
31
-
32
- if ( help ) {
33
- console . error ( 'Usage: launchpad config [--get <path>] [--json]' )
34
- console . error ( 'Examples:' )
35
- console . error ( ' launchpad config' )
36
- console . error ( ' launchpad config --get installPath' )
37
- console . error ( ' launchpad config --get services.php.version --json' )
179
+ const args = parseArgs ( ctx . argv )
180
+
181
+ if ( args . help ) {
182
+ console . log ( 'Usage: launchpad config [action] [options]' )
183
+ console . log ( '\nActions:' )
184
+ console . log ( ' get <key> Get configuration value' )
185
+ console . log ( ' set <key> <val> Set configuration value' )
186
+ console . log ( ' unset <key> Remove configuration value' )
187
+ console . log ( ' validate Validate current configuration' )
188
+ console . log ( ' list List all configuration' )
189
+ console . log ( ' reset Reset to default configuration' )
190
+ console . log ( ' profiles List available profiles' )
191
+ console . log ( '\nOptions:' )
192
+ console . log ( ' --json Output in JSON format' )
193
+ console . log ( ' --profile <name> Use specific profile' )
194
+ console . log ( '\nExamples:' )
195
+ console . log ( ' launchpad config get installPath' )
196
+ console . log ( ' launchpad config set verbose true' )
197
+ console . log ( ' launchpad config validate' )
198
+ console . log ( ' launchpad config list --json' )
199
+ console . log ( ' launchpad config --profile production validate' )
38
200
return 0
39
201
}
40
202
41
- if ( get ) {
42
- const val = getByPath ( config , get )
43
- const out = val === undefined ? getByPath ( defaultConfig , get ) : val
44
- if ( json ) {
45
- console . warn ( JSON . stringify ( out ) )
203
+ // Determine effective configuration
204
+ let effectiveConfig = getEffectiveConfig ( config )
205
+ if ( args . profile ) {
206
+ effectiveConfig = applyProfile ( config , args . profile )
207
+ }
208
+
209
+ // Handle different actions
210
+ if ( args . action === 'validate' || args . validate ) {
211
+ const result = validateConfig ( effectiveConfig , {
212
+ checkPaths : true ,
213
+ checkPermissions : true ,
214
+ } )
215
+ printValidationResult ( result , args . json || false )
216
+ return result . valid ? 0 : 1
217
+ }
218
+
219
+ if ( args . action === 'profiles' ) {
220
+ const profiles = {
221
+ active : effectiveConfig . profiles ?. active || 'development' ,
222
+ available : [ 'development' , 'production' , 'ci' ] ,
223
+ custom : Object . keys ( effectiveConfig . profiles ?. custom || { } ) ,
224
+ }
225
+
226
+ if ( args . json ) {
227
+ console . log ( JSON . stringify ( profiles , null , 2 ) )
228
+ } else {
229
+ console . log ( `Active profile: ${ profiles . active } ` )
230
+ console . log ( `Available profiles: ${ profiles . available . join ( ', ' ) } ` )
231
+ if ( profiles . custom . length > 0 ) {
232
+ console . log ( `Custom profiles: ${ profiles . custom . join ( ', ' ) } ` )
233
+ }
234
+ }
235
+ return 0
236
+ }
237
+
238
+ if ( args . action === 'reset' || args . reset ) {
239
+ const configPath = getConfigPath ( )
240
+ try {
241
+ if ( fs . existsSync ( configPath ) ) {
242
+ fs . unlinkSync ( configPath )
243
+ console . log ( '✅ Configuration reset to defaults' )
244
+ } else {
245
+ console . log ( 'ℹ️ No user configuration found, already using defaults' )
246
+ }
247
+ } catch ( error ) {
248
+ console . error ( `Error resetting config: ${ ( error as Error ) . message } ` )
249
+ return 1
250
+ }
251
+ return 0
252
+ }
253
+
254
+ if ( args . action === 'set' || args . set ) {
255
+ const key = args . set
256
+ const value = args . value
257
+
258
+ if ( ! key || value === undefined ) {
259
+ console . error ( 'Error: set requires both key and value' )
260
+ console . error ( 'Usage: launchpad config set <key> <value>' )
261
+ return 1
262
+ }
263
+
264
+ const userConfig = loadUserConfig ( )
265
+ if ( ! setByPath ( userConfig , key , value ) ) {
266
+ console . error ( `Error: Invalid key path: ${ key } ` )
267
+ return 1
268
+ }
269
+
270
+ if ( saveUserConfig ( userConfig ) ) {
271
+ console . log ( `✅ Set ${ key } = ${ value } ` )
272
+
273
+ // Validate the new configuration
274
+ const newConfig = { ...defaultConfig , ...userConfig }
275
+ const result = validateConfig ( newConfig )
276
+ if ( ! result . valid ) {
277
+ console . log ( '\n⚠️ Warning: Configuration now has validation errors:' )
278
+ for ( const error of result . errors ) {
279
+ console . log ( ` • ${ error } ` )
280
+ }
281
+ }
282
+ }
283
+ return 0
284
+ }
285
+
286
+ if ( args . action === 'unset' || args . unset ) {
287
+ const key = args . unset
288
+
289
+ if ( ! key ) {
290
+ console . error ( 'Error: unset requires a key' )
291
+ console . error ( 'Usage: launchpad config unset <key>' )
292
+ return 1
293
+ }
294
+
295
+ const userConfig = loadUserConfig ( )
296
+ if ( ! unsetByPath ( userConfig , key ) ) {
297
+ console . error ( `Error: Key not found: ${ key } ` )
298
+ return 1
299
+ }
300
+
301
+ if ( saveUserConfig ( userConfig ) ) {
302
+ console . log ( `✅ Unset ${ key } ` )
303
+ }
304
+ return 0
305
+ }
306
+
307
+ if ( args . action === 'get' || args . get ) {
308
+ const key = args . get
309
+ const val = getByPath ( effectiveConfig , key )
310
+ const out = val === undefined ? getByPath ( defaultConfig , key ) : val
311
+
312
+ if ( args . json ) {
313
+ console . log ( JSON . stringify ( out ) )
46
314
} else if ( typeof out === 'object' ) {
47
- console . warn ( JSON . stringify ( out , null , 2 ) )
315
+ console . log ( JSON . stringify ( out , null , 2 ) )
316
+ } else {
317
+ console . log ( String ( out ) )
318
+ }
319
+ return 0
320
+ }
321
+
322
+ if ( args . action === 'list' || args . list ) {
323
+ if ( args . json ) {
324
+ console . log ( JSON . stringify ( effectiveConfig ) )
48
325
} else {
49
- console . warn ( String ( out ) )
326
+ console . log ( JSON . stringify ( effectiveConfig , null , 2 ) )
50
327
}
51
328
return 0
52
329
}
53
330
54
- // Print merged effective config (shallow pretty)
55
- const effective = { ...defaultConfig , ...config }
56
- if ( json ) {
57
- console . warn ( JSON . stringify ( effective ) )
331
+ // Default: show effective config
332
+ if ( args . json ) {
333
+ console . log ( JSON . stringify ( effectiveConfig ) )
58
334
} else {
59
- console . warn ( JSON . stringify ( effective , null , 2 ) )
335
+ console . log ( JSON . stringify ( effectiveConfig , null , 2 ) )
60
336
}
61
337
return 0
62
338
} ,
0 commit comments