Files
gh-anton-abyzov-specweave-p…/commands/import-projects.ts
2025-11-29 17:56:44 +08:00

287 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* JIRA Project Import Command (Post-Init)
*
* Import additional JIRA projects after initial setup with smart filtering,
* resume support, and merge capabilities.
*
* Features:
* - Smart filtering (active, type, lead, JQL)
* - Filter presets (production, active-only, agile-only)
* - Dry-run preview mode
* - Resume interrupted imports
* - Progress tracking with ETA
* - Merge with existing projects (no duplicates)
*
* NEW (v0.24.0): Post-init flexibility for project management
*
* @module plugins/specweave-jira/commands/import-projects
*/
import chalk from 'chalk';
import { confirm } from '@inquirer/prompts';
import { existsSync } from 'fs';
import path from 'path';
import { JiraClient } from '../../../src/integrations/jira/jira-client.js';
import { FilterProcessor, type FilterOptions } from '../../../src/integrations/jira/filter-processor.js';
import { AsyncProjectLoader } from '../../../src/cli/helpers/async-project-loader.js';
import { mergeEnvList, getEnvValue } from '../../../src/utils/env-manager.js';
import { consoleLogger, type Logger } from '../../../src/utils/logger.js';
import { credentialsManager } from '../../../src/core/credentials-manager.js';
export interface ImportProjectsOptions {
filter?: 'active' | 'archived' | 'all';
type?: string[];
lead?: string;
jql?: string;
preset?: string;
dryRun?: boolean;
resume?: boolean;
noProgress?: boolean;
logger?: Logger;
}
export interface ImportState {
total: number;
completed: string[];
remaining: string[];
timestamp: number;
}
/**
* Import JIRA projects command
*
* TC-063: Post-Init Import (Merge with Existing)
* TC-064: Filter Active Projects Only
* TC-068: Resume Interrupted Import
* TC-069: Dry-Run Preview
* TC-070: Progress During Import
*
* @param options - Import options
*/
export async function importProjects(options: ImportProjectsOptions = {}): Promise<void> {
const logger = options.logger ?? consoleLogger;
const projectRoot = process.cwd();
// Step 1: Load credentials
console.log(chalk.cyan('\n📥 JIRA Project Import\n'));
const credentials = credentialsManager.getJiraCredentials();
if (!credentials) {
console.log(chalk.red('❌ No JIRA credentials found'));
console.log(chalk.gray(' Run: specweave init'));
return;
}
const client = new JiraClient(credentials);
const filterProcessor = new FilterProcessor(client, { logger });
// Step 2: Check for resume state
if (options.resume) {
const resumed = await resumeImport(projectRoot, client, filterProcessor, options);
if (resumed) {
return;
}
console.log(chalk.yellow('⚠️ No import state found. Starting fresh import.\n'));
}
// Step 3: Read existing projects from .env
const existing = await loadExistingProjects(projectRoot, logger);
console.log(chalk.gray(`Current projects: ${existing.length > 0 ? existing.join(', ') : 'none'}\n`));
// Step 4: Fetch available projects from JIRA
console.log(chalk.cyan('📡 Fetching available JIRA projects...\n'));
let allProjects: any[] = [];
try {
const response = await client.searchProjects({ maxResults: 1000 });
allProjects = response.values || [];
console.log(chalk.green(`✓ Found ${allProjects.length} total projects\n`));
} catch (error: any) {
console.log(chalk.red(`❌ Failed to fetch projects: ${error.message}`));
return;
}
// Step 5: Apply filters
let filteredProjects = allProjects;
if (options.preset) {
// Use filter preset
console.log(chalk.cyan(`🔍 Applying preset: ${options.preset}\n`));
try {
filteredProjects = await filterProcessor.applyPreset(allProjects, options.preset);
} catch (error: any) {
console.log(chalk.red(`${error.message}`));
return;
}
} else if (options.filter || options.type || options.lead || options.jql) {
// Build filter options
const filterOptions: FilterOptions = {};
if (options.filter === 'active') {
filterOptions.active = true;
} else if (options.filter === 'archived') {
filterOptions.active = false;
}
if (options.type) {
filterOptions.types = options.type;
}
if (options.lead) {
filterOptions.lead = options.lead;
}
if (options.jql) {
filterOptions.jql = options.jql;
}
console.log(chalk.cyan('🔍 Applying filters...\n'));
filteredProjects = await filterProcessor.applyFilters(allProjects, filterOptions);
}
// Step 6: Exclude existing projects
const newProjects = filteredProjects.filter(p => {
return !existing.some(e => e.toLowerCase() === p.key.toLowerCase());
});
if (newProjects.length === 0) {
console.log(chalk.yellow('⚠️ No new projects found to import'));
console.log(chalk.gray(' All available projects are already imported\n'));
return;
}
// Step 7: Show preview
console.log(chalk.cyan('📋 Import Preview:\n'));
console.log(chalk.white(` Total available: ${allProjects.length}`));
console.log(chalk.white(` After filtering: ${filteredProjects.length}`));
console.log(chalk.white(` Already imported: ${existing.length}`));
console.log(chalk.white(` New projects: ${chalk.green.bold(newProjects.length)}\n`));
if (newProjects.length <= 10) {
console.log(chalk.gray('Projects to import:'));
newProjects.forEach(p => {
const typeLabel = p.projectTypeKey || 'unknown';
const leadLabel = p.lead?.displayName || 'no lead';
console.log(chalk.gray(`${p.key} - ${p.name} (${typeLabel}, ${leadLabel})`));
});
console.log('');
}
// Step 8: Dry-run mode (exit without changes)
if (options.dryRun) {
console.log(chalk.yellow('🔍 Dry-run mode: No changes will be made\n'));
console.log(chalk.green(`✓ Preview complete: ${newProjects.length} project(s) would be imported\n`));
return;
}
// Step 9: Confirm import
const confirmed = await confirm({
message: `Import ${newProjects.length} new project(s)?`,
default: true
});
if (!confirmed) {
console.log(chalk.yellow('\n⏭ Import canceled\n'));
return;
}
// Step 10: Extract project keys
const projectKeys = newProjects.map(p => p.key);
// Step 11: Merge with existing
console.log(chalk.cyan('\n📥 Importing projects...\n'));
try {
await mergeEnvList({
key: 'JIRA_PROJECTS',
newValues: projectKeys,
projectRoot,
logger,
createBackup: true
});
console.log(chalk.green(`\n✅ Successfully imported ${projectKeys.length} project(s)\n`));
console.log(chalk.gray('Updated: .env (JIRA_PROJECTS)'));
console.log(chalk.gray('Backup: .env.backup\n'));
} catch (error: any) {
console.log(chalk.red(`\n❌ Import failed: ${error.message}\n`));
}
}
/**
* Load existing JIRA projects from .env
*/
async function loadExistingProjects(projectRoot: string, logger: Logger): Promise<string[]> {
const value = await getEnvValue(projectRoot, 'JIRA_PROJECTS');
if (!value) {
return [];
}
return value.split(',').map(v => v.trim()).filter(v => v.length > 0);
}
/**
* Resume interrupted import
*/
async function resumeImport(
projectRoot: string,
client: JiraClient,
filterProcessor: FilterProcessor,
options: ImportProjectsOptions
): Promise<boolean> {
const stateFile = path.join(projectRoot, '.specweave', 'cache', 'import-state.json');
if (!existsSync(stateFile)) {
return false;
}
console.log(chalk.cyan('🔄 Resuming interrupted import...\n'));
// Load state and continue
// (Implementation would load state, skip completed, import remaining)
return false; // Placeholder - full implementation would handle resume
}
/**
* Command entry point (called by CLI)
*/
export default async function main(): Promise<void> {
// Parse CLI arguments
const args = process.argv.slice(2);
const options: ImportProjectsOptions = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--filter') {
options.filter = args[++i] as 'active' | 'archived' | 'all';
} else if (arg === '--type') {
options.type = args[++i].split(',');
} else if (arg === '--lead') {
options.lead = args[++i];
} else if (arg === '--jql') {
options.jql = args[++i];
} else if (arg === '--preset') {
options.preset = args[++i];
} else if (arg === '--dry-run') {
options.dryRun = true;
} else if (arg === '--resume') {
options.resume = true;
} else if (arg === '--no-progress') {
options.noProgress = true;
}
}
await importProjects(options);
}
// Run command if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(error => {
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
process.exit(1);
});
}