chore: initial backup of Claude Code configuration
Includes: CLAUDE.md, settings.json, agents, commands, rules, skills, hooks, contexts, evals, get-shit-done, plugin configs (installed list and marketplace sources). Excludes credentials, runtime caches, telemetry, session data, and plugin binary cache.
This commit is contained in:
307
get-shit-done/bin/lib/config.cjs
Normal file
307
get-shit-done/bin/lib/config.cjs
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Config — Planning config CRUD operations
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { output, error } = require('./core.cjs');
|
||||
const {
|
||||
VALID_PROFILES,
|
||||
getAgentToModelMapForProfile,
|
||||
formatAgentToModelMapAsTable,
|
||||
} = require('./model-profiles.cjs');
|
||||
|
||||
const VALID_CONFIG_KEYS = new Set([
|
||||
'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
|
||||
'search_gitignored', 'brave_search',
|
||||
'workflow.research', 'workflow.plan_check', 'workflow.verifier',
|
||||
'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
|
||||
'workflow._auto_chain_active',
|
||||
'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template',
|
||||
'planning.commit_docs', 'planning.search_gitignored',
|
||||
]);
|
||||
|
||||
const CONFIG_KEY_SUGGESTIONS = {
|
||||
'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
|
||||
'agents.nyquist_validation_enabled': 'workflow.nyquist_validation',
|
||||
'nyquist.validation_enabled': 'workflow.nyquist_validation',
|
||||
};
|
||||
|
||||
function validateKnownConfigKeyPath(keyPath) {
|
||||
const suggested = CONFIG_KEY_SUGGESTIONS[keyPath];
|
||||
if (suggested) {
|
||||
error(`Unknown config key: ${keyPath}. Did you mean ${suggested}?`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the config file exists (creates it if needed).
|
||||
*
|
||||
* Does not call `output()`, so can be used as one step in a command without triggering `exit(0)` in
|
||||
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
||||
*/
|
||||
function ensureConfigFile(cwd) {
|
||||
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)) {
|
||||
return { created: false, reason: 'already_exists' };
|
||||
}
|
||||
|
||||
// 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');
|
||||
return { created: true, path: '.planning/config.json' };
|
||||
} catch (err) {
|
||||
error('Failed to create config.json: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to ensure the config file exists (creates it if needed).
|
||||
*
|
||||
* Note that this exits the process (via `output()`) even in the happy path; use
|
||||
* `ensureConfigFile()` directly if you need to avoid this.
|
||||
*/
|
||||
function cmdConfigEnsureSection(cwd, raw) {
|
||||
const ensureConfigFileResult = ensureConfigFile(cwd);
|
||||
if (ensureConfigFileResult.created) {
|
||||
output(ensureConfigFileResult, raw, 'created');
|
||||
} else {
|
||||
output(ensureConfigFileResult, raw, 'exists');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value in the config file, allowing nested values via dot notation (e.g.,
|
||||
* "workflow.research").
|
||||
*
|
||||
* Does not call `output()`, so can be used as one step in a command without triggering `exit(0)` in
|
||||
* the happy path. But note that `error()` will still `exit(1)` out of the process.
|
||||
*/
|
||||
function setConfigValue(cwd, keyPath, parsedValue) {
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
|
||||
// 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];
|
||||
}
|
||||
const previousValue = current[keys[keys.length - 1]]; // Capture previous value before overwriting
|
||||
current[keys[keys.length - 1]] = parsedValue;
|
||||
|
||||
// Write back
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
return { updated: true, key: keyPath, value: parsedValue, previousValue };
|
||||
} catch (err) {
|
||||
error('Failed to write config.json: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to set a value in the config file, allowing nested values via dot notation (e.g.,
|
||||
* "workflow.research").
|
||||
*
|
||||
* Note that this exits the process (via `output()`) even in the happy path; use `setConfigValue()`
|
||||
* directly if you need to avoid this.
|
||||
*/
|
||||
function cmdConfigSet(cwd, keyPath, value, raw) {
|
||||
if (!keyPath) {
|
||||
error('Usage: config-set <key.path> <value>');
|
||||
}
|
||||
|
||||
validateKnownConfigKeyPath(keyPath);
|
||||
|
||||
if (!VALID_CONFIG_KEYS.has(keyPath)) {
|
||||
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
const setConfigValueResult = setConfigValue(cwd, keyPath, parsedValue);
|
||||
output(setConfigValueResult, raw, `${keyPath}=${parsedValue}`);
|
||||
}
|
||||
|
||||
function cmdConfigGet(cwd, keyPath, raw) {
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
|
||||
if (!keyPath) {
|
||||
error('Usage: config-get <key.path>');
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to set the model profile in the config file.
|
||||
*
|
||||
* Note that this exits the process (via `output()`) even in the happy path.
|
||||
*/
|
||||
function cmdConfigSetModelProfile(cwd, profile, raw) {
|
||||
if (!profile) {
|
||||
error(`Usage: config-set-model-profile <${VALID_PROFILES.join('|')}>`);
|
||||
}
|
||||
|
||||
const normalizedProfile = profile.toLowerCase().trim();
|
||||
if (!VALID_PROFILES.includes(normalizedProfile)) {
|
||||
error(`Invalid profile '${profile}'. Valid profiles: ${VALID_PROFILES.join(', ')}`);
|
||||
}
|
||||
|
||||
// Ensure config exists (create if needed)
|
||||
ensureConfigFile(cwd);
|
||||
|
||||
// Set the model profile in the config
|
||||
const { previousValue } = setConfigValue(cwd, 'model_profile', normalizedProfile, raw);
|
||||
const previousProfile = previousValue || 'balanced';
|
||||
|
||||
// Build result value / message and return
|
||||
const agentToModelMap = getAgentToModelMapForProfile(normalizedProfile);
|
||||
const result = {
|
||||
updated: true,
|
||||
profile: normalizedProfile,
|
||||
previousProfile,
|
||||
agentToModelMap,
|
||||
};
|
||||
const rawValue = getCmdConfigSetModelProfileResultMessage(
|
||||
normalizedProfile,
|
||||
previousProfile,
|
||||
agentToModelMap
|
||||
);
|
||||
output(result, raw, rawValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message to display for the result of the `config-set-model-profile` command when
|
||||
* displaying raw output.
|
||||
*/
|
||||
function getCmdConfigSetModelProfileResultMessage(
|
||||
normalizedProfile,
|
||||
previousProfile,
|
||||
agentToModelMap
|
||||
) {
|
||||
const agentToModelTable = formatAgentToModelMapAsTable(agentToModelMap);
|
||||
const didChange = previousProfile !== normalizedProfile;
|
||||
const paragraphs = didChange
|
||||
? [
|
||||
`✓ Model profile set to: ${normalizedProfile} (was: ${previousProfile})`,
|
||||
'Agents will now use:',
|
||||
agentToModelTable,
|
||||
'Next spawned agents will use the new profile.',
|
||||
]
|
||||
: [
|
||||
`✓ Model profile is already set to: ${normalizedProfile}`,
|
||||
'Agents are using:',
|
||||
agentToModelTable,
|
||||
];
|
||||
return paragraphs.join('\n\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cmdConfigEnsureSection,
|
||||
cmdConfigSet,
|
||||
cmdConfigGet,
|
||||
cmdConfigSetModelProfile,
|
||||
};
|
||||
Reference in New Issue
Block a user