@liebig-technology/clockodo-cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/index.js +201 -34
- package/package.json +11 -13
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Commands:
|
|
|
17
17
|
status Show running clock and today's summary
|
|
18
18
|
start Start time tracking
|
|
19
19
|
stop Stop time tracking
|
|
20
|
+
edit Edit the running clock entry
|
|
21
|
+
extend Extend the running clock by N minutes
|
|
20
22
|
entries Manage time entries
|
|
21
23
|
customers Manage customers
|
|
22
24
|
projects Manage projects
|
|
@@ -54,18 +56,23 @@ Credentials are stored in `~/.config/clockodo-cli/config.json`. Environment vari
|
|
|
54
56
|
```bash
|
|
55
57
|
# Time tracking
|
|
56
58
|
clockodo start --customer 123 --service 456 --text "Working on feature"
|
|
59
|
+
clockodo start --customer 123 --service 456 --billable # explicit billability
|
|
57
60
|
clockodo stop
|
|
58
61
|
clockodo status
|
|
62
|
+
clockodo edit --text "New description" --billable # edit running entry
|
|
63
|
+
clockodo extend 30 # extend running clock by 30 minutes
|
|
59
64
|
|
|
60
65
|
# Entries
|
|
61
66
|
clockodo entries # list today's entries
|
|
62
67
|
clockodo entries --since 2026-01-01 --until 2026-01-31 # date range
|
|
68
|
+
clockodo entries --billable # only billable entries
|
|
63
69
|
clockodo entries create --from "09:00" --to "12:30" --customer 123 --service 456
|
|
70
|
+
clockodo entries update 42 --text "New description" --customer 789 --service 456
|
|
64
71
|
|
|
65
72
|
# Reports
|
|
66
|
-
clockodo report
|
|
67
|
-
clockodo report week
|
|
68
|
-
clockodo report month
|
|
73
|
+
clockodo report # today, grouped by project
|
|
74
|
+
clockodo report week --group customer # this week by customer
|
|
75
|
+
clockodo report month --group text # this month by description
|
|
69
76
|
|
|
70
77
|
# Absences, work times, user reports
|
|
71
78
|
clockodo absences list --year 2026
|
package/dist/index.js
CHANGED
|
@@ -458,7 +458,7 @@ function registerAbsencesCommands(program2) {
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
// src/commands/clock.ts
|
|
461
|
-
import { Billability } from "clockodo";
|
|
461
|
+
import { Billability, getEntryDurationUntilNow } from "clockodo";
|
|
462
462
|
|
|
463
463
|
// src/lib/prompts.ts
|
|
464
464
|
import * as p2 from "@clack/prompts";
|
|
@@ -595,7 +595,7 @@ function parseDateTime(input) {
|
|
|
595
595
|
|
|
596
596
|
// src/commands/clock.ts
|
|
597
597
|
function registerClockCommands(program2) {
|
|
598
|
-
program2.command("start").description("Start time tracking").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").action(async (cmdOpts) => {
|
|
598
|
+
program2.command("start").description("Start time tracking").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (cmdOpts) => {
|
|
599
599
|
const opts = program2.opts();
|
|
600
600
|
const mode = resolveOutputMode(opts);
|
|
601
601
|
const client = getClient();
|
|
@@ -619,9 +619,11 @@ function registerClockCommands(program2) {
|
|
|
619
619
|
const result = await client.startClock({
|
|
620
620
|
customersId,
|
|
621
621
|
servicesId,
|
|
622
|
+
...cmdOpts.billable !== void 0 && {
|
|
623
|
+
billable: cmdOpts.billable ? Billability.Billable : Billability.NotBillable
|
|
624
|
+
},
|
|
622
625
|
...projectsId && { projectsId },
|
|
623
|
-
...cmdOpts.text && { text: cmdOpts.text }
|
|
624
|
-
...cmdOpts.billable && { billable: Billability.Billable }
|
|
626
|
+
...cmdOpts.text && { text: cmdOpts.text }
|
|
625
627
|
});
|
|
626
628
|
if (mode !== "human") {
|
|
627
629
|
printResult({ data: result.running }, opts);
|
|
@@ -652,6 +654,57 @@ function registerClockCommands(program2) {
|
|
|
652
654
|
console.log(` Description: ${result.stopped.text}`);
|
|
653
655
|
}
|
|
654
656
|
});
|
|
657
|
+
program2.command("edit").description("Edit the running clock entry").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (cmdOpts) => {
|
|
658
|
+
const opts = program2.opts();
|
|
659
|
+
const client = getClient();
|
|
660
|
+
const clock = await client.getClock();
|
|
661
|
+
if (!clock.running) {
|
|
662
|
+
throw new CliError("No clock is currently running.", ExitCode.EMPTY_RESULTS);
|
|
663
|
+
}
|
|
664
|
+
const updates = { id: clock.running.id };
|
|
665
|
+
if (cmdOpts.customer !== void 0) updates.customersId = cmdOpts.customer;
|
|
666
|
+
if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
|
|
667
|
+
if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
|
|
668
|
+
if (cmdOpts.text !== void 0) updates.text = cmdOpts.text;
|
|
669
|
+
if (cmdOpts.billable !== void 0)
|
|
670
|
+
updates.billable = cmdOpts.billable ? Billability.Billable : Billability.NotBillable;
|
|
671
|
+
if (Object.keys(updates).length === 1) {
|
|
672
|
+
throw new CliError(
|
|
673
|
+
"No update flags provided.",
|
|
674
|
+
ExitCode.INVALID_ARGS,
|
|
675
|
+
"Use --text, --customer, --project, --service, --billable, or --no-billable"
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const result = await client.editEntry(updates);
|
|
679
|
+
const mode = resolveOutputMode(opts);
|
|
680
|
+
if (mode !== "human") {
|
|
681
|
+
printResult({ data: result.entry }, opts);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
printSuccess("Running entry updated");
|
|
685
|
+
});
|
|
686
|
+
program2.command("extend").description("Extend the running clock by N minutes").argument("<minutes>", "Minutes to extend by", parseIntStrict).action(async (minutes) => {
|
|
687
|
+
const opts = program2.opts();
|
|
688
|
+
const client = getClient();
|
|
689
|
+
const clock = await client.getClock();
|
|
690
|
+
if (!clock.running) {
|
|
691
|
+
throw new CliError("No clock is currently running.", ExitCode.EMPTY_RESULTS);
|
|
692
|
+
}
|
|
693
|
+
const durationBefore = getEntryDurationUntilNow(clock.running);
|
|
694
|
+
const duration = durationBefore + minutes * 60;
|
|
695
|
+
const result = await client.changeClockDuration({
|
|
696
|
+
entriesId: clock.running.id,
|
|
697
|
+
durationBefore,
|
|
698
|
+
duration
|
|
699
|
+
});
|
|
700
|
+
const mode = resolveOutputMode(opts);
|
|
701
|
+
if (mode !== "human") {
|
|
702
|
+
printResult({ data: result.updated }, opts);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
printSuccess(`Clock extended by ${minutes} minutes`);
|
|
706
|
+
console.log(` New duration: ${formatDuration(duration)}`);
|
|
707
|
+
});
|
|
655
708
|
}
|
|
656
709
|
|
|
657
710
|
// src/commands/config.ts
|
|
@@ -764,10 +817,10 @@ function registerCustomersCommands(program2) {
|
|
|
764
817
|
// src/commands/entries.ts
|
|
765
818
|
import { styleText as styleText4 } from "util";
|
|
766
819
|
import * as p4 from "@clack/prompts";
|
|
767
|
-
import { getEntryDurationUntilNow, isTimeEntry } from "clockodo";
|
|
820
|
+
import { Billability as Billability2, getEntryDurationUntilNow as getEntryDurationUntilNow2, isTimeEntry } from "clockodo";
|
|
768
821
|
function registerEntriesCommands(program2) {
|
|
769
822
|
const entries = program2.command("entries").description("Manage time entries");
|
|
770
|
-
entries.command("list", { isDefault: true }).description("List time entries").option("--since <date>", "Start date (default: today)", "today").option("--until <date>", "End date (default: today)").option("--customer <id>", "Filter by customer ID", parseIntStrict).option("--project <id>", "Filter by project ID", parseIntStrict).option("--service <id>", "Filter by service ID", parseIntStrict).option("--text <search>", "Filter by description text").option(
|
|
823
|
+
entries.command("list", { isDefault: true }).description("List time entries").option("--since <date>", "Start date (default: today)", "today").option("--until <date>", "End date (default: today)").option("--customer <id>", "Filter by customer ID", parseIntStrict).option("--project <id>", "Filter by project ID", parseIntStrict).option("--service <id>", "Filter by service ID", parseIntStrict).option("--text <search>", "Filter by description text").option("-b, --billable", "Show only billable entries").option("--no-billable", "Show only non-billable entries").option(
|
|
771
824
|
"-g, --group <field>",
|
|
772
825
|
"Group by: customer, project, service, text (shows summary table instead)"
|
|
773
826
|
).action(async (cmdOpts) => {
|
|
@@ -780,6 +833,8 @@ function registerEntriesCommands(program2) {
|
|
|
780
833
|
if (cmdOpts.project) filter.projectsId = cmdOpts.project;
|
|
781
834
|
if (cmdOpts.service) filter.servicesId = cmdOpts.service;
|
|
782
835
|
if (cmdOpts.text) filter.text = cmdOpts.text;
|
|
836
|
+
if (cmdOpts.billable !== void 0)
|
|
837
|
+
filter.billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
|
|
783
838
|
const result = await client.getEntries({
|
|
784
839
|
timeSince: since,
|
|
785
840
|
timeUntil: until,
|
|
@@ -795,7 +850,7 @@ function registerEntriesCommands(program2) {
|
|
|
795
850
|
}
|
|
796
851
|
return;
|
|
797
852
|
}
|
|
798
|
-
const totalSeconds = entryList.reduce((sum, e) => sum +
|
|
853
|
+
const totalSeconds = entryList.reduce((sum, e) => sum + getEntryDurationUntilNow2(e), 0);
|
|
799
854
|
if (cmdOpts.group) {
|
|
800
855
|
const groupKey = resolveGroupKey(cmdOpts.group);
|
|
801
856
|
const groups = groupEntries(entryList, groupKey);
|
|
@@ -841,7 +896,7 @@ function registerEntriesCommands(program2) {
|
|
|
841
896
|
formatDate(new Date(e.timeSince)),
|
|
842
897
|
formatTime(e.timeSince),
|
|
843
898
|
isTimeEntry(e) && !e.timeUntil ? styleText4("green", "running") : formatTime(e.timeUntil ?? e.timeSince),
|
|
844
|
-
formatDuration(
|
|
899
|
+
formatDuration(getEntryDurationUntilNow2(e)),
|
|
845
900
|
e.text || styleText4("dim", "\u2014")
|
|
846
901
|
]);
|
|
847
902
|
printTable(["ID", "Date", "Start", "End", "Duration", "Description"], rows, opts);
|
|
@@ -862,7 +917,7 @@ function registerEntriesCommands(program2) {
|
|
|
862
917
|
return;
|
|
863
918
|
}
|
|
864
919
|
const timeUntilDisplay = isTimeEntry(e) && !e.timeUntil ? "running" : formatTime(e.timeUntil ?? e.timeSince);
|
|
865
|
-
const duration =
|
|
920
|
+
const duration = getEntryDurationUntilNow2(e);
|
|
866
921
|
printDetail(
|
|
867
922
|
[
|
|
868
923
|
["ID", e.id],
|
|
@@ -874,12 +929,12 @@ function registerEntriesCommands(program2) {
|
|
|
874
929
|
["Customer ID", e.customersId],
|
|
875
930
|
["Project ID", e.projectsId ?? null],
|
|
876
931
|
["Service ID", isTimeEntry(e) ? e.servicesId : null],
|
|
877
|
-
["Billable", e.billable
|
|
932
|
+
["Billable", formatBillable(e.billable)]
|
|
878
933
|
],
|
|
879
934
|
opts
|
|
880
935
|
);
|
|
881
936
|
});
|
|
882
|
-
entries.command("create").description("Create a time entry").requiredOption("--from <datetime>", "Start time (e.g., '2024-01-15 09:00' or '09:00')").requiredOption("--to <datetime>", "End time (e.g., '2024-01-15 17:00' or '17:00')").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").action(async (cmdOpts) => {
|
|
937
|
+
entries.command("create").description("Create a time entry").requiredOption("--from <datetime>", "Start time (e.g., '2024-01-15 09:00' or '09:00')").requiredOption("--to <datetime>", "End time (e.g., '2024-01-15 17:00' or '17:00')").option("-c, --customer <id>", "Customer ID", parseIntStrict).option("-p, --project <id>", "Project ID", parseIntStrict).option("-s, --service <id>", "Service ID", parseIntStrict).option("-t, --text <description>", "Entry description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (cmdOpts) => {
|
|
883
938
|
const opts = program2.opts();
|
|
884
939
|
const mode = resolveOutputMode(opts);
|
|
885
940
|
const client = getClient();
|
|
@@ -900,10 +955,20 @@ function registerEntriesCommands(program2) {
|
|
|
900
955
|
"Use --customer and --service flags, or set defaults via: clockodo config set"
|
|
901
956
|
);
|
|
902
957
|
}
|
|
958
|
+
let billable;
|
|
959
|
+
if (cmdOpts.billable !== void 0) {
|
|
960
|
+
billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
|
|
961
|
+
} else if (projectsId) {
|
|
962
|
+
const { data: project } = await client.getProject({ id: projectsId });
|
|
963
|
+
billable = project.billableDefault ? Billability2.Billable : Billability2.NotBillable;
|
|
964
|
+
} else {
|
|
965
|
+
const { data: customer } = await client.getCustomer({ id: customersId });
|
|
966
|
+
billable = customer.billableDefault ? Billability2.Billable : Billability2.NotBillable;
|
|
967
|
+
}
|
|
903
968
|
const result = await client.addEntry({
|
|
904
969
|
customersId,
|
|
905
970
|
servicesId,
|
|
906
|
-
billable
|
|
971
|
+
billable,
|
|
907
972
|
timeSince: parseDateTime(cmdOpts.from),
|
|
908
973
|
timeUntil: parseDateTime(cmdOpts.to),
|
|
909
974
|
...projectsId && { projectsId },
|
|
@@ -914,20 +979,24 @@ function registerEntriesCommands(program2) {
|
|
|
914
979
|
return;
|
|
915
980
|
}
|
|
916
981
|
const entry = result.entry;
|
|
917
|
-
const duration =
|
|
982
|
+
const duration = getEntryDurationUntilNow2(entry);
|
|
918
983
|
printSuccess(`Entry created (ID: ${entry.id})`);
|
|
919
984
|
console.log(
|
|
920
985
|
` ${formatTime(entry.timeSince)} \u2014 ${entry.timeUntil ? formatTime(entry.timeUntil) : "?"} (${formatDuration(duration)})`
|
|
921
986
|
);
|
|
922
987
|
});
|
|
923
|
-
entries.command("update <id>").description("Update a time entry").option("--from <datetime>", "New start time").option("--to <datetime>", "New end time").option("-t, --text <description>", "New description").option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (id, cmdOpts) => {
|
|
988
|
+
entries.command("update <id>").description("Update a time entry").option("--from <datetime>", "New start time").option("--to <datetime>", "New end time").option("-t, --text <description>", "New description").option("-c, --customer <id>", "New customer ID", parseIntStrict).option("--project <id>", "New project ID", parseIntStrict).option("-s, --service <id>", "New service ID", parseIntStrict).option("-b, --billable", "Mark as billable").option("--no-billable", "Mark as not billable").action(async (id, cmdOpts) => {
|
|
924
989
|
const opts = program2.opts();
|
|
925
990
|
const client = getClient();
|
|
926
991
|
const updates = { id: parseId(id) };
|
|
927
992
|
if (cmdOpts.from) updates.timeSince = parseDateTime(cmdOpts.from);
|
|
928
993
|
if (cmdOpts.to) updates.timeUntil = parseDateTime(cmdOpts.to);
|
|
929
994
|
if (cmdOpts.text !== void 0) updates.text = cmdOpts.text;
|
|
930
|
-
if (cmdOpts.
|
|
995
|
+
if (cmdOpts.customer !== void 0) updates.customersId = cmdOpts.customer;
|
|
996
|
+
if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
|
|
997
|
+
if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
|
|
998
|
+
if (cmdOpts.billable !== void 0)
|
|
999
|
+
updates.billable = cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable;
|
|
931
1000
|
const result = await client.editEntry(updates);
|
|
932
1001
|
const mode = resolveOutputMode(opts);
|
|
933
1002
|
if (mode !== "human") {
|
|
@@ -989,7 +1058,7 @@ function groupEntries(entries, key) {
|
|
|
989
1058
|
groupValue = val != null ? String(val) : "(none)";
|
|
990
1059
|
}
|
|
991
1060
|
const existing = map.get(groupValue);
|
|
992
|
-
const duration =
|
|
1061
|
+
const duration = getEntryDurationUntilNow2(e);
|
|
993
1062
|
if (existing) {
|
|
994
1063
|
existing.count++;
|
|
995
1064
|
existing.seconds += duration;
|
|
@@ -999,6 +1068,16 @@ function groupEntries(entries, key) {
|
|
|
999
1068
|
}
|
|
1000
1069
|
return [...map.entries()].map(([k, v]) => ({ key: k, ...v })).sort((a, b) => b.seconds - a.seconds);
|
|
1001
1070
|
}
|
|
1071
|
+
function formatBillable(value) {
|
|
1072
|
+
switch (value) {
|
|
1073
|
+
case Billability2.Billable:
|
|
1074
|
+
return "Yes";
|
|
1075
|
+
case Billability2.Billed:
|
|
1076
|
+
return "Billed";
|
|
1077
|
+
default:
|
|
1078
|
+
return "No";
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1002
1081
|
|
|
1003
1082
|
// src/commands/projects.ts
|
|
1004
1083
|
function registerProjectsCommands(program2) {
|
|
@@ -1058,14 +1137,112 @@ function registerProjectsCommands(program2) {
|
|
|
1058
1137
|
|
|
1059
1138
|
// src/commands/report.ts
|
|
1060
1139
|
import { styleText as styleText5 } from "util";
|
|
1140
|
+
import { getEntryDurationUntilNow as getEntryDurationUntilNow3 } from "clockodo";
|
|
1141
|
+
var GROUP_ALIASES = {
|
|
1142
|
+
customer: "customers_id",
|
|
1143
|
+
customers: "customers_id",
|
|
1144
|
+
customers_id: "customers_id",
|
|
1145
|
+
project: "projects_id",
|
|
1146
|
+
projects: "projects_id",
|
|
1147
|
+
projects_id: "projects_id",
|
|
1148
|
+
service: "services_id",
|
|
1149
|
+
services: "services_id",
|
|
1150
|
+
services_id: "services_id",
|
|
1151
|
+
text: "text",
|
|
1152
|
+
description: "text"
|
|
1153
|
+
};
|
|
1154
|
+
function resolveReportGroupKey(input) {
|
|
1155
|
+
const resolved = GROUP_ALIASES[input.toLowerCase()];
|
|
1156
|
+
if (!resolved) {
|
|
1157
|
+
throw new CliError(
|
|
1158
|
+
`Unknown group field: "${input}". Valid options: customer, project, service, text`,
|
|
1159
|
+
ExitCode.INVALID_ARGS
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
return resolved;
|
|
1163
|
+
}
|
|
1164
|
+
function groupEntriesByText(entries) {
|
|
1165
|
+
const map = /* @__PURE__ */ new Map();
|
|
1166
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1167
|
+
for (const e of entries) {
|
|
1168
|
+
const groupKey = e.text || "(no description)";
|
|
1169
|
+
const duration = getEntryDurationUntilNow3(e);
|
|
1170
|
+
const existing = map.get(groupKey);
|
|
1171
|
+
const timeRange = { since: e.timeSince, until: e.timeUntil ?? now };
|
|
1172
|
+
if (existing) {
|
|
1173
|
+
existing.count++;
|
|
1174
|
+
existing.seconds += duration;
|
|
1175
|
+
existing.timeRanges.push(timeRange);
|
|
1176
|
+
} else {
|
|
1177
|
+
map.set(groupKey, {
|
|
1178
|
+
key: groupKey,
|
|
1179
|
+
count: 1,
|
|
1180
|
+
seconds: duration,
|
|
1181
|
+
timeRanges: [timeRange]
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return [...map.values()].sort((a, b) => b.seconds - a.seconds);
|
|
1186
|
+
}
|
|
1187
|
+
async function runTextReport(program2, since, until) {
|
|
1188
|
+
const opts = program2.opts();
|
|
1189
|
+
const client = getClient();
|
|
1190
|
+
const result = await client.getEntries({
|
|
1191
|
+
timeSince: toClockodoDateTime(since),
|
|
1192
|
+
timeUntil: toClockodoDateTime(until)
|
|
1193
|
+
});
|
|
1194
|
+
const entryList = result.entries ?? [];
|
|
1195
|
+
const groups = groupEntriesByText(entryList);
|
|
1196
|
+
const totalSeconds = groups.reduce((sum, g) => sum + g.seconds, 0);
|
|
1197
|
+
const mode = resolveOutputMode(opts);
|
|
1198
|
+
if (mode !== "human") {
|
|
1199
|
+
printResult(
|
|
1200
|
+
{
|
|
1201
|
+
data: {
|
|
1202
|
+
period: { since: formatDate(since), until: formatDate(until) },
|
|
1203
|
+
groups,
|
|
1204
|
+
total: { seconds: totalSeconds, formatted: formatDuration(totalSeconds) }
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
opts
|
|
1208
|
+
);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
printReportHeader(since, until);
|
|
1212
|
+
if (groups.length === 0) {
|
|
1213
|
+
console.log(styleText5("dim", " No entries found for this period."));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const rows = groups.map((g) => {
|
|
1217
|
+
const ranges = g.timeRanges.map((r) => `${formatTime(r.since)}\u2013${formatTime(r.until)}`).join(", ");
|
|
1218
|
+
return [g.key, formatDuration(g.seconds), formatDecimalHours(g.seconds), ranges];
|
|
1219
|
+
});
|
|
1220
|
+
printTable(["Description", "Duration", "Hours", "Time Ranges"], rows, opts);
|
|
1221
|
+
printReportTotal(totalSeconds);
|
|
1222
|
+
}
|
|
1223
|
+
function printReportHeader(since, until) {
|
|
1224
|
+
console.log();
|
|
1225
|
+
console.log(` ${styleText5("bold", "Report")}: ${formatDate(since)} \u2014 ${formatDate(until)}`);
|
|
1226
|
+
console.log();
|
|
1227
|
+
}
|
|
1228
|
+
function printReportTotal(totalSeconds) {
|
|
1229
|
+
console.log();
|
|
1230
|
+
console.log(
|
|
1231
|
+
` ${styleText5("bold", "Total")}: ${formatDuration(totalSeconds)} (${formatDecimalHours(totalSeconds)})`
|
|
1232
|
+
);
|
|
1233
|
+
console.log();
|
|
1234
|
+
}
|
|
1061
1235
|
async function runReport(program2, since, until, cmdOpts) {
|
|
1236
|
+
const groupField = resolveReportGroupKey(cmdOpts.group ?? "projects_id");
|
|
1237
|
+
if (groupField === "text") {
|
|
1238
|
+
return runTextReport(program2, since, until);
|
|
1239
|
+
}
|
|
1062
1240
|
const opts = program2.opts();
|
|
1063
1241
|
const client = getClient();
|
|
1064
|
-
const grouping = [cmdOpts.group ?? "projects_id"];
|
|
1065
1242
|
const result = await client.getEntryGroups({
|
|
1066
1243
|
timeSince: toClockodoDateTime(since),
|
|
1067
1244
|
timeUntil: toClockodoDateTime(until),
|
|
1068
|
-
grouping
|
|
1245
|
+
grouping: [groupField]
|
|
1069
1246
|
});
|
|
1070
1247
|
const groups = result.groups ?? [];
|
|
1071
1248
|
const totalSeconds = groups.reduce((sum, g) => sum + (g.duration ?? 0), 0);
|
|
@@ -1083,9 +1260,7 @@ async function runReport(program2, since, until, cmdOpts) {
|
|
|
1083
1260
|
);
|
|
1084
1261
|
return;
|
|
1085
1262
|
}
|
|
1086
|
-
|
|
1087
|
-
console.log(` ${styleText5("bold", "Report")}: ${formatDate(since)} \u2014 ${formatDate(until)}`);
|
|
1088
|
-
console.log();
|
|
1263
|
+
printReportHeader(since, until);
|
|
1089
1264
|
if (groups.length === 0) {
|
|
1090
1265
|
console.log(styleText5("dim", " No entries found for this period."));
|
|
1091
1266
|
return;
|
|
@@ -1096,31 +1271,23 @@ async function runReport(program2, since, until, cmdOpts) {
|
|
|
1096
1271
|
formatDecimalHours(g.duration ?? 0)
|
|
1097
1272
|
]);
|
|
1098
1273
|
printTable(["Name", "Duration", "Hours"], rows, opts);
|
|
1099
|
-
|
|
1100
|
-
console.log(
|
|
1101
|
-
` ${styleText5("bold", "Total")}: ${formatDuration(totalSeconds)} (${formatDecimalHours(totalSeconds)})`
|
|
1102
|
-
);
|
|
1103
|
-
console.log();
|
|
1274
|
+
printReportTotal(totalSeconds);
|
|
1104
1275
|
}
|
|
1105
1276
|
function registerReportCommands(program2) {
|
|
1106
1277
|
const report = program2.command("report").description("Aggregated time reports");
|
|
1107
|
-
report.command("today", { isDefault: true }).description("Today's summary").option(
|
|
1108
|
-
"-g, --group <field>",
|
|
1109
|
-
"Group by: projects_id, customers_id, services_id",
|
|
1110
|
-
"projects_id"
|
|
1111
|
-
).action(async (cmdOpts) => {
|
|
1278
|
+
report.command("today", { isDefault: true }).description("Today's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
|
|
1112
1279
|
const now = /* @__PURE__ */ new Date();
|
|
1113
1280
|
await runReport(program2, startOfDay(now), endOfDay(now), cmdOpts);
|
|
1114
1281
|
});
|
|
1115
|
-
report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by
|
|
1282
|
+
report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
|
|
1116
1283
|
const now = /* @__PURE__ */ new Date();
|
|
1117
1284
|
await runReport(program2, startOfWeek(now), endOfWeek(now), cmdOpts);
|
|
1118
1285
|
});
|
|
1119
|
-
report.command("month").description("This month's summary").option("-g, --group <field>", "Group by
|
|
1286
|
+
report.command("month").description("This month's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
|
|
1120
1287
|
const now = /* @__PURE__ */ new Date();
|
|
1121
1288
|
await runReport(program2, startOfMonth(now), endOfMonth(now), cmdOpts);
|
|
1122
1289
|
});
|
|
1123
|
-
report.command("custom").description("Custom date range report").requiredOption("--since <date>", "Start date").requiredOption("--until <date>", "End date").option("-g, --group <field>", "Group by
|
|
1290
|
+
report.command("custom").description("Custom date range report").requiredOption("--since <date>", "Start date").requiredOption("--until <date>", "End date").option("-g, --group <field>", "Group by: customer, project, service, text", "projects_id").action(async (cmdOpts) => {
|
|
1124
1291
|
const since = new Date(parseDateTime(cmdOpts.since));
|
|
1125
1292
|
const until = new Date(parseDateTime(cmdOpts.until));
|
|
1126
1293
|
await runReport(program2, since, until, cmdOpts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liebig-technology/clockodo-cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "AI-friendly CLI for the Clockodo time tracking API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,17 +13,6 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=22.0.0"
|
|
15
15
|
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsup",
|
|
18
|
-
"dev": "tsx src/index.ts",
|
|
19
|
-
"typecheck": "tsc --noEmit",
|
|
20
|
-
"lint": "biome check .",
|
|
21
|
-
"lint:fix": "biome check --write .",
|
|
22
|
-
"test": "vitest run",
|
|
23
|
-
"test:watch": "vitest",
|
|
24
|
-
"prepublishOnly": "pnpm build",
|
|
25
|
-
"prepare": "husky"
|
|
26
|
-
},
|
|
27
16
|
"keywords": [
|
|
28
17
|
"clockodo",
|
|
29
18
|
"time-tracking",
|
|
@@ -62,5 +51,14 @@
|
|
|
62
51
|
"*": [
|
|
63
52
|
"biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
|
|
64
53
|
]
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"dev": "tsx src/index.ts",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"lint": "biome check .",
|
|
60
|
+
"lint:fix": "biome check --write .",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest"
|
|
65
63
|
}
|
|
66
|
-
}
|
|
64
|
+
}
|