supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
759 lines โข 30.9 kB
JavaScript
;
/**
* Interactive Configuration Updates System
* Phase 4, Checkpoint D2 - Interactive configuration prompt engine and user choice handling
*/
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.ConfigurationPromptEngine = void 0;
const logger_1 = require("../core/utils/logger");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const readline = __importStar(require("readline"));
class ConfigurationPromptEngine {
constructor(options = {}) {
this.sessions = new Map();
this.options = {
interactiveMode: true,
autoAcceptLowRisk: false,
requireConfirmationForHighRisk: true,
includePreview: true,
batchSimilarChanges: true,
minimumRiskThreshold: 'LOW',
timeoutSeconds: 300,
saveChoices: true,
...options
};
if (this.options.interactiveMode) {
this.initializeReadline();
}
}
/**
* Generate configuration prompts from schema changes and migration suggestions
*/
async generatePrompts(changes, impacts, suggestions) {
logger_1.Logger.info('๐ฏ Generating configuration prompts from schema analysis...');
const prompts = [];
// Group related changes for batch processing
const groupedChanges = this.options.batchSimilarChanges
? this.groupRelatedChanges(changes)
: changes.map(c => [c]);
// Generate prompts for schema changes
for (const changeGroup of groupedChanges) {
const schemaPrompt = this.generateSchemaChangePrompt(changeGroup, impacts);
if (schemaPrompt) {
prompts.push(schemaPrompt);
}
}
// Generate prompts for migration suggestions
for (const suggestion of suggestions) {
const migrationPrompt = this.generateMigrationPrompt(suggestion, changes, impacts);
if (migrationPrompt) {
prompts.push(migrationPrompt);
}
}
// Generate prompts for impact resolutions
for (const impact of impacts) {
if (impact.migrationNeeded) {
const impactPrompt = this.generateImpactResolutionPrompt(impact, changes);
if (impactPrompt) {
prompts.push(impactPrompt);
}
}
}
// Sort prompts by priority and dependencies
const sortedPrompts = this.sortPromptsByPriority(prompts);
logger_1.Logger.info(`๐ฏ Generated ${sortedPrompts.length} configuration prompts`);
return sortedPrompts;
}
/**
* Start an interactive configuration session
*/
async startConfigurationSession(changes, impacts, suggestions) {
const sessionId = `config-session-${Date.now()}`;
const prompts = await this.generatePrompts(changes, impacts, suggestions);
const session = {
id: sessionId,
startTime: new Date(),
prompts,
choices: [],
status: 'in_progress',
metadata: {
triggeredByChangeId: changes[0]?.id || 'unknown',
affectedFiles: this.extractAffectedFiles(impacts),
totalPrompts: prompts.length,
completedPrompts: 0,
estimatedTotalTime: prompts.reduce((sum, p) => sum + p.metadata.estimatedTime, 0)
}
};
this.sessions.set(sessionId, session);
logger_1.Logger.info(`๐ Started configuration session: ${sessionId} (${prompts.length} prompts)`);
if (this.options.interactiveMode) {
await this.runInteractiveSession(session);
}
else {
// In non-interactive mode, auto-complete with default choices
for (const prompt of prompts) {
const defaultOption = prompt.options.find(o => o.id === prompt.defaultOption) || prompt.options[0];
if (defaultOption) {
const choice = {
promptId: prompt.id,
optionId: defaultOption.id,
timestamp: new Date(),
confidence: 50
};
session.choices.push(choice);
}
}
session.status = 'completed';
session.endTime = new Date();
session.metadata.completedPrompts = session.choices.length;
}
return session;
}
/**
* Present a single prompt to the user and collect their choice
*/
async presentPrompt(prompt) {
logger_1.Logger.info(`\n๐ Configuration Prompt: ${prompt.title}`);
if (!this.options.interactiveMode) {
// Return default choice in non-interactive mode
const defaultOption = prompt.options.find(o => o.id === prompt.defaultOption) || prompt.options[0];
return {
promptId: prompt.id,
optionId: defaultOption.id,
timestamp: new Date(),
confidence: 50 // Low confidence for auto-choices
};
}
// Display prompt information
console.log(`\n${'='.repeat(80)}`);
console.log(`๐ ${prompt.title}`);
console.log(`${'='.repeat(80)}`);
console.log(`\n๐ Description:`);
console.log(` ${prompt.description}`);
if (prompt.context.affectedFiles.length > 0) {
console.log(`\n๐ Affected Files:`);
prompt.context.affectedFiles.forEach(file => {
console.log(` โข ${file}`);
});
}
console.log(`\nโ๏ธ Impact: ${prompt.metadata.riskLevel} risk, ~${prompt.metadata.estimatedTime}min`);
if (this.options.includePreview && prompt.context.schemaChanges.length > 0) {
console.log(`\n๐ Schema Changes:`);
prompt.context.schemaChanges.forEach(change => {
console.log(` โข ${change.description} (${change.severity})`);
});
}
console.log(`\n๐ง Available Options:`);
prompt.options.forEach((option, index) => {
const defaultMarker = option.id === prompt.defaultOption ? ' (default)' : '';
console.log(` ${index + 1}. ${option.label}${defaultMarker}`);
console.log(` ${option.description}`);
if (option.consequences.length > 0) {
console.log(` Consequences: ${option.consequences.join(', ')}`);
}
});
// Auto-accept low risk if configured
if (this.options.autoAcceptLowRisk && prompt.metadata.riskLevel === 'LOW') {
const acceptOption = prompt.options.find(o => o.type === 'accept');
if (acceptOption) {
console.log(`\nโ
Auto-accepting low-risk change: ${acceptOption.label}`);
return {
promptId: prompt.id,
optionId: acceptOption.id,
timestamp: new Date(),
confidence: 80
};
}
}
// Get user input
const choice = await this.getUserChoice(prompt);
return choice;
}
/**
* Validate a user choice against prompt constraints
*/
validateChoice(prompt, choice) {
const errors = [];
const warnings = [];
// Check if option exists
const selectedOption = prompt.options.find(o => o.id === choice.optionId);
if (!selectedOption) {
errors.push(`Invalid option: ${choice.optionId}`);
return { isValid: false, errors, warnings };
}
// Check prerequisites
for (const prereq of selectedOption.prerequisites) {
// In a real implementation, this would check if prerequisites are met
// For now, we'll just log them
logger_1.Logger.debug(`Prerequisite required: ${prereq}`);
}
// Check risk level confirmation for high-risk choices
if (this.options.requireConfirmationForHighRisk &&
selectedOption.riskLevel === 'HIGH' &&
choice.confidence < 70) {
warnings.push('High-risk option selected with low confidence');
}
// Validate custom parameters if provided
if (choice.customParameters && selectedOption.type === 'customize') {
const paramValidation = this.validateCustomParameters(selectedOption, choice.customParameters);
errors.push(...paramValidation.errors);
warnings.push(...paramValidation.warnings);
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Execute the actions associated with a user choice
*/
async executeChoice(prompt, choice, dryRun = false) {
const selectedOption = prompt.options.find(o => o.id === choice.optionId);
if (!selectedOption) {
return {
success: false,
executedActions: [],
errors: [`Option not found: ${choice.optionId}`],
rollbackActions: []
};
}
logger_1.Logger.info(`${dryRun ? '๐งช Dry run: ' : 'โก Executing: '}${selectedOption.label}`);
const executedActions = [];
const errors = [];
const rollbackActions = [];
for (const action of selectedOption.actions) {
try {
const result = await this.executeAction(action, choice.customParameters || {}, dryRun);
if (result.success) {
executedActions.push(action);
if (action.rollbackAction) {
rollbackActions.unshift(action.rollbackAction); // Add to front for reverse order
}
}
else {
errors.push(`Action failed: ${action.description} - ${result.error}`);
break; // Stop on first failure
}
}
catch (error) {
errors.push(`Action error: ${action.description} - ${error.message}`);
break;
}
}
return {
success: errors.length === 0,
executedActions,
errors,
rollbackActions
};
}
/**
* Get the current session status
*/
getSessionStatus(sessionId) {
return this.sessions.get(sessionId);
}
/**
* Get all active sessions
*/
getActiveSessions() {
return Array.from(this.sessions.values()).filter(s => s.status === 'in_progress');
}
/**
* Cleanup completed sessions
*/
cleanupSessions() {
const cutoffTime = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
for (const [sessionId, session] of this.sessions) {
if (session.status !== 'in_progress' && session.startTime < cutoffTime) {
this.sessions.delete(sessionId);
}
}
}
/**
* Private: Initialize readline interface
*/
initializeReadline() {
this.readline = readline.createInterface({
input: process.stdin,
output: process.stdout
});
}
/**
* Private: Group related schema changes for batch processing
*/
groupRelatedChanges(changes) {
const groups = new Map();
for (const change of changes) {
const groupKey = change.tableName || 'global';
if (!groups.has(groupKey)) {
groups.set(groupKey, []);
}
groups.get(groupKey).push(change);
}
return Array.from(groups.values());
}
/**
* Private: Generate prompt for schema changes
*/
generateSchemaChangePrompt(changes, impacts) {
if (changes.length === 0)
return null;
const change = changes[0];
const relatedImpacts = impacts.filter(i => i.changes.some(c => changes.some(ch => ch.id === `change-${c.field}`)));
const options = [];
// Accept option
if (change.impact !== 'DATA_LOSS') {
options.push({
id: 'accept',
label: 'Accept Changes',
description: 'Apply the schema changes and update configurations automatically',
type: 'accept',
consequences: [
'Configuration files will be updated',
'Seed data may need regeneration'
],
prerequisites: [],
actions: [
{
type: 'file_update',
description: 'Update configuration files',
targetPath: 'supa-seed.config.js',
parameters: { changes }
}
],
reversible: true,
riskLevel: change.severity === 'HIGH' ? 'HIGH' : 'MEDIUM'
});
}
// Customize option
options.push({
id: 'customize',
label: 'Customize Changes',
description: 'Manually review and customize how changes are applied',
type: 'customize',
consequences: [
'Manual configuration required',
'Extended setup time'
],
prerequisites: ['User input required'],
actions: [
{
type: 'user_input',
description: 'Collect customization preferences',
targetPath: '',
parameters: { prompt: 'customization' }
}
],
reversible: true,
riskLevel: 'LOW'
});
// Defer option
options.push({
id: 'defer',
label: 'Defer Changes',
description: 'Skip these changes for now (can be applied later)',
type: 'defer',
consequences: [
'Configuration remains unchanged',
'Manual intervention may be required later'
],
prerequisites: [],
actions: [],
reversible: true,
riskLevel: 'LOW'
});
return {
id: `schema-change-${change.id}`,
type: 'schema_change',
title: `Schema Change: ${change.description}`,
description: `${change.description}\n\nSeverity: ${change.severity}, Impact: ${change.impact}`,
context: {
affectedFiles: relatedImpacts.map(i => i.configFile),
schemaChanges: changes,
migrationSuggestions: [],
impactAnalysis: relatedImpacts,
estimatedEffort: changes.length > 5 ? 'HIGH' : changes.length > 2 ? 'MEDIUM' : 'LOW'
},
options,
defaultOption: change.severity === 'LOW' ? 'accept' : 'customize',
required: change.migrationRequired,
dependencies: [],
metadata: {
priority: change.severity,
riskLevel: change.impact === 'BREAKING' ? 'HIGH' : change.impact === 'COMPATIBLE' ? 'LOW' : 'MEDIUM',
reversible: !change.dataBackupRequired,
estimatedTime: changes.length * 5 // 5 minutes per change
}
};
}
/**
* Private: Generate prompt for migration suggestions
*/
generateMigrationPrompt(suggestion, changes, impacts) {
if (suggestion.type === 'AUTOMATIC') {
// Skip prompts for automatic migrations in most cases
return null;
}
const options = [];
// Execute option
options.push({
id: 'execute',
label: `Execute ${suggestion.title}`,
description: suggestion.description,
type: 'accept',
consequences: suggestion.steps.map(s => s.description),
prerequisites: suggestion.prerequisites,
actions: suggestion.steps.map(step => ({
type: step.type.toLowerCase(),
description: step.description,
targetPath: step.configPath || '',
parameters: step.configChanges || {},
validation: step.validation ? {
type: 'semantic',
command: step.validation,
expectedOutcome: 'success'
} : undefined
})),
reversible: suggestion.rollbackPlan.length > 0,
riskLevel: suggestion.riskLevel
});
// Skip option
options.push({
id: 'skip',
label: 'Skip Migration',
description: 'Skip this migration and handle manually later',
type: 'skip',
consequences: ['Manual migration required later'],
prerequisites: [],
actions: [],
reversible: true,
riskLevel: 'LOW'
});
return {
id: `migration-${suggestion.id}`,
type: 'migration_suggestion',
title: suggestion.title,
description: suggestion.description,
context: {
affectedFiles: [],
schemaChanges: changes.filter(c => suggestion.affectedChanges.includes(c.id)),
migrationSuggestions: [suggestion],
impactAnalysis: impacts,
estimatedEffort: suggestion.estimatedTime > 60 ? 'HIGH' : suggestion.estimatedTime > 30 ? 'MEDIUM' : 'LOW'
},
options,
defaultOption: suggestion.type === 'SEMI_AUTOMATIC' ? 'execute' : 'skip',
required: suggestion.priority === 'CRITICAL',
dependencies: [],
metadata: {
priority: suggestion.priority,
riskLevel: suggestion.riskLevel,
reversible: suggestion.rollbackPlan.length > 0,
estimatedTime: suggestion.estimatedTime
}
};
}
/**
* Private: Generate prompt for impact resolution
*/
generateImpactResolutionPrompt(impact, changes) {
if (!impact.migrationNeeded)
return null;
const options = [];
// Auto-resolve option
options.push({
id: 'auto-resolve',
label: 'Auto-resolve Impact',
description: 'Automatically resolve configuration impacts',
type: 'accept',
consequences: ['Configuration will be updated automatically'],
prerequisites: [],
actions: [{
type: 'file_update',
description: `Update ${impact.configFile}`,
targetPath: impact.configFile,
parameters: { changes: impact.changes }
}],
reversible: true,
riskLevel: impact.estimatedEffort === 'HIGH' ? 'HIGH' : 'MEDIUM'
});
// Manual review option
options.push({
id: 'manual-review',
label: 'Manual Review',
description: 'Manually review and resolve configuration impacts',
type: 'customize',
consequences: ['Manual configuration editing required'],
prerequisites: ['File editing access'],
actions: [{
type: 'user_input',
description: 'Open file for manual editing',
targetPath: impact.configFile,
parameters: { action: 'edit' }
}],
reversible: true,
riskLevel: 'LOW'
});
return {
id: `impact-${impact.configFile.replace(/[^a-zA-Z0-9]/g, '-')}`,
type: 'impact_resolution',
title: `Configuration Impact: ${path.basename(impact.configFile)}`,
description: `Configuration file needs updates due to schema changes.\nAffected sections: ${impact.affectedSections.join(', ')}`,
context: {
affectedFiles: [impact.configFile],
schemaChanges: changes,
migrationSuggestions: [],
impactAnalysis: [impact],
estimatedEffort: impact.estimatedEffort
},
options,
defaultOption: impact.estimatedEffort === 'LOW' ? 'auto-resolve' : 'manual-review',
required: true,
dependencies: [],
metadata: {
priority: impact.estimatedEffort === 'HIGH' ? 'HIGH' : 'MEDIUM',
riskLevel: impact.backupRecommended ? 'HIGH' : 'MEDIUM',
reversible: true,
estimatedTime: impact.estimatedEffort === 'HIGH' ? 30 : impact.estimatedEffort === 'MEDIUM' ? 15 : 5
}
};
}
/**
* Private: Sort prompts by priority and dependencies
*/
sortPromptsByPriority(prompts) {
const priorityOrder = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
return prompts.sort((a, b) => {
// First sort by priority
const priorityDiff = priorityOrder[b.metadata.priority] - priorityOrder[a.metadata.priority];
if (priorityDiff !== 0)
return priorityDiff;
// Then by required status
if (a.required !== b.required)
return a.required ? -1 : 1;
// Then by estimated time (shorter first)
return a.metadata.estimatedTime - b.metadata.estimatedTime;
});
}
/**
* Private: Extract affected files from impacts
*/
extractAffectedFiles(impacts) {
return [...new Set(impacts.map(i => i.configFile))];
}
/**
* Private: Run interactive session
*/
async runInteractiveSession(session) {
console.log(`\n๐ Starting Configuration Session`);
console.log(` Session ID: ${session.id}`);
console.log(` Prompts: ${session.prompts.length}`);
console.log(` Estimated Time: ${session.metadata.estimatedTotalTime} minutes`);
for (const prompt of session.prompts) {
try {
const choice = await this.presentPrompt(prompt);
const validation = this.validateChoice(prompt, choice);
if (!validation.isValid) {
console.log(`\nโ Invalid choice: ${validation.errors.join(', ')}`);
continue; // Re-prompt
}
if (validation.warnings.length > 0) {
console.log(`\nโ ๏ธ Warnings: ${validation.warnings.join(', ')}`);
}
session.choices.push(choice);
session.metadata.completedPrompts++;
// Execute the choice if not in dry-run mode
if (this.options.interactiveMode) {
const execution = await this.executeChoice(prompt, choice, false);
if (!execution.success) {
console.log(`\nโ Execution failed: ${execution.errors.join(', ')}`);
}
else {
console.log(`\nโ
Successfully executed: ${execution.executedActions.length} actions`);
}
}
}
catch (error) {
logger_1.Logger.error(`Prompt failed: ${prompt.id} - ${error.message}`);
}
}
session.status = 'completed';
session.endTime = new Date();
session.metadata.actualTime = Math.floor((session.endTime.getTime() - session.startTime.getTime()) / 1000 / 60);
console.log(`\n๐ Configuration session completed!`);
console.log(` Total time: ${session.metadata.actualTime} minutes`);
console.log(` Choices made: ${session.choices.length}/${session.prompts.length}`);
if (this.options.saveChoices) {
await this.saveSession(session);
}
}
/**
* Private: Get user choice through readline
*/
async getUserChoice(prompt) {
if (!this.readline) {
throw new Error('Readline not initialized for interactive mode');
}
return new Promise((resolve) => {
const question = `\n๐ค Select option (1-${prompt.options.length})${prompt.defaultOption ? ` [default: ${prompt.options.findIndex(o => o.id === prompt.defaultOption) + 1}]` : ''}: `;
this.readline.question(question, (answer) => {
const selection = answer.trim();
let optionIndex;
if (selection === '' && prompt.defaultOption) {
optionIndex = prompt.options.findIndex(o => o.id === prompt.defaultOption);
}
else {
optionIndex = parseInt(selection) - 1;
}
if (optionIndex >= 0 && optionIndex < prompt.options.length) {
const selectedOption = prompt.options[optionIndex];
// Ask for confidence level
this.readline.question(`\n๐ฏ Confidence level (1-100) [80]: `, (confidenceAnswer) => {
const confidence = confidenceAnswer.trim() === '' ? 80 : parseInt(confidenceAnswer) || 80;
resolve({
promptId: prompt.id,
optionId: selectedOption.id,
timestamp: new Date(),
confidence: Math.max(1, Math.min(100, confidence))
});
});
}
else {
console.log(`\nโ Invalid selection. Please choose 1-${prompt.options.length}.`);
// Recursively ask again
this.getUserChoice(prompt).then(resolve);
}
});
});
}
/**
* Private: Validate custom parameters
*/
validateCustomParameters(option, parameters) {
// Basic validation - in a real implementation this would be more sophisticated
return {
errors: [],
warnings: Object.keys(parameters).length === 0 ? ['No custom parameters provided'] : []
};
}
/**
* Private: Execute a single configuration action
*/
async executeAction(action, customParams, dryRun) {
logger_1.Logger.debug(`${dryRun ? 'Dry run: ' : 'Executing:'} ${action.type} - ${action.description}`);
if (dryRun) {
return { success: true };
}
try {
switch (action.type) {
case 'file_update':
return await this.executeFileUpdate(action, customParams);
case 'file_create':
return await this.executeFileCreate(action, customParams);
case 'file_backup':
return await this.executeFileBackup(action, customParams);
case 'validation':
return await this.executeValidation(action, customParams);
case 'user_input':
return { success: true }; // User input already collected
default:
return { success: false, error: `Unknown action type: ${action.type}` };
}
}
catch (error) {
return { success: false, error: error.message };
}
}
/**
* Private: Execute file update action
*/
async executeFileUpdate(action, customParams) {
// Placeholder implementation - would update configuration files
logger_1.Logger.info(`Would update file: ${action.targetPath}`);
return { success: true };
}
/**
* Private: Execute file create action
*/
async executeFileCreate(action, customParams) {
// Placeholder implementation - would create new files
logger_1.Logger.info(`Would create file: ${action.targetPath}`);
return { success: true };
}
/**
* Private: Execute file backup action
*/
async executeFileBackup(action, customParams) {
// Placeholder implementation - would backup files
logger_1.Logger.info(`Would backup file: ${action.targetPath}`);
return { success: true };
}
/**
* Private: Execute validation action
*/
async executeValidation(action, customParams) {
// Placeholder implementation - would run validation
logger_1.Logger.info(`Would validate: ${action.description}`);
return { success: true };
}
/**
* Private: Save session to disk
*/
async saveSession(session) {
const sessionFile = path.join(process.cwd(), '.supa-seed', 'sessions', `${session.id}.json`);
const sessionDir = path.dirname(sessionFile);
if (!fs.existsSync(sessionDir)) {
fs.mkdirSync(sessionDir, { recursive: true });
}
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
logger_1.Logger.info(`๐พ Configuration session saved: ${sessionFile}`);
}
/**
* Cleanup resources
*/
dispose() {
if (this.readline) {
this.readline.close();
this.readline = undefined;
}
this.cleanupSessions();
}
}
exports.ConfigurationPromptEngine = ConfigurationPromptEngine;
//# sourceMappingURL=interactive-configuration.js.map