Skip to content

Commit 5979c82

Browse files
BjoernSchotteclaude
andcommitted
feat(jira): add time tracking Phase 1 - worklog CRUD
Add worklog management with natural time input parsing: - Supports multiple time formats: 1h30m, 1.5h, 90m, 1:30, 1d, 1w - Started date parsing: today, yesterday, HH:MM, YYYY-MM-DD - Rounding to nearest interval (15m, 30m, 1h) - Full CRUD operations: add, list, update, delete Commands: jira worklog add <issue> <time> [--comment] [--started] [--round] jira worklog list --issue <key> jira worklog update --issue <key> --id <id> [--time] [--comment] jira worklog delete --issue <key> --id <id> --confirm Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 19f8deb commit 5979c82

5 files changed

Lines changed: 719 additions & 12 deletions

File tree

‎apps/cli/src/commands/jira.ts‎

Lines changed: 266 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1326
export 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

8541116
async 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:
9461209
Examples:
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

Comments
 (0)