Initial commit
This commit is contained in:
181
commands/import-projects.js
Normal file
181
commands/import-projects.js
Normal file
@@ -0,0 +1,181 @@
|
||||
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 } from "../../../src/integrations/jira/filter-processor.js";
|
||||
import { mergeEnvList, getEnvValue } from "../../../src/utils/env-manager.js";
|
||||
import { consoleLogger } from "../../../src/utils/logger.js";
|
||||
import { credentialsManager } from "../../../src/core/credentials-manager.js";
|
||||
async function importProjects(options = {}) {
|
||||
const logger = options.logger ?? consoleLogger;
|
||||
const projectRoot = process.cwd();
|
||||
console.log(chalk.cyan("\n\u{1F4E5} JIRA Project Import\n"));
|
||||
const credentials = credentialsManager.getJiraCredentials();
|
||||
if (!credentials) {
|
||||
console.log(chalk.red("\u274C No JIRA credentials found"));
|
||||
console.log(chalk.gray(" Run: specweave init"));
|
||||
return;
|
||||
}
|
||||
const client = new JiraClient(credentials);
|
||||
const filterProcessor = new FilterProcessor(client, { logger });
|
||||
if (options.resume) {
|
||||
const resumed = await resumeImport(projectRoot, client, filterProcessor, options);
|
||||
if (resumed) {
|
||||
return;
|
||||
}
|
||||
console.log(chalk.yellow("\u26A0\uFE0F No import state found. Starting fresh import.\n"));
|
||||
}
|
||||
const existing = await loadExistingProjects(projectRoot, logger);
|
||||
console.log(chalk.gray(`Current projects: ${existing.length > 0 ? existing.join(", ") : "none"}
|
||||
`));
|
||||
console.log(chalk.cyan("\u{1F4E1} Fetching available JIRA projects...\n"));
|
||||
let allProjects = [];
|
||||
try {
|
||||
const response = await client.searchProjects({ maxResults: 1e3 });
|
||||
allProjects = response.values || [];
|
||||
console.log(chalk.green(`\u2713 Found ${allProjects.length} total projects
|
||||
`));
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`\u274C Failed to fetch projects: ${error.message}`));
|
||||
return;
|
||||
}
|
||||
let filteredProjects = allProjects;
|
||||
if (options.preset) {
|
||||
console.log(chalk.cyan(`\u{1F50D} Applying preset: ${options.preset}
|
||||
`));
|
||||
try {
|
||||
filteredProjects = await filterProcessor.applyPreset(allProjects, options.preset);
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`\u274C ${error.message}`));
|
||||
return;
|
||||
}
|
||||
} else if (options.filter || options.type || options.lead || options.jql) {
|
||||
const 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("\u{1F50D} Applying filters...\n"));
|
||||
filteredProjects = await filterProcessor.applyFilters(allProjects, filterOptions);
|
||||
}
|
||||
const newProjects = filteredProjects.filter((p) => {
|
||||
return !existing.some((e) => e.toLowerCase() === p.key.toLowerCase());
|
||||
});
|
||||
if (newProjects.length === 0) {
|
||||
console.log(chalk.yellow("\u26A0\uFE0F No new projects found to import"));
|
||||
console.log(chalk.gray(" All available projects are already imported\n"));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.cyan("\u{1F4CB} 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)}
|
||||
`));
|
||||
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(` \u2728 ${p.key} - ${p.name} (${typeLabel}, ${leadLabel})`));
|
||||
});
|
||||
console.log("");
|
||||
}
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.yellow("\u{1F50D} Dry-run mode: No changes will be made\n"));
|
||||
console.log(chalk.green(`\u2713 Preview complete: ${newProjects.length} project(s) would be imported
|
||||
`));
|
||||
return;
|
||||
}
|
||||
const confirmed = await confirm({
|
||||
message: `Import ${newProjects.length} new project(s)?`,
|
||||
default: true
|
||||
});
|
||||
if (!confirmed) {
|
||||
console.log(chalk.yellow("\n\u23ED\uFE0F Import canceled\n"));
|
||||
return;
|
||||
}
|
||||
const projectKeys = newProjects.map((p) => p.key);
|
||||
console.log(chalk.cyan("\n\u{1F4E5} Importing projects...\n"));
|
||||
try {
|
||||
await mergeEnvList({
|
||||
key: "JIRA_PROJECTS",
|
||||
newValues: projectKeys,
|
||||
projectRoot,
|
||||
logger,
|
||||
createBackup: true
|
||||
});
|
||||
console.log(chalk.green(`
|
||||
\u2705 Successfully imported ${projectKeys.length} project(s)
|
||||
`));
|
||||
console.log(chalk.gray("Updated: .env (JIRA_PROJECTS)"));
|
||||
console.log(chalk.gray("Backup: .env.backup\n"));
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`
|
||||
\u274C Import failed: ${error.message}
|
||||
`));
|
||||
}
|
||||
}
|
||||
async function loadExistingProjects(projectRoot, logger) {
|
||||
const value = await getEnvValue(projectRoot, "JIRA_PROJECTS");
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
||||
}
|
||||
async function resumeImport(projectRoot, client, filterProcessor, options) {
|
||||
const stateFile = path.join(projectRoot, ".specweave", "cache", "import-state.json");
|
||||
if (!existsSync(stateFile)) {
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.cyan("\u{1F504} Resuming interrupted import...\n"));
|
||||
return false;
|
||||
}
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {};
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--filter") {
|
||||
options.filter = args[++i];
|
||||
} 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);
|
||||
}
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red(`
|
||||
\u274C Error: ${error.message}
|
||||
`));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
export {
|
||||
main as default,
|
||||
importProjects
|
||||
};
|
||||
97
commands/import-projects.md
Normal file
97
commands/import-projects.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: specweave-jira:import-projects
|
||||
description: Import additional JIRA projects post-init with smart filtering, resume, and dry-run support
|
||||
---
|
||||
|
||||
# JIRA Project Import Command
|
||||
|
||||
Import additional JIRA projects after initial setup with advanced filtering and merge capabilities.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Import all active projects
|
||||
/specweave-jira:import-projects --filter active
|
||||
|
||||
# Import with preset filter
|
||||
/specweave-jira:import-projects --preset production
|
||||
|
||||
# Import with custom JQL
|
||||
/specweave-jira:import-projects --jql "project NOT IN (TEST, SANDBOX)"
|
||||
|
||||
# Dry-run preview (no changes)
|
||||
/specweave-jira:import-projects --dry-run
|
||||
|
||||
# Resume interrupted import
|
||||
/specweave-jira:import-projects --resume
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `--filter <type>` - Filter by status (active, archived, all)
|
||||
- `--type <types>` - Filter by project type (software, business, service_desk)
|
||||
- `--lead <email>` - Filter by project lead
|
||||
- `--jql <query>` - Custom JQL filter
|
||||
- `--preset <name>` - Use saved filter preset
|
||||
- `--dry-run` - Preview without making changes
|
||||
- `--resume` - Resume interrupted import
|
||||
- `--no-progress` - Disable progress tracking
|
||||
|
||||
## Examples
|
||||
|
||||
### Import Active Projects Only
|
||||
|
||||
```bash
|
||||
/specweave-jira:import-projects --filter active
|
||||
```
|
||||
|
||||
Filters out archived projects, shows preview, prompts for confirmation, merges with existing.
|
||||
|
||||
### Import with Production Preset
|
||||
|
||||
```bash
|
||||
/specweave-jira:import-projects --preset production
|
||||
```
|
||||
|
||||
Uses the "production" preset filter (active + software + excludes TEST/SANDBOX).
|
||||
|
||||
### Custom JQL Filter
|
||||
|
||||
```bash
|
||||
/specweave-jira:import-projects --jql "lead = currentUser() AND status != Archived"
|
||||
```
|
||||
|
||||
Imports projects where you are the lead and not archived.
|
||||
|
||||
### Dry-Run Preview
|
||||
|
||||
```bash
|
||||
/specweave-jira:import-projects --filter active --dry-run
|
||||
```
|
||||
|
||||
Shows which projects would be imported without making any changes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Filtering**: Filter by status, type, lead, or custom JQL
|
||||
- **Merge with Existing**: Automatically merges with existing projects (no duplicates)
|
||||
- **Progress Tracking**: Real-time progress with ETA and cancelation support
|
||||
- **Resume Support**: Resume interrupted imports with `--resume`
|
||||
- **Dry-Run Mode**: Preview changes before applying
|
||||
- **Filter Presets**: Use saved filter combinations
|
||||
|
||||
## Implementation
|
||||
|
||||
This command:
|
||||
|
||||
1. Reads existing projects from `.env` (JIRA_PROJECTS)
|
||||
2. Fetches available projects from JIRA API
|
||||
3. Applies selected filters using FilterProcessor
|
||||
4. Shows preview with project count and reduction percentage
|
||||
5. Prompts for confirmation
|
||||
6. Batch imports with AsyncProjectLoader (50-project limit)
|
||||
7. Shows progress with ETA
|
||||
8. Merges with existing projects (no duplicates)
|
||||
9. Updates `.env` file atomically
|
||||
|
||||
Handles Ctrl+C gracefully with import state saving for resume.
|
||||
286
commands/import-projects.ts
Normal file
286
commands/import-projects.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
25
commands/refresh-cache.js
Executable file
25
commands/refresh-cache.js
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, unlinkSync, readdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
async function refreshJiraCache(projectRoot = process.cwd()) {
|
||||
const cacheDir = join(projectRoot, ".specweave", "cache", "jira");
|
||||
if (!existsSync(cacheDir)) {
|
||||
console.log("\u2705 No JIRA cache found");
|
||||
return;
|
||||
}
|
||||
console.log("\u{1F9F9} Clearing JIRA cache...");
|
||||
const files = readdirSync(cacheDir);
|
||||
let cleared = 0;
|
||||
for (const file of files) {
|
||||
const filePath = join(cacheDir, file);
|
||||
unlinkSync(filePath);
|
||||
cleared++;
|
||||
}
|
||||
console.log(`\u2705 Cleared ${cleared} cache files`);
|
||||
}
|
||||
if (require.main === module) {
|
||||
refreshJiraCache().catch(console.error);
|
||||
}
|
||||
export {
|
||||
refreshJiraCache
|
||||
};
|
||||
40
commands/refresh-cache.ts
Normal file
40
commands/refresh-cache.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* refresh-cache.ts - JIRA Plugin Cache Refresh
|
||||
*
|
||||
* Clears and refreshes JIRA sync cache to prevent stale data issues
|
||||
*
|
||||
* Usage:
|
||||
* /specweave-jira:refresh-cache
|
||||
*/
|
||||
|
||||
import { existsSync, unlinkSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export async function refreshJiraCache(projectRoot: string = process.cwd()): Promise<void> {
|
||||
const cacheDir = join(projectRoot, '.specweave', 'cache', 'jira');
|
||||
|
||||
if (!existsSync(cacheDir)) {
|
||||
console.log('✅ No JIRA cache found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🧹 Clearing JIRA cache...');
|
||||
|
||||
const files = readdirSync(cacheDir);
|
||||
let cleared = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(cacheDir, file);
|
||||
unlinkSync(filePath);
|
||||
cleared++;
|
||||
}
|
||||
|
||||
console.log(`✅ Cleared ${cleared} cache files`);
|
||||
}
|
||||
|
||||
// CLI entry
|
||||
if (require.main === module) {
|
||||
refreshJiraCache().catch(console.error);
|
||||
}
|
||||
331
commands/specweave-jira-import-boards.md
Normal file
331
commands/specweave-jira-import-boards.md
Normal file
@@ -0,0 +1,331 @@
|
||||
---
|
||||
name: specweave-jira:import-boards
|
||||
description: Import JIRA boards from a project and map them to SpecWeave projects. Creates 2-level directory structure with board-based organization.
|
||||
---
|
||||
|
||||
# Import JIRA Boards Command
|
||||
|
||||
You are a JIRA integration expert. Help the user import boards from a JIRA project and map them to SpecWeave projects.
|
||||
|
||||
## Command Usage
|
||||
|
||||
```bash
|
||||
/specweave-jira:import-boards # Interactive mode (prompts for project)
|
||||
/specweave-jira:import-boards --project CORE # Specific JIRA project
|
||||
/specweave-jira:import-boards --dry-run # Preview without creating directories
|
||||
```
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user runs this command:
|
||||
|
||||
### Step 1: Validate Prerequisites
|
||||
|
||||
1. **Check JIRA credentials** exist in `.env`:
|
||||
- `JIRA_API_TOKEN`
|
||||
- `JIRA_EMAIL`
|
||||
- `JIRA_DOMAIN`
|
||||
|
||||
2. **Check config.json** for existing board mapping:
|
||||
- If `sync.profiles.*.config.boardMapping` exists, warn user
|
||||
|
||||
### Step 2: Get Project Key
|
||||
|
||||
**If `--project` flag provided:**
|
||||
- Use the provided project key
|
||||
|
||||
**If no flag (interactive mode):**
|
||||
```
|
||||
📋 JIRA Board Import
|
||||
|
||||
Enter the JIRA project key to import boards from:
|
||||
> CORE
|
||||
|
||||
Fetching boards from project CORE...
|
||||
```
|
||||
|
||||
### Step 3: Fetch and Display Boards
|
||||
|
||||
```typescript
|
||||
import { JiraClient } from '../../../src/integrations/jira/jira-client';
|
||||
import { fetchBoardsForProject } from '../lib/jira-board-resolver';
|
||||
|
||||
const client = new JiraClient({
|
||||
domain: process.env.JIRA_DOMAIN,
|
||||
email: process.env.JIRA_EMAIL,
|
||||
apiToken: process.env.JIRA_API_TOKEN,
|
||||
instanceType: 'cloud'
|
||||
});
|
||||
|
||||
const boards = await fetchBoardsForProject(client, 'CORE');
|
||||
```
|
||||
|
||||
**Display boards:**
|
||||
```
|
||||
Found 5 boards in project CORE:
|
||||
|
||||
1. ☑ Frontend Board (Scrum, 23 active items)
|
||||
2. ☑ Backend Board (Kanban, 45 active items)
|
||||
3. ☑ Mobile Board (Scrum, 12 active items)
|
||||
4. ☐ Platform Board (Kanban, 3 active items)
|
||||
5. ☐ Archive Board (Simple, 0 items) [deselected - archive]
|
||||
|
||||
Select boards to import (Space to toggle, Enter to confirm)
|
||||
```
|
||||
|
||||
### Step 4: Map Boards to SpecWeave Projects
|
||||
|
||||
For each selected board, prompt for SpecWeave project ID:
|
||||
|
||||
```
|
||||
🏷️ Mapping boards to SpecWeave projects:
|
||||
|
||||
Board "Frontend Board" → SpecWeave project ID: [fe]
|
||||
→ Keywords for auto-classification (optional): frontend, ui, react, css
|
||||
|
||||
Board "Backend Board" → SpecWeave project ID: [be]
|
||||
→ Keywords for auto-classification (optional): api, server, database
|
||||
|
||||
Board "Mobile Board" → SpecWeave project ID: [mobile]
|
||||
→ Keywords for auto-classification (optional): ios, android, react-native
|
||||
```
|
||||
|
||||
**Project ID validation:**
|
||||
- Must be lowercase, alphanumeric with hyphens
|
||||
- Must not collide with existing project IDs
|
||||
- If collision detected, suggest prefixed version: `core-fe` instead of `fe`
|
||||
|
||||
### Step 5: Create Directory Structure
|
||||
|
||||
Create 2-level directory structure:
|
||||
|
||||
```
|
||||
.specweave/docs/internal/specs/
|
||||
└── JIRA-CORE/ ← Level 1: JIRA project
|
||||
├── fe/ ← Level 2: SpecWeave project
|
||||
│ └── .gitkeep
|
||||
├── be/
|
||||
│ └── .gitkeep
|
||||
└── mobile/
|
||||
└── .gitkeep
|
||||
```
|
||||
|
||||
### Step 6: Update config.json
|
||||
|
||||
Add board mapping to config:
|
||||
|
||||
```json
|
||||
{
|
||||
"sync": {
|
||||
"profiles": {
|
||||
"jira-default": {
|
||||
"provider": "jira",
|
||||
"config": {
|
||||
"domain": "example.atlassian.net",
|
||||
"boardMapping": {
|
||||
"projectKey": "CORE",
|
||||
"boards": [
|
||||
{
|
||||
"boardId": 123,
|
||||
"boardName": "Frontend Board",
|
||||
"specweaveProject": "fe",
|
||||
"boardType": "scrum",
|
||||
"keywords": ["frontend", "ui", "react", "css"]
|
||||
},
|
||||
{
|
||||
"boardId": 456,
|
||||
"boardName": "Backend Board",
|
||||
"specweaveProject": "be",
|
||||
"boardType": "kanban",
|
||||
"keywords": ["api", "server", "database"]
|
||||
},
|
||||
{
|
||||
"boardId": 789,
|
||||
"boardName": "Mobile Board",
|
||||
"specweaveProject": "mobile",
|
||||
"boardType": "scrum",
|
||||
"keywords": ["ios", "android", "react-native"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"multiProject": {
|
||||
"enabled": true,
|
||||
"activeProject": "fe",
|
||||
"projects": {
|
||||
"fe": {
|
||||
"name": "Frontend",
|
||||
"externalTools": {
|
||||
"jira": {
|
||||
"boardId": 123,
|
||||
"projectKey": "CORE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"be": {
|
||||
"name": "Backend",
|
||||
"externalTools": {
|
||||
"jira": {
|
||||
"boardId": 456,
|
||||
"projectKey": "CORE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mobile": {
|
||||
"name": "Mobile",
|
||||
"externalTools": {
|
||||
"jira": {
|
||||
"boardId": 789,
|
||||
"projectKey": "CORE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: Display Summary
|
||||
|
||||
```
|
||||
✅ JIRA Boards Import Complete!
|
||||
|
||||
📋 JIRA Project: CORE
|
||||
📁 Created: .specweave/docs/internal/specs/JIRA-CORE/
|
||||
|
||||
Boards imported:
|
||||
✓ Frontend Board (scrum) → fe
|
||||
Keywords: frontend, ui, react, css
|
||||
✓ Backend Board (kanban) → be
|
||||
Keywords: api, server, database
|
||||
✓ Mobile Board (scrum) → mobile
|
||||
Keywords: ios, android, react-native
|
||||
|
||||
💡 Next steps:
|
||||
1. Use /specweave:switch-project fe to switch active project
|
||||
2. Create increment: /specweave:increment "feature name"
|
||||
3. User stories will auto-sync to the correct board based on keywords
|
||||
|
||||
📖 Documentation: .specweave/docs/internal/architecture/adr/0143-jira-ado-multi-level-project-mapping.md
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Interactive Import
|
||||
```
|
||||
User: /specweave-jira:import-boards
|
||||
|
||||
You:
|
||||
📋 JIRA Board Import
|
||||
|
||||
Enter the JIRA project key: CORE
|
||||
Fetching boards...
|
||||
|
||||
Found 3 boards:
|
||||
☑ Frontend Board (scrum)
|
||||
☑ Backend Board (kanban)
|
||||
☐ Archive (simple) [deselected]
|
||||
|
||||
Mapping to SpecWeave projects:
|
||||
Frontend Board → fe
|
||||
Backend Board → be
|
||||
|
||||
✅ Import complete! 2 boards mapped.
|
||||
```
|
||||
|
||||
### Example 2: Dry Run
|
||||
```
|
||||
User: /specweave-jira:import-boards --project CORE --dry-run
|
||||
|
||||
You:
|
||||
📋 JIRA Board Import (DRY RUN)
|
||||
|
||||
Would import from project: CORE
|
||||
|
||||
Would create:
|
||||
.specweave/docs/internal/specs/JIRA-CORE/
|
||||
.specweave/docs/internal/specs/JIRA-CORE/fe/
|
||||
.specweave/docs/internal/specs/JIRA-CORE/be/
|
||||
|
||||
Would update config.json with board mapping.
|
||||
|
||||
No changes made (dry run).
|
||||
```
|
||||
|
||||
### Example 3: Already Configured
|
||||
```
|
||||
User: /specweave-jira:import-boards
|
||||
|
||||
You:
|
||||
⚠️ Board mapping already exists for project CORE
|
||||
|
||||
Current mappings:
|
||||
Frontend Board → fe
|
||||
Backend Board → be
|
||||
|
||||
Do you want to:
|
||||
1. Add more boards
|
||||
2. Replace existing mapping
|
||||
3. Cancel
|
||||
|
||||
> 1
|
||||
|
||||
Fetching additional boards...
|
||||
☐ Frontend Board (already mapped)
|
||||
☐ Backend Board (already mapped)
|
||||
☑ Mobile Board (new)
|
||||
☐ Archive (deselected)
|
||||
|
||||
Added Mobile Board → mobile
|
||||
|
||||
✅ Updated! Now 3 boards mapped.
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Missing credentials:**
|
||||
```
|
||||
❌ JIRA credentials not found
|
||||
|
||||
Please add to .env:
|
||||
JIRA_API_TOKEN=your_token
|
||||
JIRA_EMAIL=your_email@example.com
|
||||
JIRA_DOMAIN=your-company.atlassian.net
|
||||
|
||||
Or run: specweave init . (to configure JIRA)
|
||||
```
|
||||
|
||||
**Project not found:**
|
||||
```
|
||||
❌ JIRA project "INVALID" not found
|
||||
|
||||
Available projects you have access to:
|
||||
- CORE (Core Development)
|
||||
- INFRA (Infrastructure)
|
||||
- MOBILE (Mobile Team)
|
||||
|
||||
Tip: Use /specweave-jira:import-boards --project CORE
|
||||
```
|
||||
|
||||
**No boards found:**
|
||||
```
|
||||
⚠️ No boards found in project CORE
|
||||
|
||||
This could mean:
|
||||
1. The project uses classic projects (no boards)
|
||||
2. You don't have access to boards in this project
|
||||
|
||||
Suggestions:
|
||||
- Use /specweave-jira:import-projects for project-based sync
|
||||
- Ask your JIRA admin about board access
|
||||
```
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/specweave-jira:import-projects` - Import multiple JIRA projects (not boards)
|
||||
- `/specweave-jira:sync` - Sync increments with JIRA
|
||||
- `/specweave:switch-project` - Switch active SpecWeave project
|
||||
- `/specweave:init-multiproject` - Initialize multi-project mode
|
||||
298
commands/specweave-jira-import-projects.md
Normal file
298
commands/specweave-jira-import-projects.md
Normal file
@@ -0,0 +1,298 @@
|
||||
---
|
||||
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.
|
||||
240
commands/specweave-jira-sync.md
Normal file
240
commands/specweave-jira-sync.md
Normal file
@@ -0,0 +1,240 @@
|
||||
---
|
||||
name: specweave-jira:sync
|
||||
description: Sync SpecWeave increments with JIRA epics/stories. Supports import, export, two-way sync, and granular item operations
|
||||
---
|
||||
|
||||
# Sync Jira Command
|
||||
|
||||
You are a Jira synchronization expert. Help the user sync between Jira and SpecWeave with granular control.
|
||||
|
||||
## Available Operations
|
||||
|
||||
### Epic-Level Operations
|
||||
|
||||
**1. Two-way Sync (Default - Recommended)**
|
||||
```
|
||||
/specweave-jira:sync 0003 # Two-way sync (default)
|
||||
/specweave-jira:sync 0003 --direction two-way # Explicit
|
||||
```
|
||||
|
||||
**2. Import Jira Epic as SpecWeave Increment**
|
||||
```
|
||||
/specweave-jira:sync import SCRUM-123 # One-time pull
|
||||
/specweave-jira:sync SCRUM-123 --direction from-jira # Same as import
|
||||
```
|
||||
|
||||
**3. Export SpecWeave Increment to Jira**
|
||||
```
|
||||
/specweave-jira:sync export 0001 # One-time push
|
||||
/specweave-jira:sync 0001 --direction to-jira # Same as export
|
||||
```
|
||||
|
||||
### Sync Direction Options
|
||||
|
||||
**Default: `two-way`** (both directions - recommended)
|
||||
|
||||
- `--direction two-way`: SpecWeave ↔ Jira (default)
|
||||
- Pull changes FROM Jira (status, priority, comments)
|
||||
- Push changes TO Jira (tasks, progress, metadata)
|
||||
|
||||
- `--direction to-jira`: SpecWeave → Jira only
|
||||
- Push increment progress to Jira
|
||||
- Don't pull Jira changes back
|
||||
- Same as `export` operation
|
||||
|
||||
- `--direction from-jira`: Jira → SpecWeave only
|
||||
- Pull Jira issue updates
|
||||
- Don't push SpecWeave changes
|
||||
- Same as `import` operation
|
||||
|
||||
### Granular Item Operations
|
||||
|
||||
**4. Add specific Story/Bug/Task to existing Increment**
|
||||
```
|
||||
/specweave-jira:sync add SCRUM-1 to 0003
|
||||
/specweave-jira:sync add SCRUM-1 # Adds to current increment
|
||||
```
|
||||
|
||||
**5. Create Increment from specific items (cherry-pick)**
|
||||
```
|
||||
/specweave-jira:sync create "User Authentication" from SCRUM-1 SCRUM-5 SCRUM-7
|
||||
/specweave-jira:sync create "Bug Fixes Sprint 1" from SCRUM-10 SCRUM-15 SCRUM-20
|
||||
```
|
||||
|
||||
**6. Show sync status**
|
||||
```
|
||||
/specweave-jira:sync status
|
||||
/specweave-jira:sync status 0003 # Status of specific increment
|
||||
```
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user runs this command:
|
||||
|
||||
1. **Parse the command arguments**:
|
||||
- Operation: import, sync, export, add, create, or status
|
||||
- ID: Jira Epic key (e.g., SCRUM-123) or Increment ID (e.g., 0001)
|
||||
|
||||
2. **Execute the operation**:
|
||||
|
||||
**For import Epic**:
|
||||
```typescript
|
||||
import { JiraClient } from './src/integrations/jira/jira-client';
|
||||
import { JiraMapper } from './src/integrations/jira/jira-mapper';
|
||||
|
||||
const client = new JiraClient();
|
||||
const mapper = new JiraMapper(client);
|
||||
const result = await mapper.importEpicAsIncrement('SCRUM-123');
|
||||
```
|
||||
|
||||
**For add item**:
|
||||
```typescript
|
||||
import { JiraIncrementalMapper } from './src/integrations/jira/jira-incremental-mapper';
|
||||
|
||||
const incrementalMapper = new JiraIncrementalMapper(client);
|
||||
const result = await incrementalMapper.addItemToIncrement('0003', 'SCRUM-1');
|
||||
```
|
||||
|
||||
**For create from items**:
|
||||
```typescript
|
||||
const result = await incrementalMapper.createIncrementFromItems(
|
||||
'User Authentication',
|
||||
['SCRUM-1', 'SCRUM-5', 'SCRUM-7']
|
||||
);
|
||||
```
|
||||
|
||||
**For sync**:
|
||||
```typescript
|
||||
const result = await mapper.syncIncrement('0003');
|
||||
```
|
||||
|
||||
**For export**:
|
||||
```typescript
|
||||
const result = await mapper.exportIncrementAsEpic('0001', 'SCRUM');
|
||||
```
|
||||
|
||||
3. **Show results**:
|
||||
- Display sync summary
|
||||
- Show conflicts (if any)
|
||||
- List created/updated files
|
||||
- Provide links to Jira and SpecWeave
|
||||
|
||||
4. **Handle errors gracefully**:
|
||||
- Check if .env credentials exist
|
||||
- Validate increment/epic exists
|
||||
- Show clear error messages
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Import Epic
|
||||
**User**: `/specweave:sync-jira import SCRUM-2`
|
||||
**You**:
|
||||
- Import Epic SCRUM-2 from Jira
|
||||
- Show: "✅ Imported as Increment 0004"
|
||||
- List: "Created: spec.md, tasks.md, RFC document"
|
||||
- Link: "Jira: https://... | Increment: .specweave/increments/0004/"
|
||||
|
||||
### Example 2: Add Story to Current Increment
|
||||
**User**: `/specweave:sync-jira add SCRUM-1`
|
||||
**You**:
|
||||
- Determine current increment (latest or from context)
|
||||
- Fetch SCRUM-1 from Jira
|
||||
- Add to increment's spec.md (under ## User Stories)
|
||||
- Update tasks.md
|
||||
- Update RFC
|
||||
- Show: "✅ Added Story SCRUM-1 to Increment 0003"
|
||||
- Display: "Type: story | Title: User can login | Status: in-progress"
|
||||
|
||||
### Example 3: Add Bug to Specific Increment
|
||||
**User**: `/specweave:sync-jira add SCRUM-10 to 0003`
|
||||
**You**:
|
||||
- Fetch SCRUM-10 from Jira (it's a Bug)
|
||||
- Add to increment 0003's spec.md (under ## Bugs)
|
||||
- Update tasks.md
|
||||
- Update RFC
|
||||
- Show: "✅ Added Bug SCRUM-10 to Increment 0003"
|
||||
- Display: "Type: bug | Priority: P1 | Title: Fix login redirect"
|
||||
|
||||
### Example 4: Create Increment from Multiple Items
|
||||
**User**: `/specweave:sync-jira create "User Authentication" from SCRUM-1 SCRUM-5 SCRUM-7`
|
||||
**You**:
|
||||
- Fetch all 3 issues from Jira
|
||||
- Determine types (story, bug, task)
|
||||
- Create new increment 0005
|
||||
- Group by type in spec.md:
|
||||
- ## User Stories (SCRUM-1, SCRUM-5)
|
||||
- ## Technical Tasks (SCRUM-7)
|
||||
- Generate RFC with all items
|
||||
- Show: "✅ Created Increment 0005 with 3 work items"
|
||||
- Display table:
|
||||
```
|
||||
| Type | Jira Key | Title |
|
||||
|-------|----------|-----------------|
|
||||
| Story | SCRUM-1 | User login UI |
|
||||
| Story | SCRUM-5 | OAuth backend |
|
||||
| Task | SCRUM-7 | Setup provider |
|
||||
```
|
||||
|
||||
### Example 5: Two-way Sync (Default)
|
||||
**User**: `/specweave-jira:sync 0003`
|
||||
**You**:
|
||||
- Read increment 0003
|
||||
- Find linked Jira items (from spec.md frontmatter.work_items)
|
||||
- Fetch current state from Jira
|
||||
|
||||
**Detect changes (both directions)**:
|
||||
- FROM Jira: Status changes, priority updates, comments
|
||||
- FROM SpecWeave: Task completion, progress updates
|
||||
|
||||
**Show two-way sync summary**:
|
||||
```
|
||||
✅ Two-way Sync Complete: 0003 ↔ Jira
|
||||
|
||||
FROM Jira:
|
||||
• SCRUM-1: Status changed to In Progress
|
||||
• SCRUM-10: Priority raised to P1
|
||||
|
||||
FROM SpecWeave:
|
||||
• 3 tasks completed (T-005, T-006, T-007)
|
||||
• Progress: 60% → 75%
|
||||
|
||||
Conflicts: None
|
||||
```
|
||||
|
||||
**Handle conflicts if any**:
|
||||
- Show both versions (Jira vs SpecWeave)
|
||||
- Ask user which to keep or how to merge
|
||||
- Apply resolution in both directions
|
||||
|
||||
### Example 6: Status Overview
|
||||
**User**: `/specweave-jira:sync status`
|
||||
**You**:
|
||||
- Scan all increments for Jira metadata
|
||||
- Show table:
|
||||
```
|
||||
| Increment | Title | Jira Items | Last Sync |
|
||||
|-----------|------------------|------------|--------------------|
|
||||
| 0003 | Test Epic | 0 items | 2025-10-28 17:42 |
|
||||
| 0004 | User Auth | 3 items | 2025-10-28 18:00 |
|
||||
| 0005 | Bug Fixes | 5 items | Never |
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Always check if .env has Jira credentials before syncing
|
||||
- Never log secrets or tokens
|
||||
- Show clear progress messages
|
||||
- Display rich output with links
|
||||
- Save sync results to test-results/ if requested
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/specweave-github:sync` - Sync to GitHub issues (also two-way by default)
|
||||
- `/specweave:increment` - Create new increment
|
||||
- `/specweave:validate` - Validate increment quality
|
||||
|
||||
---
|
||||
|
||||
**Two-way by Default**: All sync operations are two-way unless you explicitly specify `--direction to-jira` or `--direction from-jira`. This keeps both systems synchronized automatically.
|
||||
|
||||
**Granular Control**: Unlike simple epic import/export, this command supports cherry-picking individual stories, bugs, and tasks for maximum flexibility.
|
||||
Reference in New Issue
Block a user