create-saltic
Version:
Spec-Driven Development (SDD) framework for constitutional software development - inject into any project
296 lines (254 loc) • 10.1 kB
JavaScript
#!/usr/bin/env node
const { program } = require('commander');
const chalk = require('chalk').default;
const path = require('path');
const fs = require('fs-extra');
const { execSync } = require('child_process');
program
.name('create-saltic')
.description('Inject Saltic SDD Framework into any project')
.version('0.0.1');
// Get the package root directory
const packageRoot = path.join(__dirname, '..', '..');
// Helper function to copy framework files to target project
function injectFramework(targetDir = process.cwd()) {
console.log(chalk.blue('🚀 Injecting Saltic SDD Framework into project...'));
const claudeDir = path.join(targetDir, '.claude');
const commandsDir = path.join(claudeDir, 'commands');
// Root directories (in project root)
const memoryDir = path.join(targetDir, 'memory');
const templatesDir = path.join(targetDir, 'templates');
const scriptsDir = path.join(targetDir, 'scripts');
// Create directory structure
const dirs = [claudeDir, commandsDir, memoryDir, templatesDir, scriptsDir];
dirs.forEach(dir => {
fs.ensureDirSync(dir);
console.log(chalk.green('✓'), `Created ${path.relative(targetDir, dir)}/`);
});
// Copy framework files
const copyTasks = [
{
source: path.join(packageRoot, 'src/memory'),
dest: memoryDir,
name: 'memory files'
},
{
source: path.join(packageRoot, 'src/templates'),
dest: templatesDir,
name: 'template files'
},
{
source: path.join(packageRoot, 'src/commands'),
dest: commandsDir,
name: 'command files'
},
{
source: path.join(packageRoot, 'src/scripts'),
dest: scriptsDir,
name: 'script files'
}
];
copyTasks.forEach(task => {
try {
if (fs.existsSync(task.source)) {
if (fs.statSync(task.source).isDirectory()) {
fs.copySync(task.source, task.dest, { overwrite: false });
} else {
fs.copySync(task.source, path.join(task.dest, path.basename(task.source)), { overwrite: false });
}
console.log(chalk.green('✓'), `Copied ${task.name}`);
}
} catch (error) {
console.warn(chalk.yellow('⚠'), `Failed to copy ${task.name}:`, error.message);
}
});
// Create specs directory in project root
const specsDir = path.join(targetDir, 'specs');
fs.ensureDirSync(specsDir);
console.log(chalk.green('✓'), `Created specs/ directory`);
// Create .gitignore for .claude if it doesn't exist
const gitignorePath = path.join(targetDir, '.gitignore');
const gitignoreContent = '# Saltic SDD Framework\n';
if (fs.existsSync(gitignorePath)) {
const existingContent = fs.readFileSync(gitignorePath, 'utf8');
if (!existingContent.includes('.claude/')) {
fs.appendFileSync(gitignorePath, '\n' + gitignoreContent);
console.log(chalk.green('✓'), 'Updated .gitignore');
}
} else {
fs.writeFileSync(gitignorePath, gitignoreContent);
console.log(chalk.green('✓'), 'Created .gitignore');
}
console.log(chalk.green('🎉 Saltic SDD Framework successfully injected!'));
}
// Main injection command
program
.command('inject')
.description('Inject Saltic SDD Framework into current project')
.option('-f, --force', 'Force overwrite existing files')
.option('-d, --dir <path>', 'Target directory (default: current directory)')
.action((options) => {
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
// Check if already injected
const claudeDir = path.join(targetDir, '.claude');
if (fs.existsSync(claudeDir) && !options.force) {
console.log(chalk.yellow('⚠ Saltic SDD Framework is already injected in this project.'));
console.log(chalk.white('Use --force to overwrite existing files.'));
process.exit(1);
}
injectFramework(targetDir);
});
// Quick init command (default behavior)
program
.command('init')
.description('Initialize Saltic SDD Framework in current project (alias for inject)')
.option('-f, --force', 'Force overwrite existing files')
.option('-d, --dir <path>', 'Target directory (default: current directory)')
.action((options) => {
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
injectFramework(targetDir);
});
// Show framework status
program
.command('status')
.description('Show Saltic SDD Framework status in current project')
.action(() => {
const claudeDir = path.join(process.cwd(), '.claude');
const memoryDir = path.join(process.cwd(), 'memory');
const templatesDir = path.join(process.cwd(), 'templates');
const scriptsDir = path.join(process.cwd(), 'scripts');
const frameworkComponents = [
{ path: claudeDir, name: '.claude/ directory' },
{ path: memoryDir, name: 'memory/ directory' },
{ path: templatesDir, name: 'templates/ directory' },
{ path: scriptsDir, name: 'scripts/ directory' }
];
const presentComponents = frameworkComponents.filter(comp => fs.existsSync(comp.path));
if (presentComponents.length === 0) {
console.log(chalk.red('❌ Saltic SDD Framework is not injected in this project.'));
console.log(chalk.white('Run: create-saltic inject'));
process.exit(1);
}
console.log(chalk.blue('📊 Saltic SDD Framework Status:'));
console.log(chalk.green('✓'), `Framework partially injected (${presentComponents.length}/${frameworkComponents.length} components)`);
presentComponents.forEach(comp => {
console.log(chalk.green(' ✓'), comp.name);
});
const missingComponents = frameworkComponents.filter(comp => !fs.existsSync(comp.path));
missingComponents.forEach(comp => {
console.log(chalk.red(' ✗'), comp.name);
});
// Check git branch
try {
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf8',
cwd: process.cwd()
}).trim();
console.log(chalk.white(' Current Branch:'), chalk.green(currentBranch));
if (/^\d{3}-.+/.test(currentBranch)) {
console.log(chalk.green(' ✓ Branch follows naming convention'));
} else {
console.log(chalk.yellow(' ⚠ Branch does not follow naming convention (###-feature-name)'));
}
} catch (error) {
console.log(chalk.yellow(' ⚠ Not a git repository or git not available'));
}
// Check for feature directory
try {
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf8',
cwd: process.cwd()
}).trim();
const featureDir = path.join(process.cwd(), 'specs', currentBranch);
if (fs.existsSync(featureDir)) {
console.log(chalk.green(' ✓ Feature directory exists'));
} else {
console.log(chalk.yellow(' ⚠ Feature directory does not exist'));
}
} catch (error) {
console.log(chalk.yellow(' ⚠ Could not check feature directory'));
}
});
// Remove framework from project
program
.command('remove')
.description('Remove Saltic SDD Framework from current project')
.option('-f, --force', 'Force removal without confirmation')
.action((options) => {
const claudeDir = path.join(process.cwd(), '.claude');
if (!fs.existsSync(claudeDir)) {
console.log(chalk.yellow('⚠ Saltic SDD Framework is not injected in this project.'));
process.exit(0);
}
if (!options.force) {
const inquirer = require('inquirer');
inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to remove Saltic SDD Framework from this project?',
default: false
}
]).then((answers) => {
if (answers.confirm) {
removeFramework();
} else {
console.log(chalk.blue('Operation cancelled.'));
}
});
} else {
removeFramework();
}
});
function removeFramework() {
console.log(chalk.blue('🗑️ Removing Saltic SDD Framework...'));
const claudeDir = path.join(process.cwd(), '.claude');
const memoryDir = path.join(process.cwd(), 'memory');
const templatesDir = path.join(process.cwd(), 'templates');
const scriptsDir = path.join(process.cwd(), 'scripts');
const specsDir = path.join(process.cwd(), 'specs');
// Remove framework directories and files
const removeTasks = [
{ path: claudeDir, name: '.claude/ directory' },
{ path: memoryDir, name: 'memory/ directory' },
{ path: templatesDir, name: 'templates/ directory' },
{ path: scriptsDir, name: 'scripts/ directory' }
];
removeTasks.forEach(task => {
try {
if (fs.existsSync(task.path)) {
fs.removeSync(task.path);
console.log(chalk.green('✓'), `Removed ${task.name}`);
}
} catch (error) {
console.warn(chalk.yellow('⚠'), `Failed to remove ${task.name}:`, error.message);
}
});
// Remove specs directory only if it's empty
try {
if (fs.existsSync(specsDir) && fs.readdirSync(specsDir).length === 0) {
fs.removeSync(specsDir);
console.log(chalk.green('✓'), 'Removed empty specs/ directory');
}
} catch (error) {
console.warn(chalk.yellow('⚠'), 'Failed to remove specs/:', error.message);
}
// Remove from .gitignore
const gitignorePath = path.join(process.cwd(), '.gitignore');
if (fs.existsSync(gitignorePath)) {
try {
let gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
gitignoreContent = gitignoreContent.replace(/\n# Saltic SDD Framework\n/g, '');
fs.writeFileSync(gitignorePath, gitignoreContent);
console.log(chalk.green('✓'), 'Removed from .gitignore');
} catch (error) {
console.warn(chalk.yellow('⚠'), 'Failed to update .gitignore:', error.message);
}
}
console.log(chalk.green('🎉 Saltic SDD Framework successfully removed!'));
}
// Default command is inject
if (process.argv.length === 2) {
program.commands.find(cmd => cmd.name() === 'inject').action();
}
program.parse();