/** * Config — Planning config CRUD operations */ const fs = require('fs'); const path = require('path'); const { output, error } = require('./core.cjs'); function cmdConfigEnsureSection(cwd, raw) { const configPath = path.join(cwd, '.planning', 'config.json'); const planningDir = path.join(cwd, '.planning'); // Ensure .planning directory exists try { if (!fs.existsSync(planningDir)) { fs.mkdirSync(planningDir, { recursive: true }); } } catch (err) { error('Failed to create .planning directory: ' + err.message); } // Check if config already exists if (fs.existsSync(configPath)) { const result = { created: false, reason: 'already_exists' }; output(result, raw, 'exists'); return; } // Detect Brave Search API key availability const homedir = require('os').homedir(); const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key'); const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile)); // Load user-level defaults from ~/.gsd/defaults.json if available const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json'); let userDefaults = {}; try { if (fs.existsSync(globalDefaultsPath)) { userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8')); // Migrate deprecated "depth" key to "granularity" if ('depth' in userDefaults && !('granularity' in userDefaults)) { const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' }; userDefaults.granularity = depthToGranularity[userDefaults.depth] || userDefaults.depth; delete userDefaults.depth; try { fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8'); } catch {} } } } catch (err) { // Ignore malformed global defaults, fall back to hardcoded } // Create default config (user-level defaults override hardcoded defaults) const hardcoded = { model_profile: 'balanced', commit_docs: true, search_gitignored: false, branching_strategy: 'none', phase_branch_template: 'gsd/phase-{phase}-{slug}', milestone_branch_template: 'gsd/{milestone}-{slug}', workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true, }, parallelization: true, brave_search: hasBraveSearch, }; const defaults = { ...hardcoded, ...userDefaults, workflow: { ...hardcoded.workflow, ...(userDefaults.workflow || {}) }, }; try { fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8'); const result = { created: true, path: '.planning/config.json' }; output(result, raw, 'created'); } catch (err) { error('Failed to create config.json: ' + err.message); } } function cmdConfigSet(cwd, keyPath, value, raw) { const configPath = path.join(cwd, '.planning', 'config.json'); if (!keyPath) { error('Usage: config-set '); } // Parse value (handle booleans and numbers) let parsedValue = value; if (value === 'true') parsedValue = true; else if (value === 'false') parsedValue = false; else if (!isNaN(value) && value !== '') parsedValue = Number(value); // Load existing config or start with empty object let config = {}; try { if (fs.existsSync(configPath)) { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } } catch (err) { error('Failed to read config.json: ' + err.message); } // Set nested value using dot notation (e.g., "workflow.research") const keys = keyPath.split('.'); let current = config; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (current[key] === undefined || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } current[keys[keys.length - 1]] = parsedValue; // write back try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); const result = { updated: true, key: keyPath, value: parsedValue }; output(result, raw, `${keyPath}=${parsedValue}`); } catch (err) { error('Failed to write config.json: ' + err.message); } } function cmdConfigGet(cwd, keyPath, raw) { const configPath = path.join(cwd, '.planning', 'config.json'); if (!keyPath) { error('Usage: config-get '); } let config = {}; try { if (fs.existsSync(configPath)) { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } else { error('No config.json found at ' + configPath); } } catch (err) { if (err.message.startsWith('No config.json')) throw err; error('Failed to read config.json: ' + err.message); } // Traverse dot-notation path (e.g., "workflow.auto_advance") const keys = keyPath.split('.'); let current = config; for (const key of keys) { if (current === undefined || current === null || typeof current !== 'object') { error(`Key not found: ${keyPath}`); } current = current[key]; } if (current === undefined) { error(`Key not found: ${keyPath}`); } output(current, raw, String(current)); } module.exports = { cmdConfigEnsureSection, cmdConfigSet, cmdConfigGet, };