@@ -19,6 +19,8 @@ import {
1919 TimerState ,
2020 SprintMetrics ,
2121 BulkOperationSummary ,
22+ ExportData ,
23+ ImportResult ,
2224 parseTimeToSeconds ,
2325 secondsToJiraFormat ,
2426 secondsToHuman ,
@@ -37,6 +39,10 @@ import {
3739 calculateBurndown ,
3840 getStoryPoints ,
3941 generateProgressBar as generateAnalyticsProgressBar ,
42+ collectExportData ,
43+ writeExportFile ,
44+ parseImportFile ,
45+ importIssues ,
4046} from "@atlcli/jira" ;
4147
4248export async function handleJira (
@@ -73,6 +79,12 @@ export async function handleJira(
7379 case "filter" :
7480 await handleFilter ( rest , flags , opts ) ;
7581 return ;
82+ case "export" :
83+ await handleExport ( flags , opts ) ;
84+ return ;
85+ case "import" :
86+ await handleImport ( flags , opts ) ;
87+ return ;
7688 case "search" :
7789 await handleSearch ( rest , flags , opts ) ;
7890 return ;
@@ -3095,6 +3107,244 @@ function formatIssue(issue: JiraIssue): Record<string, unknown> {
30953107 } ;
30963108}
30973109
3110+ // ============ Export/Import Commands ============
3111+
3112+ async function handleExport (
3113+ flags : Record < string , string | boolean > ,
3114+ opts : OutputOptions
3115+ ) : Promise < void > {
3116+ const jql = getFlag ( flags , "jql" ) ;
3117+ const outputPath = getFlag ( flags , "o" ) || getFlag ( flags , "output" ) ;
3118+ const format = ( getFlag ( flags , "format" ) || "json" ) as "csv" | "json" ;
3119+ const includeComments = ! hasFlag ( flags , "no-comments" ) ;
3120+ const includeAttachments = ! hasFlag ( flags , "no-attachments" ) ;
3121+
3122+ if ( ! jql ) {
3123+ output ( exportHelp ( ) , opts ) ;
3124+ return ;
3125+ }
3126+
3127+ if ( ! outputPath ) {
3128+ fail ( opts , 1 , ERROR_CODES . USAGE , "--output (-o) is required for export." ) ;
3129+ return ;
3130+ }
3131+
3132+ const client = await getClient ( flags , opts ) ;
3133+
3134+ // Fetch all matching issues
3135+ const allIssues : JiraIssue [ ] = [ ] ;
3136+ let startAt = 0 ;
3137+ const maxResults = 100 ;
3138+
3139+ if ( ! opts . json ) {
3140+ process . stderr . write ( "Searching issues...\n" ) ;
3141+ }
3142+
3143+ while ( true ) {
3144+ const result = await client . search ( jql , {
3145+ startAt,
3146+ maxResults,
3147+ fields : [ "*all" ] ,
3148+ } ) ;
3149+ allIssues . push ( ...result . issues ) ;
3150+ if ( allIssues . length >= result . total || result . issues . length === 0 ) break ;
3151+ startAt += maxResults ;
3152+ }
3153+
3154+ if ( allIssues . length === 0 ) {
3155+ if ( opts . json ) {
3156+ output ( { schemaVersion : "1" , exported : 0 , message : "No issues found" } , opts ) ;
3157+ } else {
3158+ output ( "No issues found matching the query." , opts ) ;
3159+ }
3160+ return ;
3161+ }
3162+
3163+ if ( ! opts . json ) {
3164+ process . stderr . write ( `Found ${ allIssues . length } issues. Collecting data...\n` ) ;
3165+ }
3166+
3167+ // Collect export data
3168+ const exportedIssues = await collectExportData (
3169+ client ,
3170+ allIssues ,
3171+ {
3172+ format,
3173+ includeComments,
3174+ includeAttachments,
3175+ outputPath,
3176+ } ,
3177+ ( current , total , key ) => {
3178+ if ( ! opts . json ) {
3179+ process . stderr . write ( `\rProcessing ${ current } /${ total } : ${ key } ...` ) ;
3180+ }
3181+ }
3182+ ) ;
3183+
3184+ if ( ! opts . json ) {
3185+ process . stderr . write ( "\n" ) ;
3186+ }
3187+
3188+ // Build export data structure
3189+ const exportData : ExportData = {
3190+ exportedAt : new Date ( ) . toISOString ( ) ,
3191+ query : jql ,
3192+ issues : exportedIssues ,
3193+ } ;
3194+
3195+ // Write to file
3196+ await writeExportFile ( exportData , {
3197+ format,
3198+ includeComments,
3199+ includeAttachments,
3200+ outputPath,
3201+ } ) ;
3202+
3203+ if ( opts . json ) {
3204+ output ( {
3205+ schemaVersion : "1" ,
3206+ exported : exportedIssues . length ,
3207+ format,
3208+ outputPath,
3209+ includeComments,
3210+ includeAttachments,
3211+ } , opts ) ;
3212+ } else {
3213+ output ( `Exported ${ exportedIssues . length } issues to ${ outputPath } ` , opts ) ;
3214+ if ( format === "csv" && includeAttachments ) {
3215+ output ( `Attachments saved to: ${ outputPath } _attachments/` , opts ) ;
3216+ }
3217+ }
3218+ }
3219+
3220+ async function handleImport (
3221+ flags : Record < string , string | boolean > ,
3222+ opts : OutputOptions
3223+ ) : Promise < void > {
3224+ const filePath = getFlag ( flags , "file" ) ;
3225+ const project = getFlag ( flags , "project" ) ;
3226+ const dryRun = hasFlag ( flags , "dry-run" ) ;
3227+ const skipAttachments = hasFlag ( flags , "skip-attachments" ) ;
3228+
3229+ if ( ! filePath || ! project ) {
3230+ output ( importHelp ( ) , opts ) ;
3231+ return ;
3232+ }
3233+
3234+ // Parse the import file
3235+ let issues ;
3236+ try {
3237+ issues = await parseImportFile ( filePath ) ;
3238+ } catch ( err ) {
3239+ fail ( opts , 1 , ERROR_CODES . USAGE , `Failed to parse import file: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
3240+ return ;
3241+ }
3242+
3243+ if ( issues . length === 0 ) {
3244+ if ( opts . json ) {
3245+ output ( { schemaVersion : "1" , imported : 0 , message : "No issues found in file" } , opts ) ;
3246+ } else {
3247+ output ( "No issues found in the import file." , opts ) ;
3248+ }
3249+ return ;
3250+ }
3251+
3252+ if ( ! opts . json ) {
3253+ if ( dryRun ) {
3254+ process . stderr . write ( `[DRY RUN] Would import ${ issues . length } issues into project ${ project } \n` ) ;
3255+ } else {
3256+ process . stderr . write ( `Importing ${ issues . length } issues into project ${ project } ...\n` ) ;
3257+ }
3258+ }
3259+
3260+ const client = await getClient ( flags , opts ) ;
3261+
3262+ const result = await importIssues (
3263+ client ,
3264+ issues ,
3265+ { project, dryRun, skipAttachments } ,
3266+ ( current , total , summary , status ) => {
3267+ if ( ! opts . json ) {
3268+ process . stderr . write ( `\r${ dryRun ? "[DRY RUN] " : "" } ${ current } /${ total } : ${ summary . substring ( 0 , 40 ) } ...` ) ;
3269+ }
3270+ }
3271+ ) ;
3272+
3273+ if ( ! opts . json ) {
3274+ process . stderr . write ( "\n" ) ;
3275+ }
3276+
3277+ if ( opts . json ) {
3278+ output ( {
3279+ schemaVersion : "1" ,
3280+ dryRun,
3281+ total : result . total ,
3282+ created : result . created ,
3283+ skipped : result . skipped ,
3284+ failed : result . failed ,
3285+ issues : result . issues ,
3286+ } , opts ) ;
3287+ } else {
3288+ output ( `${ dryRun ? "[DRY RUN] " : "" } Import complete:` , opts ) ;
3289+ output ( ` Total: ${ result . total } ` , opts ) ;
3290+ output ( ` Created: ${ result . created } ` , opts ) ;
3291+ if ( result . skipped > 0 ) output ( ` Skipped: ${ result . skipped } ` , opts ) ;
3292+ if ( result . failed > 0 ) {
3293+ output ( ` Failed: ${ result . failed } ` , opts ) ;
3294+ for ( const issue of result . issues . filter ( ( i ) => i . status === "failed" ) ) {
3295+ output ( ` - ${ issue . summary } : ${ issue . error } ` , opts ) ;
3296+ }
3297+ }
3298+ }
3299+ }
3300+
3301+ function exportHelp ( ) : string {
3302+ return `atlcli jira export --jql <query> -o <file> [options]
3303+
3304+ Export issues to CSV or JSON with comments and attachments.
3305+
3306+ Options:
3307+ --jql <query> JQL query to select issues (required)
3308+ -o, --output <file> Output file path (required)
3309+ --format <format> Output format: json (default) or csv
3310+ --no-comments Exclude comments from export
3311+ --no-attachments Exclude attachments from export
3312+ --profile <name> Use a specific auth profile
3313+ --json JSON output for status
3314+
3315+ Examples:
3316+ jira export --jql "project = PROJ" -o issues.json
3317+ jira export --jql "assignee = currentUser()" -o my-issues.csv --format csv
3318+ jira export --jql "sprint in openSprints()" -o sprint.json --no-attachments
3319+ ` ;
3320+ }
3321+
3322+ function importHelp ( ) : string {
3323+ return `atlcli jira import --file <path> --project <key> [options]
3324+
3325+ Import issues from CSV or JSON file (create-only mode).
3326+
3327+ Options:
3328+ --file <path> Import file path (required)
3329+ --project <key> Target project key (required)
3330+ --dry-run Preview import without creating issues
3331+ --skip-attachments Skip attachment uploads
3332+ --profile <name> Use a specific auth profile
3333+ --json JSON output
3334+
3335+ Notes:
3336+ - Import creates new issues only (does not update existing)
3337+ - Issues with existing keys are skipped
3338+ - Required fields: summary, issuetype
3339+ - Comments and attachments are included if present in file
3340+
3341+ Examples:
3342+ jira import --file issues.json --project PROJ --dry-run
3343+ jira import --file backup.csv --project PERSONAL
3344+ jira import --file export.json --project PROJ --skip-attachments
3345+ ` ;
3346+ }
3347+
30983348function jiraHelp ( ) : string {
30993349 return `atlcli jira <command>
31003350
@@ -3108,6 +3358,8 @@ Commands:
31083358 analyze Sprint analytics (velocity, burndown, scope-change, predictability)
31093359 bulk Bulk operations (edit, transition, label, delete)
31103360 filter Saved JQL filters (list, get, create, update, delete, share)
3361+ export Export issues to CSV/JSON with comments and attachments
3362+ import Import issues from CSV/JSON file
31113363 search Search with JQL
31123364 me Get current user info
31133365
@@ -3121,6 +3373,8 @@ Examples:
31213373 atlcli jira filter create --name "My Issues" --jql "assignee = currentUser()"
31223374 atlcli jira bulk label add sprint-47 --jql "sprint in openSprints()"
31233375 atlcli jira analyze velocity --board 123 --sprints 5
3376+ atlcli jira export --jql "project = PROJ" -o issues.json
3377+ atlcli jira import --file issues.json --project PROJ --dry-run
31243378 atlcli jira search --project PROJ --assignee me
31253379` ;
31263380}
0 commit comments