bamboohr-cli 1.0.19 → 1.0.20-gc6d5f1a.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/dist/index.js +187 -29
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -68,6 +68,58 @@ function positiveInt(raw) {
|
|
|
68
68
|
return n;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ../../cli-utils/dist/text-or-file.js
|
|
72
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
73
|
+
import { InvalidArgumentError as InvalidArgumentError2, Option } from "commander";
|
|
74
|
+
var STDIN_REF = "-";
|
|
75
|
+
function rejectStdinSentinel(value) {
|
|
76
|
+
if (value === "@-" || value === STDIN_REF) {
|
|
77
|
+
throw new InvalidArgumentError2(`"${value}" looks like a stdin redirect, which is not supported here. Pass the text directly, or read it from a file/stdin with the matching --\u2026-file <path|-> option.`);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function readFileOrStdin(ref) {
|
|
82
|
+
if (ref === STDIN_REF) {
|
|
83
|
+
if (process.stdin.isTTY) {
|
|
84
|
+
throw new InvalidArgumentError2('"--\u2026-file -" reads stdin, but stdin is a terminal (nothing piped).');
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return readFileSync3(0, "utf8");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
throw new InvalidArgumentError2(`failed to read stdin for "--\u2026-file -": ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return readFileSync3(ref, "utf8");
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const e = err;
|
|
96
|
+
if (e.code === "ENOENT") {
|
|
97
|
+
throw new InvalidArgumentError2(`file not found: "${ref}".`);
|
|
98
|
+
}
|
|
99
|
+
throw new InvalidArgumentError2(`failed to read file "${ref}": ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function textOrFileOption(cmd, name, opts = {}) {
|
|
103
|
+
const label = `${name.charAt(0).toUpperCase()}${name.slice(1)} content`;
|
|
104
|
+
cmd.option(`--${name} <text>`, opts.description ?? label, rejectStdinSentinel);
|
|
105
|
+
cmd.addOption(new Option(`--${name}-file <path>`, `Read --${name} from a file, or "-" for stdin (mutually exclusive with --${name})`).conflicts(name));
|
|
106
|
+
return cmd;
|
|
107
|
+
}
|
|
108
|
+
function resolveTextOrFile(opts, name, { required = true } = {}) {
|
|
109
|
+
const literal = opts[name];
|
|
110
|
+
const ref = opts[`${name}File`];
|
|
111
|
+
if (literal !== void 0 && ref !== void 0) {
|
|
112
|
+
throw new InvalidArgumentError2(`--${name} and --${name}-file are mutually exclusive; provide only one.`);
|
|
113
|
+
}
|
|
114
|
+
if (ref !== void 0) {
|
|
115
|
+
return readFileOrStdin(ref);
|
|
116
|
+
}
|
|
117
|
+
if (literal === void 0 && required) {
|
|
118
|
+
throw new InvalidArgumentError2(`a ${name} is required: provide --${name} <text> or --${name}-file <path>.`);
|
|
119
|
+
}
|
|
120
|
+
return literal;
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
// ../../cli-utils/dist/builders.js
|
|
72
124
|
var EXAMPLES = /* @__PURE__ */ new WeakSet();
|
|
73
125
|
function commandPath(cmd) {
|
|
@@ -496,10 +548,10 @@ function mergeFields(existing, extra) {
|
|
|
496
548
|
}
|
|
497
549
|
|
|
498
550
|
// src/commands/employee/goals.ts
|
|
499
|
-
import { Option } from "commander";
|
|
551
|
+
import { Option as Option2 } from "commander";
|
|
500
552
|
var GOAL_FILTERS = ["open", "closed", "all"];
|
|
501
553
|
function goals(parent) {
|
|
502
|
-
const cmd = parent.command("goals <employeeId>").description("Get employee performance goals").addOption(new
|
|
554
|
+
const cmd = parent.command("goals <employeeId>").description("Get employee performance goals").addOption(new Option2("--filter <filter>", "Filter goals by status").choices(GOAL_FILTERS).default("all"));
|
|
503
555
|
examples(cmd, ["123", ["123 --filter open", "only open goals"], ["123 --filter closed", "only closed goals"]]);
|
|
504
556
|
cmd.action(async (employeeId, opts) => {
|
|
505
557
|
const client = getClient();
|
|
@@ -513,10 +565,10 @@ function goals(parent) {
|
|
|
513
565
|
|
|
514
566
|
// src/commands/employee/photo.ts
|
|
515
567
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
516
|
-
import { Option as
|
|
568
|
+
import { Option as Option3 } from "commander";
|
|
517
569
|
var PHOTO_SIZES = ["original", "large", "medium", "small", "xs", "tiny"];
|
|
518
570
|
function photo(parent) {
|
|
519
|
-
const cmd = parent.command("photo <employeeId>").description("Download employee photo").requiredOption("--output <path>", "File path to save photo").addOption(new
|
|
571
|
+
const cmd = parent.command("photo <employeeId>").description("Download employee photo").requiredOption("--output <path>", "File path to save photo").addOption(new Option3("--size <size>", "Photo size").choices(PHOTO_SIZES).default("medium"));
|
|
520
572
|
examples(cmd, [["123 --output ./photo.jpg", "medium size (default)"], "123 --output ./photo.jpg --size large"]);
|
|
521
573
|
cmd.action(async (employeeId, opts) => {
|
|
522
574
|
const client = getClient();
|
|
@@ -704,15 +756,84 @@ function balance(parent) {
|
|
|
704
756
|
}
|
|
705
757
|
|
|
706
758
|
// src/commands/timeoff/create.ts
|
|
707
|
-
import { Option as
|
|
759
|
+
import { InvalidArgumentError as InvalidArgumentError4, Option as Option4 } from "commander";
|
|
760
|
+
|
|
761
|
+
// src/utils/timeoff-dates.ts
|
|
762
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
763
|
+
var WEEKDAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
764
|
+
var YMD = /^\d{4}-\d{2}-\d{2}$/;
|
|
765
|
+
function parseYmd(value, label) {
|
|
766
|
+
if (!YMD.test(value)) {
|
|
767
|
+
throw new InvalidArgumentError3(`${label} must be a YYYY-MM-DD date, got "${value}".`);
|
|
768
|
+
}
|
|
769
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
770
|
+
if (Number.isNaN(date.getTime())) {
|
|
771
|
+
throw new InvalidArgumentError3(`${label} is not a valid date: "${value}".`);
|
|
772
|
+
}
|
|
773
|
+
return date;
|
|
774
|
+
}
|
|
775
|
+
function toYmd(date) {
|
|
776
|
+
return date.toISOString().slice(0, 10);
|
|
777
|
+
}
|
|
778
|
+
function buildTimeOffDates(params) {
|
|
779
|
+
const { start, end, units, defaultHours, holidays = [] } = params;
|
|
780
|
+
const startDate = parseYmd(start, "--start-date");
|
|
781
|
+
const endDate = parseYmd(end, "--end-date");
|
|
782
|
+
if (endDate < startDate) {
|
|
783
|
+
throw new InvalidArgumentError3(`--end-date ${end} is before --start-date ${start}.`);
|
|
784
|
+
}
|
|
785
|
+
const hoursByName = new Map(defaultHours.map((entry) => [entry.name, Number(entry.amount)]));
|
|
786
|
+
const fallbackHours = hoursByName.get("default") ?? 8;
|
|
787
|
+
const dates = [];
|
|
788
|
+
for (const day = new Date(startDate); day <= endDate; day.setUTCDate(day.getUTCDate() + 1)) {
|
|
789
|
+
const ymd = toYmd(day);
|
|
790
|
+
const workingHours = hoursByName.get(WEEKDAY_NAMES[day.getUTCDay()]) ?? fallbackHours;
|
|
791
|
+
const isHoliday = holidays.some((h) => ymd >= h.start && ymd <= h.end);
|
|
792
|
+
const working = workingHours > 0 && !isHoliday;
|
|
793
|
+
dates.push({ ymd, amount: working ? units === "hours" ? workingHours : 1 : 0 });
|
|
794
|
+
}
|
|
795
|
+
return dates;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/timeoff/create.ts
|
|
799
|
+
function parseDatesJson(raw) {
|
|
800
|
+
let parsed;
|
|
801
|
+
try {
|
|
802
|
+
parsed = JSON.parse(raw);
|
|
803
|
+
} catch {
|
|
804
|
+
throw new InvalidArgumentError4(
|
|
805
|
+
`--dates is not valid JSON. Expected an array like '[{"ymd":"2026-03-21","amount":0.5}]'.`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(
|
|
809
|
+
(d) => typeof d === "object" && d !== null && typeof d.ymd === "string"
|
|
810
|
+
)) {
|
|
811
|
+
throw new InvalidArgumentError4(
|
|
812
|
+
'--dates must be a non-empty JSON array of {"ymd":"YYYY-MM-DD","amount":<number>} objects.'
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return parsed.map((d) => ({ ymd: d.ymd, amount: Number(d.amount) }));
|
|
816
|
+
}
|
|
708
817
|
function create(parent) {
|
|
709
|
-
const cmd = parent.command("create [employeeId]").description("Create a time off request (defaults to current user)").requiredOption("--start-date <date>", "Start date (YYYY-MM-DD)").requiredOption("--end-date <date>", "End date (YYYY-MM-DD)").requiredOption("--type-id <id>", 'Time off type ID (use "timeoff types" to find IDs)', positiveInt).
|
|
710
|
-
|
|
711
|
-
|
|
818
|
+
const cmd = parent.command("create [employeeId]").description("Create a time off request (defaults to current user)").requiredOption("--start-date <date>", "Start date (YYYY-MM-DD)").requiredOption("--end-date <date>", "End date (YYYY-MM-DD)").requiredOption("--type-id <id>", 'Time off type ID (use "timeoff types" to find IDs)', positiveInt).option(
|
|
819
|
+
"--amount <amount>",
|
|
820
|
+
"Expected total (days or hours, per the type). Optional sanity check \u2014 the total is computed from the per-day breakdown",
|
|
821
|
+
parseFloat
|
|
822
|
+
).addOption(
|
|
823
|
+
new Option4("--status <status>", "Request status").choices(["requested", "approved"]).default("requested")
|
|
824
|
+
).option(
|
|
825
|
+
"--dates <json>",
|
|
826
|
+
`Per-day amounts as JSON array, e.g. '[{"ymd":"2026-03-21","amount":0.5}]' for a half day. When omitted, generated automatically: 1 day (or the weekday's working hours) per working day, 0 on weekends and company holidays`
|
|
827
|
+
);
|
|
828
|
+
textOrFileOption(cmd, "note", { description: "Note to include with the request" });
|
|
712
829
|
examples(cmd, [
|
|
713
|
-
"--start-date 2026-04-
|
|
830
|
+
["--start-date 2026-04-06 --end-date 2026-04-10 --type-id 83", "a full week off, spread per day automatically"],
|
|
714
831
|
[
|
|
715
|
-
|
|
832
|
+
`--start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --dates '[{"ymd":"2026-04-01","amount":0.5}]'`,
|
|
833
|
+
"a half day"
|
|
834
|
+
],
|
|
835
|
+
[
|
|
836
|
+
'504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --note "Doctor appointment"',
|
|
716
837
|
"for another employee"
|
|
717
838
|
]
|
|
718
839
|
]);
|
|
@@ -720,15 +841,48 @@ function create(parent) {
|
|
|
720
841
|
async (employeeId, opts) => {
|
|
721
842
|
const client = getClient();
|
|
722
843
|
const id = await resolveEmployeeId(client, employeeId);
|
|
844
|
+
const note = resolveTextOrFile(opts, "note", { required: false });
|
|
845
|
+
let dates;
|
|
846
|
+
if (opts.dates) {
|
|
847
|
+
dates = parseDatesJson(opts.dates);
|
|
848
|
+
} else {
|
|
849
|
+
const [types2, whosOut2] = await Promise.all([
|
|
850
|
+
getTimeOffTypes(client),
|
|
851
|
+
client.timeOff.getWhosOut({ start: opts.startDate, end: opts.endDate })
|
|
852
|
+
]);
|
|
853
|
+
const type = types2.timeOffTypes.find((t) => Number(t.id) === opts.typeId);
|
|
854
|
+
if (!type) {
|
|
855
|
+
throw new InvalidArgumentError4(`Unknown time off type ID ${opts.typeId}. Run "timeoff types" to list IDs.`);
|
|
856
|
+
}
|
|
857
|
+
const holidays = whosOut2.filter((e) => e.type === "holiday").map((e) => ({ start: e.start, end: e.end }));
|
|
858
|
+
dates = buildTimeOffDates({
|
|
859
|
+
start: opts.startDate,
|
|
860
|
+
end: opts.endDate,
|
|
861
|
+
units: type.units,
|
|
862
|
+
defaultHours: types2.defaultHours,
|
|
863
|
+
holidays
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
const total = dates.reduce((sum, d) => sum + d.amount, 0);
|
|
867
|
+
if (total <= 0) {
|
|
868
|
+
throw new InvalidArgumentError4(
|
|
869
|
+
"The requested range contains no working days (weekends/company holidays only). Adjust the dates or pass --dates explicitly."
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
if (opts.amount !== void 0 && Math.abs(opts.amount - total) > 1e-9) {
|
|
873
|
+
throw new InvalidArgumentError4(
|
|
874
|
+
`--amount ${opts.amount} does not match the per-day total ${total} (working days in range, minus weekends and company holidays). Omit --amount, or pass --dates to set per-day amounts explicitly (e.g. half days).`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
723
877
|
const result = await client.timeOff.createRequest({
|
|
724
878
|
employeeId: id,
|
|
725
879
|
status: opts.status,
|
|
726
880
|
start: opts.startDate,
|
|
727
881
|
end: opts.endDate,
|
|
728
882
|
timeOffTypeId: opts.typeId,
|
|
729
|
-
amount:
|
|
730
|
-
notes:
|
|
731
|
-
dates
|
|
883
|
+
amount: total,
|
|
884
|
+
notes: note ? [{ from: "employee", note }] : void 0,
|
|
885
|
+
dates
|
|
732
886
|
});
|
|
733
887
|
output(
|
|
734
888
|
result ? stripNullish(result) : { created: true, employeeId: id, start: opts.startDate, end: opts.endDate }
|
|
@@ -738,10 +892,10 @@ function create(parent) {
|
|
|
738
892
|
}
|
|
739
893
|
|
|
740
894
|
// src/commands/timeoff/requests.ts
|
|
741
|
-
import { Option as
|
|
895
|
+
import { Option as Option5 } from "commander";
|
|
742
896
|
function requests(parent) {
|
|
743
|
-
const cmd = parent.command("requests").description("Get time off requests (defaults to current user)").option("--request-id <id>", "Specific request ID", positiveInt).addOption(new
|
|
744
|
-
new
|
|
897
|
+
const cmd = parent.command("requests").description("Get time off requests (defaults to current user)").option("--request-id <id>", "Specific request ID", positiveInt).addOption(new Option5("--action <action>", "Access level filter").choices(["view", "approve"])).option("--employee-id <id>", "Filter by employee ID").option("--all", "Show all visible requests instead of just current user").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").addOption(
|
|
898
|
+
new Option5("--status <status>", "Filter by request status").choices([
|
|
745
899
|
"approved",
|
|
746
900
|
"denied",
|
|
747
901
|
"superceded",
|
|
@@ -791,25 +945,29 @@ function types(parent) {
|
|
|
791
945
|
}
|
|
792
946
|
|
|
793
947
|
// src/commands/timeoff/update-status.ts
|
|
794
|
-
import { Option as
|
|
948
|
+
import { Option as Option6 } from "commander";
|
|
795
949
|
function updateStatus(parent) {
|
|
796
950
|
const cmd = parent.command("update-status <requestId>").description("Update time off request status (approve, deny, cancel)").addOption(
|
|
797
|
-
new
|
|
798
|
-
)
|
|
951
|
+
new Option6("--status <status>", "New status").choices(["approved", "denied", "canceled"]).makeOptionMandatory()
|
|
952
|
+
);
|
|
953
|
+
textOrFileOption(cmd, "note", { description: "Note to attach to the status change" });
|
|
799
954
|
examples(cmd, [
|
|
800
955
|
"7890 --status approved",
|
|
801
956
|
['7890 --status denied --note "Team at capacity that week"', "deny with note"],
|
|
802
957
|
"7890 --status canceled"
|
|
803
958
|
]);
|
|
804
|
-
cmd.action(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
959
|
+
cmd.action(
|
|
960
|
+
async (requestId, opts) => {
|
|
961
|
+
const client = getClient();
|
|
962
|
+
const note = resolveTextOrFile(opts, "note", { required: false });
|
|
963
|
+
const result = await client.timeOff.updateRequestStatus({
|
|
964
|
+
requestId,
|
|
965
|
+
status: opts.status,
|
|
966
|
+
note
|
|
967
|
+
});
|
|
968
|
+
output(result ? stripNullish(result) : { updated: true, requestId, status: opts.status });
|
|
969
|
+
}
|
|
970
|
+
);
|
|
813
971
|
}
|
|
814
972
|
|
|
815
973
|
// src/commands/timeoff/whos-out.ts
|
|
@@ -831,7 +989,7 @@ function registerTimeoffCommands(program) {
|
|
|
831
989
|
const timeoff = program.command("timeoff").description("Time off operations");
|
|
832
990
|
examples(timeoff, [
|
|
833
991
|
"types --requestable",
|
|
834
|
-
"create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83
|
|
992
|
+
"create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83",
|
|
835
993
|
["requests --status requested --start-date 2026-01-01 --end-date 2026-12-31", "pending requests in date range"],
|
|
836
994
|
"balance 504",
|
|
837
995
|
"whos-out",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bamboohr-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20-gc6d5f1a.1",
|
|
4
4
|
"publish": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"commander": "^13.1.0",
|
|
15
|
-
"bamboohr-client": "1.0.24"
|
|
15
|
+
"bamboohr-client": "1.0.24-gc6d5f1a.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/node": "24.10.4",
|