@@ -8,7 +8,20 @@ import {
88 loadConfig ,
99 output ,
1010} from "@atlcli/core" ;
11- import { JiraClient , JiraIssue , JiraTransition , JiraSprint } from "@atlcli/jira" ;
11+ import {
12+ JiraClient ,
13+ JiraIssue ,
14+ JiraTransition ,
15+ JiraSprint ,
16+ JiraWorklog ,
17+ parseTimeToSeconds ,
18+ secondsToJiraFormat ,
19+ secondsToHuman ,
20+ roundTime ,
21+ parseRoundingInterval ,
22+ parseStartedDate ,
23+ formatWorklogDate ,
24+ } from "@atlcli/jira" ;
1225
1326export async function handleJira (
1427 args : string [ ] ,
@@ -29,6 +42,9 @@ export async function handleJira(
2942 case "sprint" :
3043 await handleSprint ( rest , flags , opts ) ;
3144 return ;
45+ case "worklog" :
46+ await handleWorklog ( rest , flags , opts ) ;
47+ return ;
3248 case "search" :
3349 await handleSearch ( rest , flags , opts ) ;
3450 return ;
@@ -849,6 +865,252 @@ Options:
849865` ;
850866}
851867
868+ // ============ Worklog Operations ============
869+
870+ async function handleWorklog (
871+ args : string [ ] ,
872+ flags : Record < string , string | boolean > ,
873+ opts : OutputOptions
874+ ) : Promise < void > {
875+ const [ sub , ...rest ] = args ;
876+ switch ( sub ) {
877+ case "add" :
878+ await handleWorklogAdd ( rest , flags , opts ) ;
879+ return ;
880+ case "list" :
881+ await handleWorklogList ( flags , opts ) ;
882+ return ;
883+ case "update" :
884+ await handleWorklogUpdate ( flags , opts ) ;
885+ return ;
886+ case "delete" :
887+ await handleWorklogDelete ( flags , opts ) ;
888+ return ;
889+ default :
890+ output ( worklogHelp ( ) , opts ) ;
891+ return ;
892+ }
893+ }
894+
895+ async function handleWorklogAdd (
896+ args : string [ ] ,
897+ flags : Record < string , string | boolean > ,
898+ opts : OutputOptions
899+ ) : Promise < void > {
900+ // Parse: jira worklog add PROJ-123 1h30m [--comment "..."] [--started "..."]
901+ const issueKey = getFlag ( flags , "issue" ) ?? args [ 0 ] ;
902+ const timeArg = args . length > 1 ? args . slice ( 1 ) . join ( " " ) : getFlag ( flags , "time" ) ;
903+
904+ if ( ! issueKey ) {
905+ fail ( opts , 1 , ERROR_CODES . USAGE , "Issue key is required." ) ;
906+ }
907+ if ( ! timeArg ) {
908+ fail ( opts , 1 , ERROR_CODES . USAGE , "Time is required (e.g., 1h30m, 1.5h, 90m)." ) ;
909+ }
910+
911+ // Parse time
912+ let timeSeconds : number ;
913+ try {
914+ timeSeconds = parseTimeToSeconds ( timeArg ) ;
915+ } catch ( e ) {
916+ fail ( opts , 1 , ERROR_CODES . USAGE , e instanceof Error ? e . message : String ( e ) ) ;
917+ }
918+
919+ // Apply rounding if specified
920+ const roundFlag = getFlag ( flags , "round" ) ;
921+ if ( roundFlag ) {
922+ try {
923+ const interval = parseRoundingInterval ( roundFlag ) ;
924+ timeSeconds = roundTime ( timeSeconds , interval ) ;
925+ } catch ( e ) {
926+ fail ( opts , 1 , ERROR_CODES . USAGE , e instanceof Error ? e . message : String ( e ) ) ;
927+ }
928+ }
929+
930+ // Parse started date if provided
931+ let started : string | undefined ;
932+ const startedFlag = getFlag ( flags , "started" ) ;
933+ if ( startedFlag ) {
934+ try {
935+ const date = parseStartedDate ( startedFlag ) ;
936+ started = formatWorklogDate ( date ) ;
937+ } catch ( e ) {
938+ fail ( opts , 1 , ERROR_CODES . USAGE , e instanceof Error ? e . message : String ( e ) ) ;
939+ }
940+ }
941+
942+ const comment = getFlag ( flags , "comment" ) ;
943+ const client = await getClient ( flags , opts ) ;
944+
945+ const worklog = await client . addWorklog ( issueKey , timeSeconds , {
946+ started,
947+ comment,
948+ } ) ;
949+
950+ output (
951+ {
952+ schemaVersion : "1" ,
953+ worklog : formatWorklog ( worklog ) ,
954+ logged : secondsToHuman ( timeSeconds ) ,
955+ } ,
956+ opts
957+ ) ;
958+ }
959+
960+ async function handleWorklogList (
961+ flags : Record < string , string | boolean > ,
962+ opts : OutputOptions
963+ ) : Promise < void > {
964+ const issueKey = getFlag ( flags , "issue" ) ;
965+ if ( ! issueKey ) {
966+ fail ( opts , 1 , ERROR_CODES . USAGE , "--issue is required." ) ;
967+ }
968+
969+ const client = await getClient ( flags , opts ) ;
970+ const limit = Number ( getFlag ( flags , "limit" ) ?? 50 ) ;
971+
972+ const result = await client . getWorklogs ( issueKey , {
973+ maxResults : Number . isNaN ( limit ) ? 50 : limit ,
974+ } ) ;
975+
976+ output (
977+ {
978+ schemaVersion : "1" ,
979+ worklogs : result . worklogs . map ( formatWorklog ) ,
980+ total : result . total ,
981+ } ,
982+ opts
983+ ) ;
984+ }
985+
986+ async function handleWorklogUpdate (
987+ flags : Record < string , string | boolean > ,
988+ opts : OutputOptions
989+ ) : Promise < void > {
990+ const issueKey = getFlag ( flags , "issue" ) ;
991+ const worklogId = getFlag ( flags , "id" ) ;
992+
993+ if ( ! issueKey || ! worklogId ) {
994+ fail ( opts , 1 , ERROR_CODES . USAGE , "--issue and --id are required." ) ;
995+ }
996+
997+ const timeArg = getFlag ( flags , "time" ) ;
998+ const comment = getFlag ( flags , "comment" ) ;
999+ const startedFlag = getFlag ( flags , "started" ) ;
1000+
1001+ if ( ! timeArg && ! comment && ! startedFlag ) {
1002+ fail ( opts , 1 , ERROR_CODES . USAGE , "At least one of --time, --comment, or --started is required." ) ;
1003+ }
1004+
1005+ const updates : {
1006+ timeSpentSeconds ?: number ;
1007+ started ?: string ;
1008+ comment ?: string ;
1009+ } = { } ;
1010+
1011+ if ( timeArg ) {
1012+ try {
1013+ updates . timeSpentSeconds = parseTimeToSeconds ( timeArg ) ;
1014+ } catch ( e ) {
1015+ fail ( opts , 1 , ERROR_CODES . USAGE , e instanceof Error ? e . message : String ( e ) ) ;
1016+ }
1017+ }
1018+
1019+ if ( startedFlag ) {
1020+ try {
1021+ updates . started = formatWorklogDate ( parseStartedDate ( startedFlag ) ) ;
1022+ } catch ( e ) {
1023+ fail ( opts , 1 , ERROR_CODES . USAGE , e instanceof Error ? e . message : String ( e ) ) ;
1024+ }
1025+ }
1026+
1027+ if ( comment ) {
1028+ updates . comment = comment ;
1029+ }
1030+
1031+ const client = await getClient ( flags , opts ) ;
1032+ const worklog = await client . updateWorklog ( issueKey , worklogId , updates ) ;
1033+
1034+ output ( { schemaVersion : "1" , worklog : formatWorklog ( worklog ) } , opts ) ;
1035+ }
1036+
1037+ async function handleWorklogDelete (
1038+ flags : Record < string , string | boolean > ,
1039+ opts : OutputOptions
1040+ ) : Promise < void > {
1041+ const issueKey = getFlag ( flags , "issue" ) ;
1042+ const worklogId = getFlag ( flags , "id" ) ;
1043+ const confirm = hasFlag ( flags , "confirm" ) ;
1044+
1045+ if ( ! issueKey || ! worklogId ) {
1046+ fail ( opts , 1 , ERROR_CODES . USAGE , "--issue and --id are required." ) ;
1047+ }
1048+ if ( ! confirm ) {
1049+ fail ( opts , 1 , ERROR_CODES . USAGE , "--confirm is required to delete a worklog." ) ;
1050+ }
1051+
1052+ const client = await getClient ( flags , opts ) ;
1053+ await client . deleteWorklog ( issueKey , worklogId ) ;
1054+
1055+ output ( { schemaVersion : "1" , deleted : { issue : issueKey , worklogId } } , opts ) ;
1056+ }
1057+
1058+ function formatWorklog ( worklog : JiraWorklog ) : Record < string , unknown > {
1059+ return {
1060+ id : worklog . id ,
1061+ issueId : worklog . issueId ,
1062+ author : worklog . author ?. displayName ,
1063+ authorId : worklog . author ?. accountId ,
1064+ timeSpent : worklog . timeSpent ,
1065+ timeSpentSeconds : worklog . timeSpentSeconds ,
1066+ timeSpentHuman : secondsToHuman ( worklog . timeSpentSeconds ) ,
1067+ started : worklog . started ,
1068+ created : worklog . created ,
1069+ updated : worklog . updated ,
1070+ comment : worklog . comment ,
1071+ } ;
1072+ }
1073+
1074+ function worklogHelp ( ) : string {
1075+ return `atlcli jira worklog <command>
1076+
1077+ Commands:
1078+ add <issue> <time> [--comment <text>] [--started <date>] [--round <interval>]
1079+ list --issue <key> [--limit <n>]
1080+ update --issue <key> --id <worklogId> [--time <time>] [--comment <text>] [--started <date>]
1081+ delete --issue <key> --id <worklogId> --confirm
1082+
1083+ Time formats:
1084+ 1h30m, 1h 30m Hours and minutes
1085+ 1.5h, 2.25h Decimal hours
1086+ 90m, 45m Minutes only
1087+ 1:30, 2:45 HH:MM format
1088+ 1d, 2d Days (8h each)
1089+ 1w Weeks (5d each)
1090+
1091+ Started date formats:
1092+ today, yesterday Relative dates
1093+ 14:30 Time today
1094+ 2026-01-12 Date (current time)
1095+ ISO 8601 Full datetime
1096+
1097+ Rounding:
1098+ --round 15m Round to nearest 15 minutes
1099+ --round 30m Round to nearest 30 minutes
1100+ --round 1h Round to nearest hour
1101+
1102+ Options:
1103+ --profile <name> Use a specific auth profile
1104+ --json JSON output
1105+
1106+ Examples:
1107+ jira worklog add PROJ-123 1h30m --comment "Feature work"
1108+ jira worklog add PROJ-123 1.5h --started 09:00
1109+ jira worklog add PROJ-123 2h --round 15m
1110+ jira worklog list --issue PROJ-123
1111+ ` ;
1112+ }
1113+
8521114// ============ Search (JQL) ============
8531115
8541116async function handleSearch (
@@ -936,6 +1198,7 @@ Commands:
9361198 issue Issue operations (get, create, update, delete, transition, comment, link)
9371199 board Board operations (list, get, backlog, issues)
9381200 sprint Sprint operations (list, get, create, start, close, add, remove)
1201+ worklog Time tracking (add, list, update, delete)
9391202 search Search with JQL
9401203 me Get current user info
9411204
@@ -946,10 +1209,10 @@ Options:
9461209Examples:
9471210 atlcli jira project list
9481211 atlcli jira issue create --project PROJ --type Task --summary "My task"
1212+ atlcli jira worklog add PROJ-123 1h30m --comment "Feature work"
1213+ atlcli jira worklog add PROJ-123 1.5h --round 15m
9491214 atlcli jira board list --project PROJ
9501215 atlcli jira sprint list --board 123
951- atlcli jira sprint create --board 123 --name "Sprint 1"
952- atlcli jira sprint add PROJ-1 PROJ-2 --sprint 456
9531216 atlcli jira search --project PROJ --assignee me
9541217` ;
9551218}
0 commit comments