@liebig-technology/clockodo-cli 0.2.1 → 0.3.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 +50 -22
- package/dist/index.js +313 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,30 +6,31 @@ CLI for the [Clockodo](https://www.clockodo.com) time tracking API. Human-friend
|
|
|
6
6
|
Usage: clockodo [options] [command]
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
--no
|
|
12
|
-
--no-
|
|
13
|
-
-
|
|
9
|
+
-V, --version output the version number
|
|
10
|
+
-j, --json Output as JSON
|
|
11
|
+
-p, --plain Output as plain text (no colors)
|
|
12
|
+
--no-color Disable colors
|
|
13
|
+
--no-input Disable interactive prompts
|
|
14
|
+
-v, --verbose Verbose output
|
|
14
15
|
|
|
15
16
|
Commands:
|
|
16
|
-
config
|
|
17
|
-
status
|
|
18
|
-
start
|
|
19
|
-
stop
|
|
20
|
-
edit
|
|
21
|
-
extend
|
|
22
|
-
entries
|
|
23
|
-
customers
|
|
24
|
-
projects
|
|
25
|
-
services
|
|
26
|
-
users
|
|
27
|
-
report
|
|
28
|
-
absences
|
|
29
|
-
worktimes
|
|
30
|
-
userreport
|
|
31
|
-
userreports
|
|
32
|
-
schema
|
|
17
|
+
config Manage CLI configuration
|
|
18
|
+
status [options] Show running clock and today's summary
|
|
19
|
+
start [options] Start time tracking
|
|
20
|
+
stop Stop time tracking
|
|
21
|
+
edit [options] Edit the running clock entry
|
|
22
|
+
extend <minutes> Extend the running clock by N minutes
|
|
23
|
+
entries Manage time entries
|
|
24
|
+
customers Manage customers
|
|
25
|
+
projects Manage projects
|
|
26
|
+
services Manage services
|
|
27
|
+
users User management
|
|
28
|
+
report Aggregated time reports
|
|
29
|
+
absences Manage absences
|
|
30
|
+
worktimes [options] Show work time intervals
|
|
31
|
+
userreport [options] Show user report (overtime, holidays, absences)
|
|
32
|
+
userreports [options] Show user reports for all users
|
|
33
|
+
schema Output machine-readable CLI structure (for AI agents)
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
Every command supports `--help` for full usage details.
|
|
@@ -69,6 +70,14 @@ clockodo entries --billable # only billable entries
|
|
|
69
70
|
clockodo entries create --from "09:00" --to "12:30" --customer 123 --service 456
|
|
70
71
|
clockodo entries update 42 --text "New description" --customer 789 --service 456
|
|
71
72
|
|
|
73
|
+
# Customers, projects, services
|
|
74
|
+
clockodo customers # list all customers
|
|
75
|
+
clockodo customers create --name "Acme Corp" # create a customer
|
|
76
|
+
clockodo customers update 42 --name "Acme Inc" # rename customer 42
|
|
77
|
+
clockodo customers delete 42 --force # delete without confirmation
|
|
78
|
+
clockodo projects create --name "Website" --customer 42
|
|
79
|
+
clockodo services create --name "Development"
|
|
80
|
+
|
|
72
81
|
# Reports
|
|
73
82
|
clockodo report # today, grouped by project
|
|
74
83
|
clockodo report week --group customer # this week by customer
|
|
@@ -82,6 +91,25 @@ clockodo userreport --year 2026
|
|
|
82
91
|
|
|
83
92
|
When `--customer` or `--service` is not provided, `start` and `entries create` launch an interactive picker (disable with `--no-input`). You can also set defaults via `clockodo config set`.
|
|
84
93
|
|
|
94
|
+
## Shell Prompt Integration
|
|
95
|
+
|
|
96
|
+
`clockodo status --prompt` outputs a single plain-text line for embedding in your terminal prompt:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
clockodo status --prompt # "Working on feature 1h 23m" or empty if idle
|
|
100
|
+
clockodo status --prompt --json # {"data":{"running":true,"text":"...","elapsed":4980,"formatted":"1h 23m"}}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Example for [Starship](https://starship.rs/):
|
|
104
|
+
|
|
105
|
+
```toml
|
|
106
|
+
# ~/.config/starship.toml
|
|
107
|
+
[custom.clockodo]
|
|
108
|
+
command = "clockodo status --prompt"
|
|
109
|
+
when = true
|
|
110
|
+
symbol = "⏱ "
|
|
111
|
+
```
|
|
112
|
+
|
|
85
113
|
## AI Agent Integration
|
|
86
114
|
|
|
87
115
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -290,6 +290,9 @@ function printDetail(entries, options) {
|
|
|
290
290
|
function printSuccess(message) {
|
|
291
291
|
console.error(styleText2("green", `\u2713 ${message}`));
|
|
292
292
|
}
|
|
293
|
+
function printInfo(message) {
|
|
294
|
+
console.error(styleText2("cyan", `\u2139 ${message}`));
|
|
295
|
+
}
|
|
293
296
|
|
|
294
297
|
// src/lib/validate.ts
|
|
295
298
|
function parseId(input, label = "ID") {
|
|
@@ -441,10 +444,10 @@ function registerAbsencesCommands(program2) {
|
|
|
441
444
|
const opts = program2.opts();
|
|
442
445
|
const client = getClient();
|
|
443
446
|
if (!cmdOpts.force && process.stdout.isTTY) {
|
|
444
|
-
const
|
|
447
|
+
const confirm6 = await p.confirm({
|
|
445
448
|
message: `Delete absence ${id}?`
|
|
446
449
|
});
|
|
447
|
-
if (!
|
|
450
|
+
if (!confirm6 || p.isCancel(confirm6)) return;
|
|
448
451
|
}
|
|
449
452
|
const absenceId = parseId(id);
|
|
450
453
|
await client.deleteAbsence({ id: absenceId });
|
|
@@ -562,6 +565,18 @@ function formatDecimalHours(seconds) {
|
|
|
562
565
|
function toClockodoDateTime(date) {
|
|
563
566
|
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
564
567
|
}
|
|
568
|
+
function parseDateTimeUntil(input) {
|
|
569
|
+
const lower = input.toLowerCase();
|
|
570
|
+
if (lower === "today" || lower === "yesterday" || lower === "tomorrow") {
|
|
571
|
+
const now = /* @__PURE__ */ new Date();
|
|
572
|
+
const offset = lower === "yesterday" ? -1 : lower === "tomorrow" ? 1 : 0;
|
|
573
|
+
return toClockodoDateTime(_endOfDay(addDays(now, offset)));
|
|
574
|
+
}
|
|
575
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
576
|
+
return toClockodoDateTime(_endOfDay(/* @__PURE__ */ new Date(`${input}T00:00:00`)));
|
|
577
|
+
}
|
|
578
|
+
return parseDateTime(input);
|
|
579
|
+
}
|
|
565
580
|
function parseDateTime(input) {
|
|
566
581
|
const now = /* @__PURE__ */ new Date();
|
|
567
582
|
switch (input.toLowerCase()) {
|
|
@@ -765,6 +780,8 @@ function registerConfigCommands(program2) {
|
|
|
765
780
|
}
|
|
766
781
|
|
|
767
782
|
// src/commands/customers.ts
|
|
783
|
+
import * as p4 from "@clack/prompts";
|
|
784
|
+
import { Billability as Billability2 } from "clockodo";
|
|
768
785
|
function registerCustomersCommands(program2) {
|
|
769
786
|
const customers = program2.command("customers").description("Manage customers");
|
|
770
787
|
customers.command("list", { isDefault: true }).description("List customers").option("--active", "Show only active customers").option("--search <text>", "Search by name").action(async (cmdOpts) => {
|
|
@@ -778,6 +795,14 @@ function registerCustomersCommands(program2) {
|
|
|
778
795
|
);
|
|
779
796
|
const items = result.data ?? [];
|
|
780
797
|
const mode = resolveOutputMode(opts);
|
|
798
|
+
if (items.length === 0) {
|
|
799
|
+
if (mode !== "human") {
|
|
800
|
+
printResult({ data: [], meta: { count: 0 } }, opts);
|
|
801
|
+
} else {
|
|
802
|
+
printInfo("No customers found.");
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
781
806
|
if (mode !== "human") {
|
|
782
807
|
printResult({ data: items, meta: { count: items.length } }, opts);
|
|
783
808
|
return;
|
|
@@ -812,12 +837,72 @@ function registerCustomersCommands(program2) {
|
|
|
812
837
|
opts
|
|
813
838
|
);
|
|
814
839
|
});
|
|
840
|
+
customers.command("create").description("Create a customer").requiredOption("--name <name>", "Customer name").option("--number <text>", "Customer number").option("--note <text>", "Note").option("--active", "Set as active (default)").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (cmdOpts) => {
|
|
841
|
+
const opts = program2.opts();
|
|
842
|
+
const client = getClient();
|
|
843
|
+
const params = {
|
|
844
|
+
name: cmdOpts.name,
|
|
845
|
+
...cmdOpts.number != null && { number: cmdOpts.number },
|
|
846
|
+
...cmdOpts.note != null && { note: cmdOpts.note },
|
|
847
|
+
...cmdOpts.active !== void 0 && { active: cmdOpts.active },
|
|
848
|
+
...cmdOpts.billable !== void 0 && {
|
|
849
|
+
billableDefault: cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
const result = await client.addCustomer(params);
|
|
853
|
+
const mode = resolveOutputMode(opts);
|
|
854
|
+
if (mode !== "human") {
|
|
855
|
+
printResult({ data: result.data }, opts);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
printSuccess(`Customer created (ID: ${result.data.id})`);
|
|
859
|
+
console.log(` ${result.data.name}`);
|
|
860
|
+
});
|
|
861
|
+
customers.command("update <id>").description("Update a customer").option("--name <name>", "New name").option("--number <text>", "New number").option("--note <text>", "New note").option("--active", "Set as active").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (id, cmdOpts) => {
|
|
862
|
+
const opts = program2.opts();
|
|
863
|
+
const client = getClient();
|
|
864
|
+
const params = {
|
|
865
|
+
id: parseId(id),
|
|
866
|
+
...cmdOpts.name != null && { name: cmdOpts.name },
|
|
867
|
+
...cmdOpts.number != null && { number: cmdOpts.number },
|
|
868
|
+
...cmdOpts.note != null && { note: cmdOpts.note },
|
|
869
|
+
...cmdOpts.active !== void 0 && { active: cmdOpts.active },
|
|
870
|
+
...cmdOpts.billable !== void 0 && {
|
|
871
|
+
billableDefault: cmdOpts.billable ? Billability2.Billable : Billability2.NotBillable
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const result = await client.editCustomer(params);
|
|
875
|
+
const mode = resolveOutputMode(opts);
|
|
876
|
+
if (mode !== "human") {
|
|
877
|
+
printResult({ data: result.data }, opts);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
printSuccess(`Customer ${id} updated`);
|
|
881
|
+
});
|
|
882
|
+
customers.command("delete <id>").description("Delete a customer").option("-f, --force", "Skip confirmation").action(async (id, cmdOpts) => {
|
|
883
|
+
const opts = program2.opts();
|
|
884
|
+
const client = getClient();
|
|
885
|
+
if (!cmdOpts.force && process.stdout.isTTY) {
|
|
886
|
+
const confirm6 = await p4.confirm({
|
|
887
|
+
message: `Delete customer ${id}?`
|
|
888
|
+
});
|
|
889
|
+
if (!confirm6 || p4.isCancel(confirm6)) return;
|
|
890
|
+
}
|
|
891
|
+
const customerId = parseId(id);
|
|
892
|
+
await client.deleteCustomer({ id: customerId });
|
|
893
|
+
const mode = resolveOutputMode(opts);
|
|
894
|
+
if (mode !== "human") {
|
|
895
|
+
printResult({ data: { success: true, id: customerId } }, opts);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
printSuccess(`Customer ${id} deleted`);
|
|
899
|
+
});
|
|
815
900
|
}
|
|
816
901
|
|
|
817
902
|
// src/commands/entries.ts
|
|
818
903
|
import { styleText as styleText4 } from "util";
|
|
819
|
-
import * as
|
|
820
|
-
import { Billability as
|
|
904
|
+
import * as p5 from "@clack/prompts";
|
|
905
|
+
import { Billability as Billability3, getEntryDurationUntilNow as getEntryDurationUntilNow2, isTimeEntry } from "clockodo";
|
|
821
906
|
function registerEntriesCommands(program2) {
|
|
822
907
|
const entries = program2.command("entries").description("Manage time entries");
|
|
823
908
|
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(
|
|
@@ -827,14 +912,14 @@ function registerEntriesCommands(program2) {
|
|
|
827
912
|
const opts = program2.opts();
|
|
828
913
|
const client = getClient();
|
|
829
914
|
const since = parseDateTime(cmdOpts.since);
|
|
830
|
-
const until = cmdOpts.until ?
|
|
915
|
+
const until = cmdOpts.until ? parseDateTimeUntil(cmdOpts.until) : toClockodoDateTime(endOfDay(/* @__PURE__ */ new Date()));
|
|
831
916
|
const filter = {};
|
|
832
917
|
if (cmdOpts.customer) filter.customersId = cmdOpts.customer;
|
|
833
918
|
if (cmdOpts.project) filter.projectsId = cmdOpts.project;
|
|
834
919
|
if (cmdOpts.service) filter.servicesId = cmdOpts.service;
|
|
835
920
|
if (cmdOpts.text) filter.text = cmdOpts.text;
|
|
836
921
|
if (cmdOpts.billable !== void 0)
|
|
837
|
-
filter.billable = cmdOpts.billable ?
|
|
922
|
+
filter.billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
|
|
838
923
|
const result = await client.getEntries({
|
|
839
924
|
timeSince: since,
|
|
840
925
|
timeUntil: until,
|
|
@@ -957,13 +1042,13 @@ function registerEntriesCommands(program2) {
|
|
|
957
1042
|
}
|
|
958
1043
|
let billable;
|
|
959
1044
|
if (cmdOpts.billable !== void 0) {
|
|
960
|
-
billable = cmdOpts.billable ?
|
|
1045
|
+
billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
|
|
961
1046
|
} else if (projectsId) {
|
|
962
1047
|
const { data: project } = await client.getProject({ id: projectsId });
|
|
963
|
-
billable = project.billableDefault ?
|
|
1048
|
+
billable = project.billableDefault ? Billability3.Billable : Billability3.NotBillable;
|
|
964
1049
|
} else {
|
|
965
1050
|
const { data: customer } = await client.getCustomer({ id: customersId });
|
|
966
|
-
billable = customer.billableDefault ?
|
|
1051
|
+
billable = customer.billableDefault ? Billability3.Billable : Billability3.NotBillable;
|
|
967
1052
|
}
|
|
968
1053
|
const result = await client.addEntry({
|
|
969
1054
|
customersId,
|
|
@@ -996,7 +1081,7 @@ function registerEntriesCommands(program2) {
|
|
|
996
1081
|
if (cmdOpts.project !== void 0) updates.projectsId = cmdOpts.project;
|
|
997
1082
|
if (cmdOpts.service !== void 0) updates.servicesId = cmdOpts.service;
|
|
998
1083
|
if (cmdOpts.billable !== void 0)
|
|
999
|
-
updates.billable = cmdOpts.billable ?
|
|
1084
|
+
updates.billable = cmdOpts.billable ? Billability3.Billable : Billability3.NotBillable;
|
|
1000
1085
|
const result = await client.editEntry(updates);
|
|
1001
1086
|
const mode = resolveOutputMode(opts);
|
|
1002
1087
|
if (mode !== "human") {
|
|
@@ -1009,10 +1094,10 @@ function registerEntriesCommands(program2) {
|
|
|
1009
1094
|
const opts = program2.opts();
|
|
1010
1095
|
const client = getClient();
|
|
1011
1096
|
if (!cmdOpts.force && process.stdout.isTTY) {
|
|
1012
|
-
const
|
|
1097
|
+
const confirm6 = await p5.confirm({
|
|
1013
1098
|
message: `Delete entry ${id}?`
|
|
1014
1099
|
});
|
|
1015
|
-
if (!
|
|
1100
|
+
if (!confirm6 || p5.isCancel(confirm6)) return;
|
|
1016
1101
|
}
|
|
1017
1102
|
const entryId = parseId(id);
|
|
1018
1103
|
await client.deleteEntry({ id: entryId });
|
|
@@ -1070,9 +1155,9 @@ function groupEntries(entries, key) {
|
|
|
1070
1155
|
}
|
|
1071
1156
|
function formatBillable(value) {
|
|
1072
1157
|
switch (value) {
|
|
1073
|
-
case
|
|
1158
|
+
case Billability3.Billable:
|
|
1074
1159
|
return "Yes";
|
|
1075
|
-
case
|
|
1160
|
+
case Billability3.Billed:
|
|
1076
1161
|
return "Billed";
|
|
1077
1162
|
default:
|
|
1078
1163
|
return "No";
|
|
@@ -1080,6 +1165,7 @@ function formatBillable(value) {
|
|
|
1080
1165
|
}
|
|
1081
1166
|
|
|
1082
1167
|
// src/commands/projects.ts
|
|
1168
|
+
import * as p6 from "@clack/prompts";
|
|
1083
1169
|
function registerProjectsCommands(program2) {
|
|
1084
1170
|
const projects = program2.command("projects").description("Manage projects");
|
|
1085
1171
|
projects.command("list", { isDefault: true }).description("List projects").option("--customer <id>", "Filter by customer ID", parseIntStrict).option("--active", "Show only active projects").option("--search <text>", "Search by name").action(async (cmdOpts) => {
|
|
@@ -1094,17 +1180,25 @@ function registerProjectsCommands(program2) {
|
|
|
1094
1180
|
);
|
|
1095
1181
|
const items = result.data ?? [];
|
|
1096
1182
|
const mode = resolveOutputMode(opts);
|
|
1183
|
+
if (items.length === 0) {
|
|
1184
|
+
if (mode !== "human") {
|
|
1185
|
+
printResult({ data: [], meta: { count: 0 } }, opts);
|
|
1186
|
+
} else {
|
|
1187
|
+
printInfo("No projects found.");
|
|
1188
|
+
}
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1097
1191
|
if (mode !== "human") {
|
|
1098
1192
|
printResult({ data: items, meta: { count: items.length } }, opts);
|
|
1099
1193
|
return;
|
|
1100
1194
|
}
|
|
1101
|
-
const rows = items.map((
|
|
1102
|
-
String(
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
String(
|
|
1106
|
-
|
|
1107
|
-
|
|
1195
|
+
const rows = items.map((proj) => [
|
|
1196
|
+
String(proj.id),
|
|
1197
|
+
proj.name,
|
|
1198
|
+
proj.number ?? "\u2014",
|
|
1199
|
+
String(proj.customersId),
|
|
1200
|
+
proj.active ? "Yes" : "No",
|
|
1201
|
+
proj.completed ? "Yes" : "No"
|
|
1108
1202
|
]);
|
|
1109
1203
|
printTable(["ID", "Name", "Number", "Customer", "Active", "Completed"], rows, opts);
|
|
1110
1204
|
});
|
|
@@ -1112,27 +1206,89 @@ function registerProjectsCommands(program2) {
|
|
|
1112
1206
|
const opts = program2.opts();
|
|
1113
1207
|
const client = getClient();
|
|
1114
1208
|
const result = await client.getProject({ id: parseId(id) });
|
|
1115
|
-
const
|
|
1209
|
+
const proj = result.data;
|
|
1116
1210
|
const mode = resolveOutputMode(opts);
|
|
1117
1211
|
if (mode !== "human") {
|
|
1118
|
-
printResult({ data:
|
|
1212
|
+
printResult({ data: proj }, opts);
|
|
1119
1213
|
return;
|
|
1120
1214
|
}
|
|
1121
1215
|
printDetail(
|
|
1122
1216
|
[
|
|
1123
|
-
["ID",
|
|
1124
|
-
["Name",
|
|
1125
|
-
["Number",
|
|
1126
|
-
["Customer ID",
|
|
1127
|
-
["Active",
|
|
1128
|
-
["Completed",
|
|
1129
|
-
["Budget",
|
|
1130
|
-
["Budget Type",
|
|
1131
|
-
["Note",
|
|
1217
|
+
["ID", proj.id],
|
|
1218
|
+
["Name", proj.name],
|
|
1219
|
+
["Number", proj.number ?? null],
|
|
1220
|
+
["Customer ID", proj.customersId],
|
|
1221
|
+
["Active", proj.active],
|
|
1222
|
+
["Completed", proj.completed],
|
|
1223
|
+
["Budget", proj.budget?.amount ?? null],
|
|
1224
|
+
["Budget Type", proj.budget?.monetary ? "Money" : "Hours"],
|
|
1225
|
+
["Note", proj.note ?? null]
|
|
1132
1226
|
],
|
|
1133
1227
|
opts
|
|
1134
1228
|
);
|
|
1135
1229
|
});
|
|
1230
|
+
projects.command("create").description("Create a project").requiredOption("--name <name>", "Project name").requiredOption("--customer <id>", "Customer ID", parseIntStrict).option("--number <text>", "Project number").option("--note <text>", "Note").option("--active", "Set as active (default)").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (cmdOpts) => {
|
|
1231
|
+
const opts = program2.opts();
|
|
1232
|
+
const client = getClient();
|
|
1233
|
+
const params = {
|
|
1234
|
+
name: cmdOpts.name,
|
|
1235
|
+
customersId: cmdOpts.customer,
|
|
1236
|
+
...cmdOpts.number != null && { number: cmdOpts.number },
|
|
1237
|
+
...cmdOpts.note != null && { note: cmdOpts.note },
|
|
1238
|
+
...cmdOpts.active !== void 0 && { active: cmdOpts.active },
|
|
1239
|
+
...cmdOpts.billable !== void 0 && {
|
|
1240
|
+
billableDefault: cmdOpts.billable
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
const result = await client.addProject(params);
|
|
1244
|
+
const mode = resolveOutputMode(opts);
|
|
1245
|
+
if (mode !== "human") {
|
|
1246
|
+
printResult({ data: result.data }, opts);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
printSuccess(`Project created (ID: ${result.data.id})`);
|
|
1250
|
+
console.log(` ${result.data.name}`);
|
|
1251
|
+
});
|
|
1252
|
+
projects.command("update <id>").description("Update a project").option("--name <name>", "New name").option("--customer <id>", "New customer ID", parseIntStrict).option("--number <text>", "New number").option("--note <text>", "New note").option("--active", "Set as active").option("--no-active", "Set as inactive").option("--billable", "Set as billable by default").option("--no-billable", "Set as not billable by default").action(async (id, cmdOpts) => {
|
|
1253
|
+
const opts = program2.opts();
|
|
1254
|
+
const client = getClient();
|
|
1255
|
+
const params = {
|
|
1256
|
+
id: parseId(id),
|
|
1257
|
+
...cmdOpts.name != null && { name: cmdOpts.name },
|
|
1258
|
+
...cmdOpts.customer != null && { customersId: cmdOpts.customer },
|
|
1259
|
+
...cmdOpts.number != null && { number: cmdOpts.number },
|
|
1260
|
+
...cmdOpts.note != null && { note: cmdOpts.note },
|
|
1261
|
+
...cmdOpts.active !== void 0 && { active: cmdOpts.active },
|
|
1262
|
+
...cmdOpts.billable !== void 0 && {
|
|
1263
|
+
billableDefault: cmdOpts.billable
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
const result = await client.editProject(params);
|
|
1267
|
+
const mode = resolveOutputMode(opts);
|
|
1268
|
+
if (mode !== "human") {
|
|
1269
|
+
printResult({ data: result.data }, opts);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
printSuccess(`Project ${id} updated`);
|
|
1273
|
+
});
|
|
1274
|
+
projects.command("delete <id>").description("Delete a project").option("-f, --force", "Skip confirmation").action(async (id, cmdOpts) => {
|
|
1275
|
+
const opts = program2.opts();
|
|
1276
|
+
const client = getClient();
|
|
1277
|
+
if (!cmdOpts.force && process.stdout.isTTY) {
|
|
1278
|
+
const confirm6 = await p6.confirm({
|
|
1279
|
+
message: `Delete project ${id}?`
|
|
1280
|
+
});
|
|
1281
|
+
if (!confirm6 || p6.isCancel(confirm6)) return;
|
|
1282
|
+
}
|
|
1283
|
+
const projectId = parseId(id);
|
|
1284
|
+
await client.deleteProject({ id: projectId });
|
|
1285
|
+
const mode = resolveOutputMode(opts);
|
|
1286
|
+
if (mode !== "human") {
|
|
1287
|
+
printResult({ data: { success: true, id: projectId } }, opts);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
printSuccess(`Project ${id} deleted`);
|
|
1291
|
+
});
|
|
1136
1292
|
}
|
|
1137
1293
|
|
|
1138
1294
|
// src/commands/report.ts
|
|
@@ -1184,12 +1340,23 @@ function groupEntriesByText(entries) {
|
|
|
1184
1340
|
}
|
|
1185
1341
|
return [...map.values()].sort((a, b) => b.seconds - a.seconds);
|
|
1186
1342
|
}
|
|
1187
|
-
|
|
1343
|
+
function buildFilter(cmdOpts) {
|
|
1344
|
+
const filter = {};
|
|
1345
|
+
if (cmdOpts.customer !== void 0) filter.customersId = cmdOpts.customer;
|
|
1346
|
+
if (cmdOpts.project !== void 0) filter.projectsId = cmdOpts.project;
|
|
1347
|
+
if (cmdOpts.service !== void 0) filter.servicesId = cmdOpts.service;
|
|
1348
|
+
if (cmdOpts.text !== void 0) filter.text = cmdOpts.text;
|
|
1349
|
+
if (cmdOpts.user !== void 0) filter.usersId = cmdOpts.user;
|
|
1350
|
+
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
1351
|
+
}
|
|
1352
|
+
async function runTextReport(program2, since, until, cmdOpts) {
|
|
1188
1353
|
const opts = program2.opts();
|
|
1189
1354
|
const client = getClient();
|
|
1355
|
+
const filter = buildFilter(cmdOpts);
|
|
1190
1356
|
const result = await client.getEntries({
|
|
1191
1357
|
timeSince: toClockodoDateTime(since),
|
|
1192
|
-
timeUntil: toClockodoDateTime(until)
|
|
1358
|
+
timeUntil: toClockodoDateTime(until),
|
|
1359
|
+
...filter && { filter }
|
|
1193
1360
|
});
|
|
1194
1361
|
const entryList = result.entries ?? [];
|
|
1195
1362
|
const groups = groupEntriesByText(entryList);
|
|
@@ -1233,16 +1400,18 @@ function printReportTotal(totalSeconds) {
|
|
|
1233
1400
|
console.log();
|
|
1234
1401
|
}
|
|
1235
1402
|
async function runReport(program2, since, until, cmdOpts) {
|
|
1236
|
-
const groupField = resolveReportGroupKey(cmdOpts.group ?? "
|
|
1403
|
+
const groupField = resolveReportGroupKey(cmdOpts.group ?? "project");
|
|
1237
1404
|
if (groupField === "text") {
|
|
1238
|
-
return runTextReport(program2, since, until);
|
|
1405
|
+
return runTextReport(program2, since, until, cmdOpts);
|
|
1239
1406
|
}
|
|
1240
1407
|
const opts = program2.opts();
|
|
1241
1408
|
const client = getClient();
|
|
1409
|
+
const filter = buildFilter(cmdOpts);
|
|
1242
1410
|
const result = await client.getEntryGroups({
|
|
1243
1411
|
timeSince: toClockodoDateTime(since),
|
|
1244
1412
|
timeUntil: toClockodoDateTime(until),
|
|
1245
|
-
grouping: [groupField]
|
|
1413
|
+
grouping: [groupField],
|
|
1414
|
+
...filter && { filter }
|
|
1246
1415
|
});
|
|
1247
1416
|
const groups = result.groups ?? [];
|
|
1248
1417
|
const totalSeconds = groups.reduce((sum, g) => sum + (g.duration ?? 0), 0);
|
|
@@ -1273,23 +1442,37 @@ async function runReport(program2, since, until, cmdOpts) {
|
|
|
1273
1442
|
printTable(["Name", "Duration", "Hours"], rows, opts);
|
|
1274
1443
|
printReportTotal(totalSeconds);
|
|
1275
1444
|
}
|
|
1445
|
+
function addFilterOptions(cmd) {
|
|
1446
|
+
return cmd.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 <text>", "Filter by description text").option("--user <id>", "Filter by user ID", parseIntStrict);
|
|
1447
|
+
}
|
|
1276
1448
|
function registerReportCommands(program2) {
|
|
1277
1449
|
const report = program2.command("report").description("Aggregated time reports");
|
|
1278
|
-
|
|
1450
|
+
addFilterOptions(
|
|
1451
|
+
report.command("today", { isDefault: true }).description("Today's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
|
|
1452
|
+
).action(async (cmdOpts) => {
|
|
1279
1453
|
const now = /* @__PURE__ */ new Date();
|
|
1280
1454
|
await runReport(program2, startOfDay(now), endOfDay(now), cmdOpts);
|
|
1281
1455
|
});
|
|
1282
|
-
|
|
1456
|
+
addFilterOptions(
|
|
1457
|
+
report.command("week").description("This week's summary (Mon-Sun)").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
|
|
1458
|
+
).action(async (cmdOpts) => {
|
|
1283
1459
|
const now = /* @__PURE__ */ new Date();
|
|
1284
1460
|
await runReport(program2, startOfWeek(now), endOfWeek(now), cmdOpts);
|
|
1285
1461
|
});
|
|
1286
|
-
|
|
1462
|
+
addFilterOptions(
|
|
1463
|
+
report.command("month").description("This month's summary").option("-g, --group <field>", "Group by: customer, project, service, text", "project")
|
|
1464
|
+
).action(async (cmdOpts) => {
|
|
1287
1465
|
const now = /* @__PURE__ */ new Date();
|
|
1288
1466
|
await runReport(program2, startOfMonth(now), endOfMonth(now), cmdOpts);
|
|
1289
1467
|
});
|
|
1290
|
-
|
|
1468
|
+
addFilterOptions(
|
|
1469
|
+
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", "project")
|
|
1470
|
+
).action(async (cmdOpts) => {
|
|
1291
1471
|
const since = new Date(parseDateTime(cmdOpts.since));
|
|
1292
|
-
const until = new Date(
|
|
1472
|
+
const until = new Date(parseDateTimeUntil(cmdOpts.until));
|
|
1473
|
+
if (since >= until) {
|
|
1474
|
+
throw new CliError("--since must be before --until", ExitCode.INVALID_ARGS);
|
|
1475
|
+
}
|
|
1293
1476
|
await runReport(program2, since, until, cmdOpts);
|
|
1294
1477
|
});
|
|
1295
1478
|
}
|
|
@@ -1326,6 +1509,7 @@ function registerSchemaCommand(program2) {
|
|
|
1326
1509
|
}
|
|
1327
1510
|
|
|
1328
1511
|
// src/commands/services.ts
|
|
1512
|
+
import * as p7 from "@clack/prompts";
|
|
1329
1513
|
function registerServicesCommands(program2) {
|
|
1330
1514
|
const services = program2.command("services").description("Manage services");
|
|
1331
1515
|
services.command("list", { isDefault: true }).description("List services").option("--active", "Show only active services").option("--search <text>", "Search by name").action(async (cmdOpts) => {
|
|
@@ -1339,6 +1523,14 @@ function registerServicesCommands(program2) {
|
|
|
1339
1523
|
);
|
|
1340
1524
|
const items = result.data ?? [];
|
|
1341
1525
|
const mode = resolveOutputMode(opts);
|
|
1526
|
+
if (items.length === 0) {
|
|
1527
|
+
if (mode !== "human") {
|
|
1528
|
+
printResult({ data: [], meta: { count: 0 } }, opts);
|
|
1529
|
+
} else {
|
|
1530
|
+
printInfo("No services found.");
|
|
1531
|
+
}
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1342
1534
|
if (mode !== "human") {
|
|
1343
1535
|
printResult({ data: items, meta: { count: items.length } }, opts);
|
|
1344
1536
|
return;
|
|
@@ -1372,16 +1564,94 @@ function registerServicesCommands(program2) {
|
|
|
1372
1564
|
opts
|
|
1373
1565
|
);
|
|
1374
1566
|
});
|
|
1567
|
+
services.command("create").description("Create a service").requiredOption("--name <name>", "Service name").option("--number <text>", "Service number").option("--note <text>", "Note").option("--active", "Set as active (default)").option("--no-active", "Set as inactive").action(async (cmdOpts) => {
|
|
1568
|
+
const opts = program2.opts();
|
|
1569
|
+
const client = getClient();
|
|
1570
|
+
const params = {
|
|
1571
|
+
name: cmdOpts.name,
|
|
1572
|
+
...cmdOpts.number != null && { number: cmdOpts.number },
|
|
1573
|
+
...cmdOpts.note != null && { note: cmdOpts.note },
|
|
1574
|
+
...cmdOpts.active !== void 0 && { active: cmdOpts.active }
|
|
1575
|
+
};
|
|
1576
|
+
const result = await client.addService(params);
|
|
1577
|
+
const mode = resolveOutputMode(opts);
|
|
1578
|
+
if (mode !== "human") {
|
|
1579
|
+
printResult({ data: result.data }, opts);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
printSuccess(`Service created (ID: ${result.data.id})`);
|
|
1583
|
+
console.log(` ${result.data.name}`);
|
|
1584
|
+
});
|
|
1585
|
+
services.command("update <id>").description("Update a service").option("--name <name>", "New name").option("--number <text>", "New number").option("--note <text>", "New note").option("--active", "Set as active").option("--no-active", "Set as inactive").action(async (id, cmdOpts) => {
|
|
1586
|
+
const opts = program2.opts();
|
|
1587
|
+
const client = getClient();
|
|
1588
|
+
const params = {
|
|
1589
|
+
id: parseId(id),
|
|
1590
|
+
...cmdOpts.name != null && { name: cmdOpts.name },
|
|
1591
|
+
...cmdOpts.number != null && { number: cmdOpts.number },
|
|
1592
|
+
...cmdOpts.note != null && { note: cmdOpts.note },
|
|
1593
|
+
...cmdOpts.active !== void 0 && { active: cmdOpts.active }
|
|
1594
|
+
};
|
|
1595
|
+
const result = await client.editService(params);
|
|
1596
|
+
const mode = resolveOutputMode(opts);
|
|
1597
|
+
if (mode !== "human") {
|
|
1598
|
+
printResult({ data: result.data }, opts);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
printSuccess(`Service ${id} updated`);
|
|
1602
|
+
});
|
|
1603
|
+
services.command("delete <id>").description("Delete a service").option("-f, --force", "Skip confirmation").action(async (id, cmdOpts) => {
|
|
1604
|
+
const opts = program2.opts();
|
|
1605
|
+
const client = getClient();
|
|
1606
|
+
if (!cmdOpts.force && process.stdout.isTTY) {
|
|
1607
|
+
const confirm6 = await p7.confirm({
|
|
1608
|
+
message: `Delete service ${id}?`
|
|
1609
|
+
});
|
|
1610
|
+
if (!confirm6 || p7.isCancel(confirm6)) return;
|
|
1611
|
+
}
|
|
1612
|
+
const serviceId = parseId(id);
|
|
1613
|
+
await client.deleteService({ id: serviceId });
|
|
1614
|
+
const mode = resolveOutputMode(opts);
|
|
1615
|
+
if (mode !== "human") {
|
|
1616
|
+
printResult({ data: { success: true, id: serviceId } }, opts);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
printSuccess(`Service ${id} deleted`);
|
|
1620
|
+
});
|
|
1375
1621
|
}
|
|
1376
1622
|
|
|
1377
1623
|
// src/commands/status.ts
|
|
1378
1624
|
import { styleText as styleText6 } from "util";
|
|
1379
1625
|
import { isTimeEntry as isTimeEntry2 } from "clockodo";
|
|
1380
1626
|
function registerStatusCommand(program2) {
|
|
1381
|
-
program2.command("status").description("Show running clock and today's summary").action(async () => {
|
|
1627
|
+
program2.command("status").description("Show running clock and today's summary").option("--prompt", "Single-line output for shell prompts (p10k, starship)").action(async (cmdOpts) => {
|
|
1382
1628
|
const opts = program2.opts();
|
|
1383
1629
|
const client = getClient();
|
|
1384
1630
|
const mode = resolveOutputMode(opts);
|
|
1631
|
+
if (cmdOpts.prompt) {
|
|
1632
|
+
const clock = await client.getClock();
|
|
1633
|
+
const running2 = clock.running;
|
|
1634
|
+
const elapsed = running2 ? elapsedSince(running2.timeSince) : 0;
|
|
1635
|
+
if (mode !== "human") {
|
|
1636
|
+
printResult(
|
|
1637
|
+
{
|
|
1638
|
+
data: {
|
|
1639
|
+
running: !!running2,
|
|
1640
|
+
text: running2?.text ?? null,
|
|
1641
|
+
elapsed,
|
|
1642
|
+
formatted: formatDuration(elapsed)
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
opts
|
|
1646
|
+
);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (running2) {
|
|
1650
|
+
const parts = [running2.text, formatDuration(elapsed)].filter(Boolean);
|
|
1651
|
+
console.log(parts.join(" "));
|
|
1652
|
+
}
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1385
1655
|
const now = /* @__PURE__ */ new Date();
|
|
1386
1656
|
const [clockResult, entriesResult] = await Promise.all([
|
|
1387
1657
|
client.getClock(),
|