299 lines
8.8 KiB
Markdown
299 lines
8.8 KiB
Markdown
---
|
|
name: specweave-jira:import-projects
|
|
description: Import additional JIRA projects post-init with filtering, resume support, and dry-run preview
|
|
---
|
|
|
|
# Import JIRA Projects Command
|
|
|
|
You are a JIRA project import expert. Help users add additional JIRA projects to their SpecWeave workspace after initial setup.
|
|
|
|
## Purpose
|
|
|
|
This command allows users to import additional JIRA projects **after** initial SpecWeave setup (`specweave init`), with advanced filtering, resume capability, and dry-run preview.
|
|
|
|
**Use Cases**:
|
|
- Adding new JIRA projects to existing workspace
|
|
- Importing archived/paused projects later
|
|
- Selective import with filters (active only, specific types, custom JQL)
|
|
|
|
## Command Syntax
|
|
|
|
```bash
|
|
# Basic import (interactive)
|
|
/specweave-jira:import-projects
|
|
|
|
# With filters
|
|
/specweave-jira:import-projects --filter active
|
|
/specweave-jira:import-projects --type agile --lead "john.doe@company.com"
|
|
/specweave-jira:import-projects --jql "project IN (BACKEND, FRONTEND) AND status != Archived"
|
|
|
|
# Dry-run (preview)
|
|
/specweave-jira:import-projects --dry-run
|
|
|
|
# Resume interrupted import
|
|
/specweave-jira:import-projects --resume
|
|
|
|
# Combined
|
|
/specweave-jira:import-projects --filter active --dry-run
|
|
```
|
|
|
|
## Your Task
|
|
|
|
When the user runs this command:
|
|
|
|
### Step 1: Validate Prerequisites
|
|
```typescript
|
|
import { readEnvFile, parseEnvFile } from '../../../src/utils/env-file.js';
|
|
|
|
// 1. Check if Jira credentials exist
|
|
const envContent = readEnvFile(process.cwd());
|
|
const parsed = parseEnvFile(envContent);
|
|
|
|
if (!parsed.JIRA_API_TOKEN || !parsed.JIRA_EMAIL || !parsed.JIRA_DOMAIN) {
|
|
console.log('❌ Missing Jira credentials. Run `specweave init` first.');
|
|
return;
|
|
}
|
|
|
|
// 2. Get existing projects
|
|
const existingProjects = parsed.JIRA_PROJECTS?.split(',') || [];
|
|
console.log(`\n📋 Current projects: ${existingProjects.join(', ') || 'None'}\n`);
|
|
```
|
|
|
|
### Step 2: Fetch Available Projects
|
|
```typescript
|
|
import { getProjectCount } from '../../../src/cli/helpers/project-count-fetcher.js';
|
|
import { AsyncProjectLoader } from '../../../src/cli/helpers/async-project-loader.js';
|
|
|
|
// Count check (< 1 second)
|
|
const countResult = await getProjectCount({
|
|
provider: 'jira',
|
|
credentials: {
|
|
domain: parsed.JIRA_DOMAIN,
|
|
email: parsed.JIRA_EMAIL,
|
|
token: parsed.JIRA_API_TOKEN,
|
|
instanceType: 'cloud'
|
|
}
|
|
});
|
|
|
|
console.log(`✓ Found ${countResult.accessible} accessible projects`);
|
|
|
|
// Fetch all projects (with smart pagination)
|
|
const loader = new AsyncProjectLoader(
|
|
{
|
|
domain: parsed.JIRA_DOMAIN,
|
|
email: parsed.JIRA_EMAIL,
|
|
token: parsed.JIRA_API_TOKEN,
|
|
instanceType: 'cloud'
|
|
},
|
|
'jira',
|
|
{
|
|
batchSize: 50,
|
|
updateFrequency: 5,
|
|
showEta: true
|
|
}
|
|
);
|
|
|
|
const result = await loader.fetchAllProjects(countResult.accessible);
|
|
let allProjects = result.projects;
|
|
```
|
|
|
|
### Step 3: Apply Filters (if specified)
|
|
```typescript
|
|
import { FilterProcessor } from '../../../src/integrations/jira/filter-processor.js';
|
|
|
|
const options = {
|
|
filter: args.filter, // 'active' | 'all'
|
|
type: args.type, // 'agile' | 'software' | 'business'
|
|
lead: args.lead, // Email address
|
|
jql: args.jql // Custom JQL
|
|
};
|
|
|
|
const filterProcessor = new FilterProcessor({ domain: parsed.JIRA_DOMAIN, token: parsed.JIRA_API_TOKEN });
|
|
const filteredProjects = await filterProcessor.applyFilters(allProjects, options);
|
|
|
|
console.log(`\n🔍 Filters applied:`);
|
|
if (options.filter === 'active') console.log(` • Active projects only`);
|
|
if (options.type) console.log(` • Type: ${options.type}`);
|
|
if (options.lead) console.log(` • Lead: ${options.lead}`);
|
|
if (options.jql) console.log(` • JQL: ${options.jql}`);
|
|
console.log(`\n📊 Results: ${filteredProjects.length} projects (down from ${allProjects.length})\n`);
|
|
```
|
|
|
|
### Step 4: Exclude Existing Projects
|
|
```typescript
|
|
const newProjects = filteredProjects.filter(p => !existingProjects.includes(p.key));
|
|
|
|
if (newProjects.length === 0) {
|
|
console.log('✅ No new projects to import. All filtered projects are already configured.');
|
|
return;
|
|
}
|
|
|
|
console.log(`📥 ${newProjects.length} new project(s) available for import:\n`);
|
|
newProjects.forEach(p => {
|
|
console.log(` ✨ ${p.key} - ${p.name} (${p.projectTypeKey}, lead: ${p.lead?.displayName || 'N/A'})`);
|
|
});
|
|
```
|
|
|
|
### Step 5: Dry-Run or Execute
|
|
```typescript
|
|
if (args.dryRun) {
|
|
console.log('\n🔎 DRY RUN: No changes will be made.\n');
|
|
console.log('The following projects would be imported:');
|
|
newProjects.forEach(p => {
|
|
const status = p.archived ? '⏭️ (archived - skipped)' : '✨';
|
|
console.log(` ${status} ${p.key} - ${p.name}`);
|
|
});
|
|
console.log(`\nTotal: ${newProjects.filter(p => !p.archived).length} projects would be imported\n`);
|
|
return;
|
|
}
|
|
|
|
// Confirm import
|
|
const { confirmed } = await inquirer.prompt([{
|
|
type: 'confirm',
|
|
name: 'confirmed',
|
|
message: `Import ${newProjects.length} project(s)?`,
|
|
default: true
|
|
}]);
|
|
|
|
if (!confirmed) {
|
|
console.log('⏭️ Import cancelled.');
|
|
return;
|
|
}
|
|
```
|
|
|
|
### Step 6: Merge with Existing
|
|
```typescript
|
|
import { updateEnvFile, mergeProjectList } from '../../../src/utils/env-manager.js';
|
|
|
|
const newKeys = newProjects.map(p => p.key);
|
|
const mergedProjects = mergeProjectList(existingProjects, newKeys);
|
|
|
|
// Update .env file (atomic write)
|
|
await updateEnvFile('JIRA_PROJECTS', mergedProjects.join(','));
|
|
|
|
console.log('\n✅ Projects imported successfully!\n');
|
|
console.log(`Updated: ${existingProjects.length} → ${mergedProjects.length} projects`);
|
|
console.log(`\nCurrent projects:\n ${mergedProjects.join(', ')}\n`);
|
|
```
|
|
|
|
### Step 7: Resume Support
|
|
```typescript
|
|
if (args.resume) {
|
|
const { CacheManager } = await import('../../../src/core/cache/cache-manager.js');
|
|
const cacheManager = new CacheManager(process.cwd());
|
|
|
|
const importState = await cacheManager.get('jira-import-state');
|
|
|
|
if (!importState) {
|
|
console.log('⚠️ No import state found. Use without --resume to start fresh.');
|
|
return;
|
|
}
|
|
|
|
console.log(`\n📂 Resuming from: ${importState.lastProject} (${importState.completed}/${importState.total})`);
|
|
|
|
// Skip already-imported projects
|
|
const remainingProjects = allProjects.filter(p => !importState.succeeded.includes(p.key));
|
|
|
|
// Continue import with remaining projects
|
|
// (use same logic as Step 6)
|
|
}
|
|
```
|
|
|
|
## Examples
|
|
|
|
### Example 1: Basic Import
|
|
**User**: `/specweave-jira:import-projects`
|
|
|
|
**Output**:
|
|
```
|
|
📋 Current projects: BACKEND, FRONTEND
|
|
|
|
✓ Found 127 accessible projects
|
|
📥 5 new project(s) available for import:
|
|
|
|
✨ MOBILE - Mobile App (agile, lead: John Doe)
|
|
✨ INFRA - Infrastructure (software, lead: Jane Smith)
|
|
✨ QA - Quality Assurance (business, lead: Bob Wilson)
|
|
✨ DOCS - Documentation (business, lead: Alice Cooper)
|
|
✨ LEGACY - Legacy System (archived - skipped)
|
|
|
|
Import 5 project(s)? (Y/n)
|
|
|
|
✅ Projects imported successfully!
|
|
|
|
Updated: 2 → 6 projects
|
|
|
|
Current projects:
|
|
BACKEND, FRONTEND, MOBILE, INFRA, QA, DOCS
|
|
```
|
|
|
|
### Example 2: Filter Active Only
|
|
**User**: `/specweave-jira:import-projects --filter active`
|
|
|
|
**Output**:
|
|
```
|
|
🔍 Filters applied:
|
|
• Active projects only
|
|
|
|
📊 Results: 120 projects (down from 127)
|
|
|
|
📥 4 new project(s) available for import:
|
|
(LEGACY project excluded because it's archived)
|
|
```
|
|
|
|
### Example 3: Dry-Run
|
|
**User**: `/specweave-jira:import-projects --dry-run`
|
|
|
|
**Output**:
|
|
```
|
|
🔎 DRY RUN: No changes will be made.
|
|
|
|
The following projects would be imported:
|
|
✨ MOBILE - Mobile App
|
|
✨ INFRA - Infrastructure
|
|
✨ QA - Quality Assurance
|
|
⏭️ LEGACY - Legacy System (archived - skipped)
|
|
|
|
Total: 3 projects would be imported
|
|
```
|
|
|
|
### Example 4: Custom JQL Filter
|
|
**User**: `/specweave-jira:import-projects --jql "project IN (MOBILE, INFRA) AND status != Archived"`
|
|
|
|
**Output**:
|
|
```
|
|
🔍 Filters applied:
|
|
• JQL: project IN (MOBILE, INFRA) AND status != Archived
|
|
|
|
📊 Results: 2 projects (down from 127)
|
|
|
|
📥 2 new project(s) available for import:
|
|
✨ MOBILE - Mobile App
|
|
✨ INFRA - Infrastructure
|
|
```
|
|
|
|
## Important Notes
|
|
|
|
- **Merge Logic**: No duplicates. Existing projects are preserved.
|
|
- **Atomic Updates**: Uses temp file + rename to prevent corruption.
|
|
- **Progress Tracking**: Shows progress bar for large imports (> 50 projects).
|
|
- **Resume Support**: Interrupted imports can be resumed with `--resume`.
|
|
- **Backup**: Creates `.env.backup` before modifying `.env`.
|
|
|
|
## Related Commands
|
|
|
|
- `/specweave:init` - Initial SpecWeave setup
|
|
- `/specweave-jira:sync` - Sync increments with Jira epics
|
|
- `/specweave-jira:refresh-cache` - Clear cached project data
|
|
|
|
## Error Handling
|
|
|
|
- **Missing Credentials**: Prompt user to run `specweave init` first
|
|
- **API Errors**: Show clear error message with suggestion
|
|
- **No New Projects**: Inform user all projects already imported
|
|
- **Permission Errors**: Check `.env` file permissions
|
|
|
|
---
|
|
|
|
**Post-Init Flexibility**: This command provides flexibility to add projects after initial setup, supporting evolving team structures and project lifecycles.
|