feat(gsd-tools): add CLI utility for command extraction
Centralizes repetitive bash patterns across 50+ GSD files. Functions: state load/update, resolve-model, find-phase, commit, verify-summary, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section Enables thin orchestrator pattern. Replaces 200-400 chars per usage. Ported from: get-shit-done-v2@ce5400f
This commit is contained in:
642
get-shit-done/bin/gsd-tools.js
Executable file
642
get-shit-done/bin/gsd-tools.js
Executable file
@@ -0,0 +1,642 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* GSD Tools — CLI utility for GSD workflow operations
|
||||
*
|
||||
* Replaces repetitive inline bash patterns across ~50 GSD command/workflow/agent files.
|
||||
* Centralizes: config parsing, model resolution, phase lookup, git commits, summary verification.
|
||||
*
|
||||
* Usage: node gsd-tools.js <command> [args] [--raw]
|
||||
*
|
||||
* Commands:
|
||||
* state load Load project config + state
|
||||
* state update <field> <value> Update a STATE.md field
|
||||
* resolve-model <agent-type> Get model for agent based on profile
|
||||
* find-phase <phase> Find phase directory by number
|
||||
* commit <message> [--files f1 f2] Commit planning docs
|
||||
* verify-summary <path> Verify a SUMMARY.md file
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// ─── Model Profile Table ─────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_PROFILES = {
|
||||
'gsd-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
||||
'gsd-roadmapper': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
||||
'gsd-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
||||
'gsd-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
||||
'gsd-project-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
||||
'gsd-research-synthesizer': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
||||
'gsd-debugger': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
||||
'gsd-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
|
||||
'gsd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
||||
'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
||||
'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
||||
};
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function loadConfig(cwd) {
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
const defaults = {
|
||||
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}',
|
||||
research: true,
|
||||
plan_checker: true,
|
||||
verifier: true,
|
||||
parallelization: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
const get = (key, nested) => {
|
||||
if (parsed[key] !== undefined) return parsed[key];
|
||||
if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
|
||||
return parsed[nested.section][nested.field];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const parallelization = (() => {
|
||||
const val = get('parallelization');
|
||||
if (typeof val === 'boolean') return val;
|
||||
if (typeof val === 'object' && val !== null && 'enabled' in val) return val.enabled;
|
||||
return defaults.parallelization;
|
||||
})();
|
||||
|
||||
return {
|
||||
model_profile: get('model_profile') ?? defaults.model_profile,
|
||||
commit_docs: get('commit_docs', { section: 'planning', field: 'commit_docs' }) ?? defaults.commit_docs,
|
||||
search_gitignored: get('search_gitignored', { section: 'planning', field: 'search_gitignored' }) ?? defaults.search_gitignored,
|
||||
branching_strategy: get('branching_strategy', { section: 'git', field: 'branching_strategy' }) ?? defaults.branching_strategy,
|
||||
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
||||
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
||||
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
||||
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
||||
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
||||
parallelization,
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
function isGitIgnored(cwd, targetPath) {
|
||||
try {
|
||||
execSync('git check-ignore -q -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function execGit(cwd, args) {
|
||||
try {
|
||||
const escaped = args.map(a => {
|
||||
if (/^[a-zA-Z0-9._\-/=:@]+$/.test(a)) return a;
|
||||
return "'" + a.replace(/'/g, "'\\''") + "'";
|
||||
});
|
||||
const stdout = execSync('git ' + escaped.join(' '), {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
return { exitCode: 0, stdout: stdout.trim(), stderr: '' };
|
||||
} catch (err) {
|
||||
return {
|
||||
exitCode: err.status ?? 1,
|
||||
stdout: (err.stdout ?? '').toString().trim(),
|
||||
stderr: (err.stderr ?? '').toString().trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePhaseName(phase) {
|
||||
const match = phase.match(/^(\d+(?:\.\d+)?)/);
|
||||
if (!match) return phase;
|
||||
const num = match[1];
|
||||
const parts = num.split('.');
|
||||
const padded = parts[0].padStart(2, '0');
|
||||
return parts.length > 1 ? `${padded}.${parts[1]}` : padded;
|
||||
}
|
||||
|
||||
function output(result, raw, rawValue) {
|
||||
if (raw && rawValue !== undefined) {
|
||||
process.stdout.write(String(rawValue));
|
||||
} else {
|
||||
process.stdout.write(JSON.stringify(result, null, 2));
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
process.stderr.write('Error: ' + message + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ─── Commands ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmdGenerateSlug(text, raw) {
|
||||
if (!text) {
|
||||
error('text required for slug generation');
|
||||
}
|
||||
|
||||
const slug = text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
const result = { slug };
|
||||
output(result, raw, slug);
|
||||
}
|
||||
|
||||
function cmdCurrentTimestamp(format, raw) {
|
||||
const now = new Date();
|
||||
let result;
|
||||
|
||||
switch (format) {
|
||||
case 'date':
|
||||
result = now.toISOString().split('T')[0];
|
||||
break;
|
||||
case 'filename':
|
||||
result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
||||
break;
|
||||
case 'full':
|
||||
default:
|
||||
result = now.toISOString();
|
||||
break;
|
||||
}
|
||||
|
||||
output({ timestamp: result }, raw, result);
|
||||
}
|
||||
|
||||
function cmdListTodos(cwd, area, raw) {
|
||||
const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
|
||||
|
||||
let count = 0;
|
||||
const todos = [];
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
||||
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
||||
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
||||
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
||||
|
||||
const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
|
||||
|
||||
// Apply area filter if specified
|
||||
if (area && todoArea !== area) continue;
|
||||
|
||||
count++;
|
||||
todos.push({
|
||||
file,
|
||||
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
||||
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
||||
area: todoArea,
|
||||
path: path.join('.planning', 'todos', 'pending', file),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const result = { count, todos };
|
||||
output(result, raw, count.toString());
|
||||
}
|
||||
|
||||
function cmdVerifyPathExists(cwd, targetPath, raw) {
|
||||
if (!targetPath) {
|
||||
error('path required for verification');
|
||||
}
|
||||
|
||||
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(fullPath);
|
||||
const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
|
||||
const result = { exists: true, type };
|
||||
output(result, raw, 'true');
|
||||
} catch {
|
||||
const result = { exists: false, type: null };
|
||||
output(result, raw, 'false');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Create default config
|
||||
const defaults = {
|
||||
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,
|
||||
},
|
||||
parallelization: true,
|
||||
};
|
||||
|
||||
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 cmdStateLoad(cwd, raw) {
|
||||
const config = loadConfig(cwd);
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
|
||||
let stateRaw = '';
|
||||
try {
|
||||
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
|
||||
} catch {}
|
||||
|
||||
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
|
||||
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
||||
const stateExists = stateRaw.length > 0;
|
||||
|
||||
const result = {
|
||||
config,
|
||||
state_raw: stateRaw,
|
||||
state_exists: stateExists,
|
||||
roadmap_exists: roadmapExists,
|
||||
config_exists: configExists,
|
||||
};
|
||||
|
||||
// For --raw, output a condensed key=value format
|
||||
if (raw) {
|
||||
const c = config;
|
||||
const lines = [
|
||||
`model_profile=${c.model_profile}`,
|
||||
`commit_docs=${c.commit_docs}`,
|
||||
`branching_strategy=${c.branching_strategy}`,
|
||||
`phase_branch_template=${c.phase_branch_template}`,
|
||||
`milestone_branch_template=${c.milestone_branch_template}`,
|
||||
`parallelization=${c.parallelization}`,
|
||||
`research=${c.research}`,
|
||||
`plan_checker=${c.plan_checker}`,
|
||||
`verifier=${c.verifier}`,
|
||||
`config_exists=${configExists}`,
|
||||
`roadmap_exists=${roadmapExists}`,
|
||||
`state_exists=${stateExists}`,
|
||||
];
|
||||
process.stdout.write(lines.join('\n'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
output(result);
|
||||
}
|
||||
|
||||
function cmdStateUpdate(cwd, field, value) {
|
||||
if (!field || value === undefined) {
|
||||
error('field and value required for state update');
|
||||
}
|
||||
|
||||
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||
try {
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (pattern.test(content)) {
|
||||
content = content.replace(pattern, `$1${value}`);
|
||||
fs.writeFileSync(statePath, content, 'utf-8');
|
||||
output({ updated: true });
|
||||
} else {
|
||||
output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
|
||||
}
|
||||
} catch {
|
||||
output({ updated: false, reason: 'STATE.md not found' });
|
||||
}
|
||||
}
|
||||
|
||||
function cmdResolveModel(cwd, agentType, raw) {
|
||||
if (!agentType) {
|
||||
error('agent-type required');
|
||||
}
|
||||
|
||||
const config = loadConfig(cwd);
|
||||
const profile = config.model_profile || 'balanced';
|
||||
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
if (!agentModels) {
|
||||
const result = { model: 'sonnet', profile, unknown_agent: true };
|
||||
output(result, raw, 'sonnet');
|
||||
return;
|
||||
}
|
||||
|
||||
const model = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
||||
const result = { model, profile };
|
||||
output(result, raw, model);
|
||||
}
|
||||
|
||||
function cmdFindPhase(cwd, phase, raw) {
|
||||
if (!phase) {
|
||||
error('phase identifier required');
|
||||
}
|
||||
|
||||
const phasesDir = path.join(cwd, '.planning', 'phases');
|
||||
const normalized = normalizePhaseName(phase);
|
||||
|
||||
const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] };
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
||||
|
||||
const match = dirs.find(d => d.startsWith(normalized));
|
||||
if (!match) {
|
||||
output(notFound, raw, '');
|
||||
return;
|
||||
}
|
||||
|
||||
const dirMatch = match.match(/^(\d+(?:\.\d+)?)-?(.*)/);
|
||||
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
||||
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
||||
|
||||
const phaseDir = path.join(phasesDir, match);
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
|
||||
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').sort();
|
||||
|
||||
const result = {
|
||||
found: true,
|
||||
directory: path.join('.planning', 'phases', match),
|
||||
phase_number: phaseNumber,
|
||||
phase_name: phaseName,
|
||||
plans,
|
||||
summaries,
|
||||
};
|
||||
|
||||
output(result, raw, result.directory);
|
||||
} catch {
|
||||
output(notFound, raw, '');
|
||||
}
|
||||
}
|
||||
|
||||
function cmdCommit(cwd, message, files, raw) {
|
||||
if (!message) {
|
||||
error('commit message required');
|
||||
}
|
||||
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
// Check commit_docs config
|
||||
if (!config.commit_docs) {
|
||||
const result = { committed: false, hash: null, reason: 'skipped_commit_docs_false' };
|
||||
output(result, raw, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if .planning is gitignored
|
||||
if (isGitIgnored(cwd, '.planning')) {
|
||||
const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
|
||||
output(result, raw, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage files
|
||||
const filesToStage = files && files.length > 0 ? files : ['.planning/'];
|
||||
for (const file of filesToStage) {
|
||||
execGit(cwd, ['add', file]);
|
||||
}
|
||||
|
||||
// Commit
|
||||
const commitResult = execGit(cwd, ['commit', '-m', message]);
|
||||
if (commitResult.exitCode !== 0) {
|
||||
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
||||
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
||||
output(result, raw, 'nothing');
|
||||
return;
|
||||
}
|
||||
const result = { committed: false, hash: null, reason: 'nothing_to_commit', error: commitResult.stderr };
|
||||
output(result, raw, 'nothing');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get short hash
|
||||
const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
|
||||
const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
|
||||
const result = { committed: true, hash, reason: 'committed' };
|
||||
output(result, raw, hash || 'committed');
|
||||
}
|
||||
|
||||
function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
|
||||
if (!summaryPath) {
|
||||
error('summary-path required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(cwd, summaryPath);
|
||||
const checkCount = checkFileCount || 2;
|
||||
|
||||
// Check 1: Summary exists
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
const result = {
|
||||
passed: false,
|
||||
checks: {
|
||||
summary_exists: false,
|
||||
files_created: { checked: 0, found: 0, missing: [] },
|
||||
commits_exist: false,
|
||||
self_check: 'not_found',
|
||||
},
|
||||
errors: ['SUMMARY.md not found'],
|
||||
};
|
||||
output(result, raw, 'failed');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const errors = [];
|
||||
|
||||
// Check 2: Spot-check files mentioned in summary
|
||||
const mentionedFiles = new Set();
|
||||
const patterns = [
|
||||
/`([^`]+\.[a-zA-Z]+)`/g,
|
||||
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
let m;
|
||||
while ((m = pattern.exec(content)) !== null) {
|
||||
const filePath = m[1];
|
||||
if (filePath && !filePath.startsWith('http') && filePath.includes('/')) {
|
||||
mentionedFiles.add(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
|
||||
const missing = [];
|
||||
for (const file of filesToCheck) {
|
||||
if (!fs.existsSync(path.join(cwd, file))) {
|
||||
missing.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: Commits exist
|
||||
const commitHashPattern = /\b[0-9a-f]{7,40}\b/g;
|
||||
const hashes = content.match(commitHashPattern) || [];
|
||||
let commitsExist = false;
|
||||
if (hashes.length > 0) {
|
||||
for (const hash of hashes.slice(0, 3)) {
|
||||
const result = execGit(cwd, ['cat-file', '-t', hash]);
|
||||
if (result.exitCode === 0 && result.stdout === 'commit') {
|
||||
commitsExist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 4: Self-check section
|
||||
let selfCheck = 'not_found';
|
||||
const selfCheckPattern = /##\s*(?:Self[- ]?Check|Verification|Quality Check)/i;
|
||||
if (selfCheckPattern.test(content)) {
|
||||
const passPattern = /(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i;
|
||||
const failPattern = /(?:fail|✗|❌|incomplete|blocked)/i;
|
||||
const checkSection = content.slice(content.search(selfCheckPattern));
|
||||
if (failPattern.test(checkSection)) {
|
||||
selfCheck = 'failed';
|
||||
} else if (passPattern.test(checkSection)) {
|
||||
selfCheck = 'passed';
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) errors.push('Missing files: ' + missing.join(', '));
|
||||
if (!commitsExist && hashes.length > 0) errors.push('Referenced commit hashes not found in git history');
|
||||
if (selfCheck === 'failed') errors.push('Self-check section indicates failure');
|
||||
|
||||
const checks = {
|
||||
summary_exists: true,
|
||||
files_created: { checked: filesToCheck.length, found: filesToCheck.length - missing.length, missing },
|
||||
commits_exist: commitsExist,
|
||||
self_check: selfCheck,
|
||||
};
|
||||
|
||||
const passed = missing.length === 0 && selfCheck !== 'failed';
|
||||
const result = { passed, checks, errors };
|
||||
output(result, raw, passed ? 'passed' : 'failed');
|
||||
}
|
||||
|
||||
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const rawIndex = args.indexOf('--raw');
|
||||
const raw = rawIndex !== -1;
|
||||
if (rawIndex !== -1) args.splice(rawIndex, 1);
|
||||
|
||||
const command = args[0];
|
||||
const cwd = process.cwd();
|
||||
|
||||
if (!command) {
|
||||
error('Usage: gsd-tools <command> [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section');
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'state': {
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'update') {
|
||||
cmdStateUpdate(cwd, args[2], args[3]);
|
||||
} else {
|
||||
cmdStateLoad(cwd, raw);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'resolve-model': {
|
||||
cmdResolveModel(cwd, args[1], raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'find-phase': {
|
||||
cmdFindPhase(cwd, args[1], raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'commit': {
|
||||
const message = args[1];
|
||||
// Parse --files flag
|
||||
const filesIndex = args.indexOf('--files');
|
||||
const files = filesIndex !== -1 ? args.slice(filesIndex + 1) : [];
|
||||
cmdCommit(cwd, message, files, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'verify-summary': {
|
||||
const summaryPath = args[1];
|
||||
const countIndex = args.indexOf('--check-count');
|
||||
const checkCount = countIndex !== -1 ? parseInt(args[countIndex + 1], 10) : 2;
|
||||
cmdVerifySummary(cwd, summaryPath, checkCount, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'generate-slug': {
|
||||
cmdGenerateSlug(args[1], raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'current-timestamp': {
|
||||
cmdCurrentTimestamp(args[1] || 'full', raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list-todos': {
|
||||
cmdListTodos(cwd, args[1], raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'verify-path-exists': {
|
||||
cmdVerifyPathExists(cwd, args[1], raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'config-ensure-section': {
|
||||
cmdConfigEnsureSection(cwd, raw);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
error(`Unknown command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user