Skip to content

Commit 8329dce

Browse files
committed
chore: wip
1 parent 79d00fb commit 8329dce

File tree

5 files changed

+937
-64
lines changed

5 files changed

+937
-64
lines changed

packages/launchpad/src/commands/config.ts

Lines changed: 299 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,62 @@
11
import type { Command } from '../cli/types'
22
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'
37

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 = {}
624
for (let i = 0; i < argv.length; i++) {
725
const a = argv[i]
826
if (a === '--help' || a === '-h') opts.help = true
927
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
1031
else if (a.startsWith('--get=')) opts.get = a.slice('--get='.length)
1132
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+
}
1260
}
1361
return opts
1462
}
@@ -23,40 +71,268 @@ function getByPath(obj: any, path: string): any {
2371
return cur
2472
}
2573

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+
26175
const command: Command = {
27176
name: 'config',
28-
description: 'Inspect Launchpad configuration (read-only)',
177+
description: 'Manage Launchpad configuration',
29178
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')
38200
return 0
39201
}
40202

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))
46314
} 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))
48325
} else {
49-
console.warn(String(out))
326+
console.log(JSON.stringify(effectiveConfig, null, 2))
50327
}
51328
return 0
52329
}
53330

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))
58334
} else {
59-
console.warn(JSON.stringify(effective, null, 2))
335+
console.log(JSON.stringify(effectiveConfig, null, 2))
60336
}
61337
return 0
62338
},

0 commit comments

Comments
 (0)