@pimzino/claude-code-spec-workflow
Version:
Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent task execution, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have
406 lines • 18.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpecWorkflowUpdater = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const task_generator_1 = require("./task-generator");
class SpecWorkflowUpdater {
constructor(projectRoot = process.cwd()) {
this.projectRoot = projectRoot;
this.claudeDir = (0, path_1.join)(projectRoot, '.claude');
this.commandsDir = (0, path_1.join)(this.claudeDir, 'commands');
this.templatesDir = (0, path_1.join)(this.claudeDir, 'templates');
this.agentsDir = (0, path_1.join)(this.claudeDir, 'agents');
this.specsDir = (0, path_1.join)(this.claudeDir, 'specs');
// Initialize source markdown directories
this.markdownDir = (0, path_1.join)(__dirname, 'markdown');
this.markdownCommandsDir = (0, path_1.join)(this.markdownDir, 'commands');
this.markdownTemplatesDir = (0, path_1.join)(this.markdownDir, 'templates');
this.markdownAgentsDir = (0, path_1.join)(this.markdownDir, 'agents');
}
/**
* Create a backup of the current .claude directory before making changes
*/
async createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = (0, path_1.join)(this.projectRoot, `.claude.backup-${timestamp}`);
try {
// Copy the entire .claude directory to backup location
await this.copyDirectory(this.claudeDir, backupDir);
console.log(`Backup created: ${backupDir}`);
return backupDir;
}
catch (error) {
console.error('Failed to create backup:', error);
throw new Error('Backup creation failed. Update cancelled for safety.');
}
}
/**
* Recursively copy a directory
*/
async copyDirectory(src, dest) {
await fs_1.promises.mkdir(dest, { recursive: true });
const entries = await fs_1.promises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = (0, path_1.join)(src, entry.name);
const destPath = (0, path_1.join)(dest, entry.name);
if (entry.isDirectory()) {
await this.copyDirectory(srcPath, destPath);
}
else {
await fs_1.promises.copyFile(srcPath, destPath);
}
}
}
/**
* List all backup directories in the project root
*/
async listBackups() {
try {
const entries = await fs_1.promises.readdir(this.projectRoot, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory() && entry.name.startsWith('.claude.backup-'))
.map(entry => entry.name)
.sort()
.reverse(); // Most recent first
}
catch {
return [];
}
}
/**
* Clean up old backups, keeping only the most recent N backups
*/
async cleanupOldBackups(keepCount = 5) {
const backups = await this.listBackups();
const toDelete = backups.slice(keepCount);
for (const backup of toDelete) {
try {
const backupPath = (0, path_1.join)(this.projectRoot, backup);
await fs_1.promises.rm(backupPath, { recursive: true });
console.log(`Cleaned up old backup: ${backup}`);
}
catch (error) {
console.warn(`Failed to clean up backup ${backup}:`, error);
}
}
}
/**
* Delete the entire .claude directory for a fresh start
*/
async deleteClaudeDirectory() {
try {
await fs_1.promises.rm(this.claudeDir, { recursive: true });
console.log('Deleted existing .claude directory for fresh installation');
}
catch (error) {
console.error('Failed to delete .claude directory:', error);
throw new Error('Failed to delete existing .claude directory. Update cancelled for safety.');
}
}
/**
* Restore user content (specs and task commands) from backup
*/
async restoreUserContent(backupDir) {
// The backup directory IS the .claude directory (from createBackup method)
const backupSpecsDir = (0, path_1.join)(backupDir, 'specs');
const backupCommandsDir = (0, path_1.join)(backupDir, 'commands');
try {
// Restore specs directory if it exists in backup
try {
await fs_1.promises.access(backupSpecsDir);
const specEntries = await fs_1.promises.readdir(backupSpecsDir, { withFileTypes: true });
const specDirs = specEntries.filter(entry => entry.isDirectory());
if (specDirs.length > 0) {
console.log(`Restoring ${specDirs.length} spec(s) from backup...`);
// Ensure specs directory exists
await fs_1.promises.mkdir(this.specsDir, { recursive: true });
// Copy each spec directory
for (const specDir of specDirs) {
const sourceSpecDir = (0, path_1.join)(backupSpecsDir, specDir.name);
const destSpecDir = (0, path_1.join)(this.specsDir, specDir.name);
await this.copyDirectory(sourceSpecDir, destSpecDir);
console.log(` Restored spec: ${specDir.name}`);
}
}
}
catch {
// No specs directory in backup, that's fine
console.log('No specs found in backup to restore');
}
// Restore task command directories if they exist in backup
try {
await fs_1.promises.access(backupCommandsDir);
const commandEntries = await fs_1.promises.readdir(backupCommandsDir, { withFileTypes: true });
const taskCommandDirs = commandEntries.filter(entry => entry.isDirectory());
if (taskCommandDirs.length > 0) {
console.log(`Restoring ${taskCommandDirs.length} task command folder(s) from backup...`);
// Ensure commands directory exists
await fs_1.promises.mkdir(this.commandsDir, { recursive: true });
// Copy each task command directory (these are spec-specific)
for (const taskDir of taskCommandDirs) {
const sourceTaskDir = (0, path_1.join)(backupCommandsDir, taskDir.name);
const destTaskDir = (0, path_1.join)(this.commandsDir, taskDir.name);
await this.copyDirectory(sourceTaskDir, destTaskDir);
console.log(` Restored task commands: ${taskDir.name}`);
}
}
}
catch {
// No task command directories in backup, that's fine
console.log('No task command directories found in backup to restore');
}
// Restore settings.local.json if it exists in backup
try {
const backupSettingsFile = (0, path_1.join)(backupDir, 'settings.local.json');
await fs_1.promises.access(backupSettingsFile);
const destSettingsFile = (0, path_1.join)(this.claudeDir, 'settings.local.json');
await fs_1.promises.copyFile(backupSettingsFile, destSettingsFile);
console.log(' Restored Claude Code settings (settings.local.json)');
}
catch {
// No settings.local.json in backup, that's fine
console.log('No Claude Code settings found in backup to restore');
}
console.log('User content restoration complete');
}
catch (error) {
console.error('Failed to restore user content from backup:', error);
throw new Error('Failed to restore user content from backup. Please manually recover from backup if needed.');
}
}
async updateCommands() {
// List of default command files to update (exclude task command folders)
const commandNames = [
'spec-create',
'spec-execute',
'spec-status',
'spec-list',
'spec-steering-setup',
'bug-create',
'bug-analyze',
'bug-fix',
'bug-verify',
'bug-status'
];
// Only delete known default command files (preserve custom files)
for (const commandName of commandNames) {
const commandPath = (0, path_1.join)(this.commandsDir, `${commandName}.md`);
try {
await fs_1.promises.unlink(commandPath);
}
catch {
// File might not exist, which is fine
}
}
// Copy new command files
for (const commandName of commandNames) {
const sourceFile = (0, path_1.join)(this.markdownCommandsDir, `${commandName}.md`);
const destFile = (0, path_1.join)(this.commandsDir, `${commandName}.md`);
try {
const commandContent = await fs_1.promises.readFile(sourceFile, 'utf-8');
await fs_1.promises.writeFile(destFile, commandContent, 'utf-8');
}
catch (error) {
console.error(`Failed to update command ${commandName}:`, error);
throw error;
}
}
}
async updateTemplates() {
const templateNames = [
'requirements-template.md',
'design-template.md',
'tasks-template.md',
'product-template.md',
'tech-template.md',
'structure-template.md',
'bug-report-template.md',
'bug-analysis-template.md',
'bug-verification-template.md'
];
// Only delete known default template files (preserve custom templates)
for (const templateName of templateNames) {
const templatePath = (0, path_1.join)(this.templatesDir, templateName);
try {
await fs_1.promises.unlink(templatePath);
}
catch {
// File might not exist, which is fine
}
}
// Ensure templates directory exists
await fs_1.promises.mkdir(this.templatesDir, { recursive: true });
// Copy new template files
for (const templateName of templateNames) {
const sourceFile = (0, path_1.join)(this.markdownTemplatesDir, templateName);
const destFile = (0, path_1.join)(this.templatesDir, templateName);
try {
const templateContent = await fs_1.promises.readFile(sourceFile, 'utf-8');
await fs_1.promises.writeFile(destFile, templateContent, 'utf-8');
}
catch (error) {
console.error(`Failed to update template ${templateName}:`, error);
throw error;
}
}
}
async updateAgents() {
// Agents are now mandatory - always update them
// List of available agent files
const agentFiles = [
'spec-requirements-validator.md',
'spec-design-validator.md',
'spec-task-validator.md',
'spec-task-executor.md',
];
// Only delete known default agent files (preserve custom agents)
for (const agentFile of agentFiles) {
const agentPath = (0, path_1.join)(this.agentsDir, agentFile);
try {
await fs_1.promises.unlink(agentPath);
}
catch {
// File might not exist, which is fine
}
}
// Ensure agents directory exists
await fs_1.promises.mkdir(this.agentsDir, { recursive: true });
// Copy new agent files
for (const agentFile of agentFiles) {
const sourceFile = (0, path_1.join)(this.markdownAgentsDir, agentFile);
const destFile = (0, path_1.join)(this.agentsDir, agentFile);
try {
const agentContent = await fs_1.promises.readFile(sourceFile, 'utf-8');
await fs_1.promises.writeFile(destFile, agentContent, 'utf-8');
}
catch (error) {
console.error(`Failed to update agent ${agentFile}:`, error);
throw error;
}
}
}
async regenerateTaskCommands() {
console.log('Scanning for existing specs...');
// Find all existing specs
let specDirs = [];
try {
const specsEntries = await fs_1.promises.readdir(this.specsDir, { withFileTypes: true });
specDirs = specsEntries
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
if (specDirs.length === 0) {
console.log('No specs found to regenerate task commands for.');
return;
}
console.log(`Found ${specDirs.length} spec(s): ${specDirs.join(', ')}`);
}
catch {
console.log('No specs directory found, skipping task command regeneration.');
// Specs directory might not exist
return;
}
// For each spec, regenerate task commands if tasks.md exists
for (const specName of specDirs) {
const specDir = (0, path_1.join)(this.specsDir, specName);
const tasksFile = (0, path_1.join)(specDir, 'tasks.md');
const commandsSpecDir = (0, path_1.join)(this.commandsDir, specName);
try {
// Check if tasks.md exists
await fs_1.promises.access(tasksFile);
// Read tasks.md
const tasksContent = await fs_1.promises.readFile(tasksFile, 'utf8');
// Parse tasks and generate commands
const tasks = (0, task_generator_1.parseTasksFromMarkdown)(tasksContent);
if (tasks.length === 0) {
console.log(` ${specName}: No tasks found in tasks.md (check format: "- [ ] 1. Task description"), skipping`);
continue;
}
console.log(` ${specName}: Regenerating ${tasks.length} task commands...`);
// Delete existing task commands for this spec
try {
await fs_1.promises.rm(commandsSpecDir, { recursive: true });
}
catch {
// Directory might not exist
}
// Create spec commands directory
await fs_1.promises.mkdir(commandsSpecDir, { recursive: true });
// Generate commands
for (const task of tasks) {
await (0, task_generator_1.generateTaskCommand)(commandsSpecDir, specName, task);
}
console.log(` ${specName}: Generated commands for tasks: ${tasks.map(t => t.id).join(', ')}`);
}
catch {
console.log(` ${specName}: No tasks.md found, skipping`);
// tasks.md doesn't exist for this spec, skip
continue;
}
}
console.log('Task command regeneration complete!');
}
/**
* Update with fresh install approach: backup -> delete -> fresh install -> restore user content
*/
async updateWithFreshInstall() {
console.log('Starting fresh installation update...');
// 1. Create backup first
const backupDir = await this.createBackup();
try {
// 2. Delete existing .claude directory
await this.deleteClaudeDirectory();
// 3. Run fresh installation (agents are now mandatory)
const { SpecWorkflowSetup } = await Promise.resolve().then(() => __importStar(require('./setup')));
const setup = new SpecWorkflowSetup(this.projectRoot);
await setup.runSetup();
console.log('Fresh installation complete');
// 4. Restore user content (specs and task commands)
await this.restoreUserContent(backupDir);
// 5. Auto-generate task commands for restored specs
await this.regenerateTaskCommands();
console.log('Fresh installation update complete!');
}
catch (error) {
console.error('Fresh installation update failed:', error);
console.log(`Your data is safely backed up in: ${backupDir}`);
console.log('You can manually restore your specs and task commands from the backup if needed.');
throw error;
}
}
}
exports.SpecWorkflowUpdater = SpecWorkflowUpdater;
//# sourceMappingURL=update.js.map