prprompts-flutter-generator
Version:
AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp
716 lines (593 loc) • 23.4 kB
JavaScript
/**
* Post-install script for PRPROMPTS Flutter Generator
* Automatically configures AI assistant commands after npm install
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');
// ANSI color codes
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
blue: '\x1b[34m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function commandExists(command) {
try {
if (os.platform() === 'win32') {
execSync(`where ${command}`, { stdio: 'ignore' });
} else {
execSync(`which ${command}`, { stdio: 'ignore' });
}
return true;
} catch {
return false;
}
}
/**
* Detect Windows shell environment (PowerShell, CMD, Git Bash, WSL)
* @returns {string} 'powershell', 'cmd', 'bash', or 'wsl'
*/
function detectWindowsShell() {
if (os.platform() !== 'win32') {
return 'unix';
}
// Check if running in WSL
try {
const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
if (release.includes('microsoft') || release.includes('wsl')) {
return 'wsl';
}
} catch {
// Not WSL
}
// Check for Git Bash by looking at common env vars
if (process.env.MSYSTEM || process.env.BASH) {
return 'bash';
}
// Check if PowerShell is the parent process
if (process.env.PSModulePath || process.env.WT_SESSION) {
return 'powershell';
}
// Default to CMD
return 'cmd';
}
function getConfigPath(ai) {
const home = os.homedir();
const platform = os.platform();
if (platform === 'win32') {
// Windows: %USERPROFILE%\.config\<ai>
return path.join(home, '.config', ai);
} else {
// macOS/Linux: ~/.config/<ai>
return path.join(home, '.config', ai);
}
}
function ensureDirectory(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function copyPrompts(ai, configPath) {
const promptsDir = path.join(configPath, 'prompts');
ensureDirectory(promptsDir);
// Use the correct AI directory (`.claude`, `.qwen`, or `.gemini`)
const sourceDir = path.join(__dirname, '..', `.${ai}`, 'prompts');
if (!fs.existsSync(sourceDir)) {
log(` ⚠️ Warning: Source prompts directory not found at ${sourceDir}`, 'yellow');
return false;
}
const files = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
files.forEach(file => {
const source = path.join(sourceDir, file);
const dest = path.join(promptsDir, file);
fs.copyFileSync(source, dest);
});
log(` ✓ Copied ${files.length} prompt files`, 'green');
return true;
}
function copyConfig(ai, configPath) {
const configFile = path.join(configPath, 'config.yml');
// Use the correct AI directory (`.claude`, `.qwen`, or `.gemini`)
const sourceConfig = path.join(__dirname, '..', `.${ai}`, 'config.yml');
if (!fs.existsSync(sourceConfig)) {
log(` ⚠️ Warning: Source config not found at ${sourceConfig}`, 'yellow');
return false;
}
// Don't overwrite existing config
if (fs.existsSync(configFile)) {
log(` ℹ️ Config already exists, skipping`, 'cyan');
return true;
}
fs.copyFileSync(sourceConfig, configFile);
log(` ✓ Copied config file`, 'green');
return true;
}
function copyCommands(ai, configPath) {
const commandsDir = path.join(configPath, 'commands');
ensureDirectory(commandsDir);
// Use the correct AI directory (`.claude`, `.qwen`, or `.gemini`)
const sourceDir = path.join(__dirname, '..', `.${ai}`, 'commands');
if (!fs.existsSync(sourceDir)) {
log(` ⚠️ Warning: Source commands directory not found at ${sourceDir}`, 'yellow');
return false;
}
// Copy all subdirectories (prd/, planning/, prprompts/, automation/, refactoring/)
const subdirs = fs.readdirSync(sourceDir).filter(item => {
const fullPath = path.join(sourceDir, item);
return fs.statSync(fullPath).isDirectory();
});
let totalFiles = 0;
subdirs.forEach(subdir => {
const sourceSubdir = path.join(sourceDir, subdir);
const destSubdir = path.join(commandsDir, subdir);
ensureDirectory(destSubdir);
// Copy both .md and .toml files (Qwen/Gemini need .toml for slash commands)
const files = fs.readdirSync(sourceSubdir).filter(f => f.endsWith('.md') || f.endsWith('.toml'));
files.forEach(file => {
const source = path.join(sourceSubdir, file);
const dest = path.join(destSubdir, file);
fs.copyFileSync(source, dest);
totalFiles++;
});
});
log(` ✓ Copied ${totalFiles} command files in ${subdirs.length} categories`, 'green');
return true;
}
function copyExtensionManifest(ai, configPath) {
// Claude doesn't use extension.json for command discovery
if (ai === 'claude') {
return true;
}
const destFile = path.join(configPath, 'extension.json');
const sourceFile = path.join(__dirname, '..', `${ai}-extension.json`);
if (!fs.existsSync(sourceFile)) {
log(` ⚠️ Warning: Extension manifest not found at ${sourceFile}`, 'yellow');
return false;
}
// Always overwrite extension.json to ensure latest version with all features
fs.copyFileSync(sourceFile, destFile);
// Read and display version
try {
const manifest = JSON.parse(fs.readFileSync(destFile, 'utf8'));
log(` ✓ Copied extension manifest (v${manifest.version})`, 'green');
} catch {
log(` ✓ Copied extension manifest`, 'green');
}
return true;
}
function copyDirectoryRecursive(source, dest) {
// Create destination directory
ensureDirectory(dest);
// Read all items in source directory
const items = fs.readdirSync(source);
items.forEach(item => {
const sourcePath = path.join(source, item);
const destPath = path.join(dest, item);
if (fs.statSync(sourcePath).isDirectory()) {
// Recursively copy directories
copyDirectoryRecursive(sourcePath, destPath);
} else {
// Copy file
fs.copyFileSync(sourcePath, destPath);
}
});
}
function installClaudePlugin(configPath) {
log('\nInstalling Claude Code Plugin...', 'blue');
try {
const sourcePluginDir = path.join(__dirname, '..', '.claude-plugin');
const destPluginDir = path.join(configPath, '.claude-plugin');
if (!fs.existsSync(sourcePluginDir)) {
log(' ⚠️ .claude-plugin directory not found, skipping', 'yellow');
return false;
}
// Copy entire .claude-plugin directory
copyDirectoryRecursive(sourcePluginDir, destPluginDir);
// Verify plugin.json was copied
const pluginJsonPath = path.join(destPluginDir, 'plugin.json');
if (fs.existsSync(pluginJsonPath)) {
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
log(` ✓ Installed Claude Code Plugin v${pluginJson.version}`, 'green');
return true;
}
return false;
} catch (error) {
log(` ⚠️ Failed to install Claude plugin: ${error.message}`, 'yellow');
return false;
}
}
function installClaudeHooks(configPath) {
log('\nInstalling Claude Code Hooks...', 'blue');
try {
const sourceHooksDir = path.join(__dirname, '..', 'hooks');
const destHooksDir = path.join(configPath, 'hooks');
if (!fs.existsSync(sourceHooksDir)) {
log(' ⚠️ hooks directory not found, skipping', 'yellow');
return false;
}
// Copy entire hooks directory
copyDirectoryRecursive(sourceHooksDir, destHooksDir);
// Verify hooks.json was copied
const hooksJsonPath = path.join(destHooksDir, 'hooks.json');
if (fs.existsSync(hooksJsonPath)) {
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
const hookCount = Object.keys(hooksJson.hooks || {}).length;
log(` ✓ Installed ${hookCount} hook event types`, 'green');
// Make scripts executable on Unix systems
if (os.platform() !== 'win32') {
const scriptsDir = path.join(__dirname, '..', 'scripts');
const scriptFiles = ['check-flutter-sdk.sh'];
scriptFiles.forEach(script => {
const scriptPath = path.join(scriptsDir, script);
if (fs.existsSync(scriptPath)) {
try {
fs.chmodSync(scriptPath, '755');
} catch {
// Ignore chmod errors
}
}
});
}
return true;
}
return false;
} catch (error) {
log(` ⚠️ Failed to install Claude hooks: ${error.message}`, 'yellow');
return false;
}
}
function installQwenSettings(configPath) {
log('\nInstalling Qwen Code Settings...', 'blue');
try {
const sourceSettingsFile = path.join(__dirname, '..', '.qwen', 'settings.json');
const destSettingsFile = path.join(configPath, 'settings.json');
if (!fs.existsSync(sourceSettingsFile)) {
log(' ⚠️ .qwen/settings.json not found, skipping', 'yellow');
return false;
}
// Copy settings.json
fs.copyFileSync(sourceSettingsFile, destSettingsFile);
// Read and display version
try {
const settings = JSON.parse(fs.readFileSync(destSettingsFile, 'utf8'));
log(` ✓ Installed Qwen settings v${settings.extension.version}`, 'green');
// Display MCP status
if (settings.mcp && settings.mcp.enabled) {
log(' ✓ MCP server configuration included', 'green');
} else {
log(' ℹ️ MCP server disabled (can be enabled in settings.json)', 'cyan');
}
} catch {
log(' ✓ Installed Qwen settings', 'green');
}
return true;
} catch (error) {
log(` ⚠️ Failed to install Qwen settings: ${error.message}`, 'yellow');
return false;
}
}
function installForAI(ai, aiName) {
log(`\nConfiguring ${aiName}...`, 'blue');
const configPath = getConfigPath(ai);
ensureDirectory(configPath);
const promptsSuccess = copyPrompts(ai, configPath);
const configSuccess = copyConfig(ai, configPath);
const commandsSuccess = copyCommands(ai, configPath);
const manifestSuccess = copyExtensionManifest(ai, configPath);
// AI-specific installations
let extraSuccess = true;
if (ai === 'claude') {
// Install Claude Code plugin and hooks
const pluginSuccess = installClaudePlugin(configPath);
const hooksSuccess = installClaudeHooks(configPath);
extraSuccess = pluginSuccess && hooksSuccess;
} else if (ai === 'qwen') {
// Install Qwen settings
extraSuccess = installQwenSettings(configPath);
}
if (promptsSuccess && configSuccess && commandsSuccess && manifestSuccess && extraSuccess) {
log(`✓ ${aiName} configured successfully!`, 'green');
return true;
}
return false;
}
function installQwenSkills() {
if (!commandExists('qwen')) {
return false;
}
log('\nInstalling Qwen Code Skills...', 'blue');
try {
const bashScriptPath = path.join(__dirname, 'install-qwen-skills.sh');
const psScriptPath = path.join(__dirname, 'install-qwen-skills.ps1');
const batScriptPath = path.join(__dirname, 'install-qwen-skills.bat');
// Run installation script based on platform and environment
if (os.platform() === 'win32') {
const shell = detectWindowsShell();
if ((shell === 'bash' || shell === 'wsl') && fs.existsSync(bashScriptPath)) {
// Git Bash or WSL - use bash script
execSync(`bash "${bashScriptPath}"`, { stdio: 'inherit' });
} else if (shell === 'powershell' && fs.existsSync(psScriptPath)) {
// PowerShell - use .ps1 script
execSync(`powershell -ExecutionPolicy Bypass -File "${psScriptPath}"`, { stdio: 'inherit' });
} else if (fs.existsSync(batScriptPath)) {
// CMD - use .bat script
execSync(`"${batScriptPath}"`, { stdio: 'inherit' });
} else if (fs.existsSync(psScriptPath)) {
// Fallback to PowerShell
execSync(`powershell -ExecutionPolicy Bypass -File "${psScriptPath}"`, { stdio: 'inherit' });
} else {
log(' ⚠️ No Windows installer found, skipping', 'yellow');
return false;
}
} else {
// macOS/Linux - use bash script
if (!fs.existsSync(bashScriptPath)) {
log(' ⚠️ Qwen skills installer not found, skipping', 'yellow');
return false;
}
execSync(`bash "${bashScriptPath}"`, { stdio: 'inherit' });
}
log('\n✓ Qwen Code Skills installed successfully!', 'green');
return true;
} catch (error) {
log(` ⚠️ Failed to install Qwen skills: ${error.message}`, 'yellow');
return false;
}
}
function installGeminiSkills() {
if (!commandExists('gemini')) {
return false;
}
log('\nInstalling Gemini CLI Skills...', 'blue');
try {
const bashScriptPath = path.join(__dirname, 'install-gemini-skills.sh');
const psScriptPath = path.join(__dirname, 'install-gemini-skills.ps1');
const batScriptPath = path.join(__dirname, 'install-gemini-skills.bat');
// Run installation script based on platform and environment
if (os.platform() === 'win32') {
const shell = detectWindowsShell();
if ((shell === 'bash' || shell === 'wsl') && fs.existsSync(bashScriptPath)) {
// Git Bash or WSL - use bash script
execSync(`bash "${bashScriptPath}"`, { stdio: 'inherit' });
} else if (shell === 'powershell' && fs.existsSync(psScriptPath)) {
// PowerShell - use .ps1 script
execSync(`powershell -ExecutionPolicy Bypass -File "${psScriptPath}"`, { stdio: 'inherit' });
} else if (fs.existsSync(batScriptPath)) {
// CMD - use .bat script
execSync(`"${batScriptPath}"`, { stdio: 'inherit' });
} else if (fs.existsSync(psScriptPath)) {
// Fallback to PowerShell
execSync(`powershell -ExecutionPolicy Bypass -File "${psScriptPath}"`, { stdio: 'inherit' });
} else {
log(' ⚠️ No Windows installer found, skipping', 'yellow');
return false;
}
} else {
// macOS/Linux - use bash script
if (!fs.existsSync(bashScriptPath)) {
log(' ⚠️ Gemini skills installer not found, skipping', 'yellow');
return false;
}
execSync(`bash "${bashScriptPath}"`, { stdio: 'inherit' });
}
log('\n✓ Gemini CLI Skills installed successfully!', 'green');
return true;
} catch (error) {
log(` ⚠️ Failed to install Gemini skills: ${error.message}`, 'yellow');
return false;
}
}
function generateTomlCommands() {
log('\nGenerating TOML command files...', 'blue');
try {
// Generate Qwen TOML commands
const qwenScript = path.join(__dirname, 'generate-qwen-command-toml.js');
if (fs.existsSync(qwenScript)) {
execSync(`node "${qwenScript}"`, { stdio: 'pipe' });
log(' ✓ Qwen TOML commands generated', 'green');
}
// Generate Gemini TOML commands
const geminiScript = path.join(__dirname, 'generate-gemini-command-toml.js');
if (fs.existsSync(geminiScript)) {
execSync(`node "${geminiScript}"`, { stdio: 'pipe' });
log(' ✓ Gemini TOML commands generated', 'green');
}
return true;
} catch (error) {
log(` ⚠️ Warning: Failed to generate TOML files: ${error.message}`, 'yellow');
return false;
}
}
function getPackageVersion() {
try {
const packagePath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
return packageJson.version;
} catch (error) {
return '4.4.1'; // fallback
}
}
function saveVersionInfo(installedAIs) {
const home = os.homedir();
const prpromptsDir = path.join(home, '.prprompts');
const versionFile = path.join(prpromptsDir, 'version.json');
const currentVersion = getPackageVersion();
const versionInfo = {
version: currentVersion,
updated_at: new Date().toISOString(),
installed_ais: installedAIs
};
// Save to central version file
fs.writeFileSync(versionFile, JSON.stringify(versionInfo, null, 2));
// Save to each AI config directory
installedAIs.forEach(ai => {
const aiConfigPath = getConfigPath(ai);
if (fs.existsSync(aiConfigPath)) {
const aiVersionFile = path.join(aiConfigPath, '.prprompts-version.json');
const aiVersionInfo = {
prprompts_version: currentVersion,
updated_at: new Date().toISOString(),
ai: ai
};
try {
fs.writeFileSync(aiVersionFile, JSON.stringify(aiVersionInfo, null, 2));
} catch (error) {
// Silently ignore version file write errors
}
}
});
log(`✓ Version info saved: v${currentVersion}`, 'green');
}
function createPRPROMPTSConfig() {
const home = os.homedir();
const prpromptsDir = path.join(home, '.prprompts');
const configFile = path.join(prpromptsDir, 'config.json');
ensureDirectory(prpromptsDir);
const currentVersion = getPackageVersion();
const config = {
version: currentVersion,
default_ai: 'claude',
ais: {
claude: { enabled: false, config_path: getConfigPath('claude') },
qwen: { enabled: false, config_path: getConfigPath('qwen') },
gemini: { enabled: false, config_path: getConfigPath('gemini') }
},
features: {
auto_update: true,
telemetry: false,
verbose: true,
qwen_skills: false,
gemini_skills: false
}
};
// Detect installed AIs
if (commandExists('claude')) config.ais.claude.enabled = true;
if (commandExists('qwen')) config.ais.qwen.enabled = true;
if (commandExists('gemini')) config.ais.gemini.enabled = true;
// Set default to first available AI
if (config.ais.qwen.enabled) config.default_ai = 'qwen';
if (config.ais.gemini.enabled) config.default_ai = 'gemini';
if (config.ais.claude.enabled) config.default_ai = 'claude';
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
log(`\n✓ Created unified config at ${configFile}`, 'green');
}
function main() {
log('\n========================================================', 'cyan');
log(' PRPROMPTS Flutter Generator - Post-Install Setup', 'cyan');
log('========================================================\n', 'cyan');
// Detect installed AI assistants
const ais = [
{ command: 'claude', name: 'Claude Code', key: 'claude' },
{ command: 'qwen', name: 'Qwen Code', key: 'qwen' },
{ command: 'gemini', name: 'Gemini CLI', key: 'gemini' }
];
const installedAIs = ais.filter(ai => commandExists(ai.command));
if (installedAIs.length === 0) {
log('⚠️ No AI assistants detected!', 'yellow');
log('\nPlease install at least one of the following:', 'yellow');
log(' • Claude Code: npm install -g @anthropic-ai/claude-code', 'cyan');
log(' • Qwen Code: npm install -g @qwen/qwen-code', 'cyan');
log(' • Gemini CLI: npm install -g @google/gemini-cli', 'cyan');
log('\nThen run: npm run postinstall', 'cyan');
return;
}
log('Detected AI assistants:', 'blue');
installedAIs.forEach(ai => {
try {
const version = execSync(`${ai.command} --version`, { encoding: 'utf-8' }).trim();
log(` ✓ ${ai.name}: ${version}`, 'green');
} catch {
log(` ✓ ${ai.name}`, 'green');
}
});
// Generate TOML command files (required for Qwen and Gemini)
generateTomlCommands();
// Install for each detected AI
let successCount = 0;
installedAIs.forEach(ai => {
if (installForAI(ai.key, ai.name)) {
successCount++;
}
});
// Install Qwen Code Skills (if Qwen is detected)
const qwenSkillsInstalled = installQwenSkills();
// Install Gemini CLI Skills (if Gemini is detected)
const geminiSkillsInstalled = installGeminiSkills();
// Create unified config
createPRPROMPTSConfig();
// Update config with skills status
if (qwenSkillsInstalled || geminiSkillsInstalled) {
const home = os.homedir();
const configFile = path.join(home, '.prprompts', 'config.json');
try {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
if (qwenSkillsInstalled) config.features.qwen_skills = true;
if (geminiSkillsInstalled) config.features.gemini_skills = true;
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
} catch (error) {
// Ignore config update errors
}
}
// Save version information
const installedAIsList = installedAIs.map(ai => ai.key);
saveVersionInfo(installedAIsList);
// Summary
log('\n========================================================', 'cyan');
log(' Installation Complete!', 'green');
log('========================================================\n', 'cyan');
log(`✓ Configured ${successCount} AI assistant(s)`, 'green');
if (qwenSkillsInstalled) {
log('✓ Installed Qwen Code Skills (8 slash commands)', 'green');
}
if (geminiSkillsInstalled) {
log('✓ Installed Gemini CLI Skills (8 slash commands)', 'green');
}
log('\nAvailable commands:', 'blue');
installedAIs.forEach(ai => {
log(` ${ai.command} create-prd - Create PRD interactively`, 'cyan');
log(` ${ai.command} gen-prprompts - Generate all 32 files`, 'cyan');
});
if (qwenSkillsInstalled) {
log('\n Qwen Code Skills (slash commands):', 'blue');
log(' /skills/automation/flutter-bootstrapper', 'cyan');
log(' /skills/automation/code-reviewer', 'cyan');
log(' /skills/automation/qa-auditor', 'cyan');
log(' + 5 more automation skills...', 'cyan');
}
if (geminiSkillsInstalled) {
log('\n Gemini CLI Skills (slash commands with colon separator):', 'blue');
log(' /skills:automation:flutter-bootstrapper', 'cyan');
log(' /skills:automation:code-reviewer', 'cyan');
log(' /skills:automation:qa-auditor', 'cyan');
log(' + 5 more automation skills...', 'cyan');
}
log('\nOr use the unified CLI:', 'blue');
log(' prprompts create - Create PRD', 'cyan');
log(' prprompts generate - Generate all 32 files', 'cyan');
log(' prprompts doctor - Check installation', 'cyan');
log('\nQuick Start:', 'blue');
log(' 1. cd your-flutter-project', 'cyan');
log(' 2. prprompts create', 'cyan');
log(' 3. prprompts generate', 'cyan');
log(' 4. Start coding!', 'cyan');
log('\nDocumentation:', 'blue');
log(' https://github.com/Kandil7/prprompts-flutter-generator\n', 'cyan');
}
// Run installation
try {
main();
} catch (error) {
log('\n❌ Installation failed:', 'red');
log(error.message, 'red');
log('\nPlease report this issue:', 'yellow');
log(' https://github.com/Kandil7/prprompts-flutter-generator/issues\n', 'cyan');
process.exit(1);
}