bamboohr-cli 1.0.17 → 2.0.0-g7be3698.7
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 +321 -177
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/index.ts
|
|
4
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
-
import { dirname, join as join2 } from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
import { styleText } from "util";
|
|
8
|
-
import { Command as Command6 } from "commander";
|
|
9
|
-
|
|
10
3
|
// ../../cli-utils/dist/cache.js
|
|
11
4
|
import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
|
12
5
|
import { homedir } from "os";
|
|
@@ -51,6 +44,236 @@ async function cacheGetOrFetch(options, key, fetcher) {
|
|
|
51
44
|
return data;
|
|
52
45
|
}
|
|
53
46
|
|
|
47
|
+
// ../../cli-utils/dist/bootstrap.js
|
|
48
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
49
|
+
import { dirname, join as join2 } from "path";
|
|
50
|
+
import { fileURLToPath } from "url";
|
|
51
|
+
function readPackageVersion(importMetaUrl) {
|
|
52
|
+
try {
|
|
53
|
+
const here = dirname(fileURLToPath(importMetaUrl));
|
|
54
|
+
const pkg = JSON.parse(readFileSync2(join2(here, "..", "package.json"), "utf-8"));
|
|
55
|
+
return pkg.version ?? "0.0.0";
|
|
56
|
+
} catch {
|
|
57
|
+
return "0.0.0";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ../../cli-utils/dist/validators.js
|
|
62
|
+
import { InvalidArgumentError } from "commander";
|
|
63
|
+
function positiveInt(raw) {
|
|
64
|
+
const n = parseInt(raw, 10);
|
|
65
|
+
if (Number.isNaN(n) || n < 1) {
|
|
66
|
+
throw new InvalidArgumentError(`Must be a positive integer (got "${raw}")`);
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ../../cli-utils/dist/errors.js
|
|
72
|
+
import { CommanderError } from "commander";
|
|
73
|
+
var EXIT = {
|
|
74
|
+
SUCCESS: 0,
|
|
75
|
+
GENERIC: 1,
|
|
76
|
+
USAGE: 2,
|
|
77
|
+
NOT_FOUND: 3,
|
|
78
|
+
FORBIDDEN: 4,
|
|
79
|
+
CONFLICT: 5,
|
|
80
|
+
AUTH: 6
|
|
81
|
+
};
|
|
82
|
+
var CliAuthError = class extends Error {
|
|
83
|
+
constructor(message) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = "CliAuthError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var TYPE_EXIT = {
|
|
89
|
+
usage: EXIT.USAGE,
|
|
90
|
+
not_found: EXIT.NOT_FOUND,
|
|
91
|
+
forbidden: EXIT.FORBIDDEN,
|
|
92
|
+
conflict: EXIT.CONFLICT,
|
|
93
|
+
auth: EXIT.AUTH,
|
|
94
|
+
rate_limited: EXIT.GENERIC,
|
|
95
|
+
server: EXIT.GENERIC,
|
|
96
|
+
network: EXIT.GENERIC,
|
|
97
|
+
unknown: EXIT.GENERIC
|
|
98
|
+
};
|
|
99
|
+
var NETWORK_CODES = ["ENOTFOUND", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"];
|
|
100
|
+
function httpStatus(err) {
|
|
101
|
+
const e = err;
|
|
102
|
+
return e?.response?.status ?? e?.statusCode;
|
|
103
|
+
}
|
|
104
|
+
function responseDetail(err) {
|
|
105
|
+
const e = err;
|
|
106
|
+
const data = e?.response?.data;
|
|
107
|
+
if (data && typeof data === "object")
|
|
108
|
+
return data;
|
|
109
|
+
const body = e?.body;
|
|
110
|
+
if (typeof body === "string") {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(body);
|
|
113
|
+
return parsed.error ?? parsed;
|
|
114
|
+
} catch {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (body && typeof body === "object") {
|
|
119
|
+
return body.error ?? body;
|
|
120
|
+
}
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
function errorCode(err) {
|
|
124
|
+
return err?.code;
|
|
125
|
+
}
|
|
126
|
+
function normalize(err, opts) {
|
|
127
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
128
|
+
if (err instanceof CommanderError) {
|
|
129
|
+
return {
|
|
130
|
+
type: "usage",
|
|
131
|
+
message: message || "Invalid command usage",
|
|
132
|
+
recovery: "Check the command syntax and flags; run the command with --help.",
|
|
133
|
+
retryable: false
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (err instanceof CliAuthError) {
|
|
137
|
+
return { type: "auth", message: message || "Missing credentials", recovery: opts.authRecovery, retryable: false };
|
|
138
|
+
}
|
|
139
|
+
const status = httpStatus(err);
|
|
140
|
+
const detail = responseDetail(err);
|
|
141
|
+
if (status !== void 0) {
|
|
142
|
+
switch (status) {
|
|
143
|
+
case 400:
|
|
144
|
+
return {
|
|
145
|
+
type: "usage",
|
|
146
|
+
status,
|
|
147
|
+
message: "Bad request (HTTP 400)",
|
|
148
|
+
recovery: "Check parameter values (ids, keys, query syntax) against the API.",
|
|
149
|
+
retryable: false,
|
|
150
|
+
detail
|
|
151
|
+
};
|
|
152
|
+
case 401:
|
|
153
|
+
return {
|
|
154
|
+
type: "auth",
|
|
155
|
+
status,
|
|
156
|
+
message: "Authentication failed (HTTP 401)",
|
|
157
|
+
recovery: opts.authRecovery,
|
|
158
|
+
retryable: false
|
|
159
|
+
};
|
|
160
|
+
case 403:
|
|
161
|
+
return {
|
|
162
|
+
type: "forbidden",
|
|
163
|
+
status,
|
|
164
|
+
message: "Forbidden (HTTP 403)",
|
|
165
|
+
recovery: `Your ${opts.service} account lacks permission for this operation; check token scope and resource permissions.`,
|
|
166
|
+
retryable: false,
|
|
167
|
+
detail
|
|
168
|
+
};
|
|
169
|
+
case 404:
|
|
170
|
+
return {
|
|
171
|
+
type: "not_found",
|
|
172
|
+
status,
|
|
173
|
+
message: "Not found (HTTP 404)",
|
|
174
|
+
recovery: "Verify the id/key exists and that you have access to it.",
|
|
175
|
+
retryable: false,
|
|
176
|
+
detail
|
|
177
|
+
};
|
|
178
|
+
case 409:
|
|
179
|
+
return {
|
|
180
|
+
type: "conflict",
|
|
181
|
+
status,
|
|
182
|
+
message: "Conflict (HTTP 409)",
|
|
183
|
+
recovery: "The resource changed or already exists; re-fetch current state and retry.",
|
|
184
|
+
retryable: false,
|
|
185
|
+
detail
|
|
186
|
+
};
|
|
187
|
+
case 429:
|
|
188
|
+
return {
|
|
189
|
+
type: "rate_limited",
|
|
190
|
+
status,
|
|
191
|
+
message: "Rate limited (HTTP 429)",
|
|
192
|
+
recovery: "Wait and retry the request.",
|
|
193
|
+
retryable: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (status >= 500) {
|
|
197
|
+
return {
|
|
198
|
+
type: "server",
|
|
199
|
+
status,
|
|
200
|
+
message: `Server error (HTTP ${status})`,
|
|
201
|
+
recovery: `${opts.service} returned an internal error; retry shortly.`,
|
|
202
|
+
retryable: true,
|
|
203
|
+
detail
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
type: "unknown",
|
|
208
|
+
status,
|
|
209
|
+
message: `${opts.service} error (HTTP ${status}): ${message}`,
|
|
210
|
+
recovery: "Inspect the detail field for the API response.",
|
|
211
|
+
retryable: false,
|
|
212
|
+
detail
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const code = errorCode(err);
|
|
216
|
+
if (code && NETWORK_CODES.includes(code) || NETWORK_CODES.some((c) => message.includes(c))) {
|
|
217
|
+
return {
|
|
218
|
+
type: "network",
|
|
219
|
+
message: `Cannot connect to ${opts.service}: ${message}`,
|
|
220
|
+
recovery: opts.networkRecovery ?? "Verify the *_URL is correct, the server is reachable, and you are on the VPN if required.",
|
|
221
|
+
retryable: true
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { type: "unknown", message, recovery: "Unexpected error; inspect the message.", retryable: false };
|
|
225
|
+
}
|
|
226
|
+
function classifyError(err, opts) {
|
|
227
|
+
const base = normalize(err, opts);
|
|
228
|
+
const n = opts.adapt ? opts.adapt(base, err) : base;
|
|
229
|
+
if (n.type === "auth" && n.detail === void 0 && opts.credentialInfo) {
|
|
230
|
+
n.detail = opts.credentialInfo();
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
envelope: {
|
|
234
|
+
error: {
|
|
235
|
+
type: n.type,
|
|
236
|
+
message: n.message,
|
|
237
|
+
recovery: n.recovery,
|
|
238
|
+
retryable: n.retryable,
|
|
239
|
+
...n.detail !== void 0 ? { detail: n.detail } : {}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
exitCode: TYPE_EXIT[n.type]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function createErrorHandler(opts) {
|
|
246
|
+
return (err) => {
|
|
247
|
+
const { envelope, exitCode } = classifyError(err, opts);
|
|
248
|
+
process.stderr.write(`${JSON.stringify(envelope)}
|
|
249
|
+
`);
|
|
250
|
+
return process.exit(exitCode);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function isCleanCommanderExit(err) {
|
|
254
|
+
return err instanceof CommanderError && (err.exitCode === 0 || err.code === "commander.helpDisplayed" || err.code === "commander.help" || err.code === "commander.version");
|
|
255
|
+
}
|
|
256
|
+
function routeErrors(cmd) {
|
|
257
|
+
cmd.exitOverride();
|
|
258
|
+
cmd.configureOutput({ writeErr: () => void 0 });
|
|
259
|
+
cmd.commands.forEach(routeErrors);
|
|
260
|
+
}
|
|
261
|
+
async function runCli(program, opts) {
|
|
262
|
+
routeErrors(program);
|
|
263
|
+
try {
|
|
264
|
+
await program.parseAsync();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if (isCleanCommanderExit(err)) {
|
|
267
|
+
process.exit(err.exitCode ?? 0);
|
|
268
|
+
}
|
|
269
|
+
createErrorHandler(opts)(err);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/program.ts
|
|
274
|
+
import { styleText } from "util";
|
|
275
|
+
import { Command as Command6 } from "commander";
|
|
276
|
+
|
|
54
277
|
// src/utils/cached.ts
|
|
55
278
|
var CACHE_NAME = "bamboohr";
|
|
56
279
|
var TWELVE_HOURS = 432e5;
|
|
@@ -67,41 +290,12 @@ function getMetaFields(client) {
|
|
|
67
290
|
|
|
68
291
|
// src/utils/client.ts
|
|
69
292
|
import { BambooHRClient } from "bamboohr-client";
|
|
70
|
-
|
|
71
|
-
// src/utils/credentials.ts
|
|
72
|
-
function getCredentialInfo() {
|
|
73
|
-
const companyDomain = process.env.BAMBOO_COMPANY_DOMAIN;
|
|
74
|
-
const token = process.env.BAMBOO_TOKEN;
|
|
75
|
-
return {
|
|
76
|
-
environment: {
|
|
77
|
-
BAMBOO_COMPANY_DOMAIN: {
|
|
78
|
-
value: companyDomain ?? null,
|
|
79
|
-
description: 'Company subdomain (e.g., "mycompany" for mycompany.bamboohr.com)'
|
|
80
|
-
},
|
|
81
|
-
BAMBOO_TOKEN: {
|
|
82
|
-
value: token ? "<set>" : null,
|
|
83
|
-
description: "BambooHR API token"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
tokenUrl: `https://${companyDomain ?? "<companyDomain>"}.bamboohr.com/app/settings/permissions/api_keys`,
|
|
87
|
-
hint: "Export environment variables in your shell profile (e.g., ~/.zshrc)."
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/utils/client.ts
|
|
92
293
|
function getClient() {
|
|
93
294
|
const apiToken = process.env.BAMBOO_TOKEN;
|
|
94
295
|
const companyDomain = process.env.BAMBOO_COMPANY_DOMAIN;
|
|
95
296
|
if (!apiToken || !companyDomain) {
|
|
96
297
|
const missing = [...!apiToken ? ["BAMBOO_TOKEN"] : [], ...!companyDomain ? ["BAMBOO_COMPANY_DOMAIN"] : []];
|
|
97
|
-
|
|
98
|
-
`${JSON.stringify({
|
|
99
|
-
error: `Missing required environment variables: ${missing.join(", ")}`,
|
|
100
|
-
...getCredentialInfo()
|
|
101
|
-
})}
|
|
102
|
-
`
|
|
103
|
-
);
|
|
104
|
-
process.exit(1);
|
|
298
|
+
throw new CliAuthError(`Missing required environment variables: ${missing.join(", ")}`);
|
|
105
299
|
}
|
|
106
300
|
return new BambooHRClient({ apiToken, companyDomain });
|
|
107
301
|
}
|
|
@@ -115,68 +309,6 @@ function output(data) {
|
|
|
115
309
|
process.stdout.write(`${JSON.stringify(data, null, prettyPrint ? 2 : void 0)}
|
|
116
310
|
`);
|
|
117
311
|
}
|
|
118
|
-
function handleError(err) {
|
|
119
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
-
const axiosStatus = err?.response?.status;
|
|
121
|
-
if (axiosStatus === 400) {
|
|
122
|
-
const responseData = err?.response?.data;
|
|
123
|
-
const apiErrors = responseData && typeof responseData === "object" ? responseData : void 0;
|
|
124
|
-
process.stderr.write(
|
|
125
|
-
`${JSON.stringify({
|
|
126
|
-
error: "Bad request (HTTP 400)",
|
|
127
|
-
detail: apiErrors ?? message,
|
|
128
|
-
hint: "Check that all parameters are valid (employee IDs, field names, date formats, etc.)."
|
|
129
|
-
})}
|
|
130
|
-
`
|
|
131
|
-
);
|
|
132
|
-
} else if (axiosStatus === 401) {
|
|
133
|
-
process.stderr.write(
|
|
134
|
-
`${JSON.stringify({
|
|
135
|
-
error: "Authentication failed (HTTP 401)",
|
|
136
|
-
...getCredentialInfo()
|
|
137
|
-
})}
|
|
138
|
-
`
|
|
139
|
-
);
|
|
140
|
-
} else if (axiosStatus === 403) {
|
|
141
|
-
const responseData = err?.response?.data;
|
|
142
|
-
process.stderr.write(
|
|
143
|
-
`${JSON.stringify({
|
|
144
|
-
error: "Forbidden (HTTP 403)",
|
|
145
|
-
detail: responseData && typeof responseData === "object" ? responseData : message,
|
|
146
|
-
hint: "Your account does not have permission for this operation. Check your BambooHR access level."
|
|
147
|
-
})}
|
|
148
|
-
`
|
|
149
|
-
);
|
|
150
|
-
} else if (axiosStatus === 404) {
|
|
151
|
-
process.stderr.write(
|
|
152
|
-
`${JSON.stringify({
|
|
153
|
-
error: "Not found (HTTP 404)",
|
|
154
|
-
hint: "Resource not found. Note: BambooHR also returns 404 for invalid credentials \u2014 verify that BAMBOO_TOKEN and BAMBOO_COMPANY_DOMAIN are correct."
|
|
155
|
-
})}
|
|
156
|
-
`
|
|
157
|
-
);
|
|
158
|
-
} else if (message.includes("ENOTFOUND") || message.includes("ECONNREFUSED") || message.includes("ECONNRESET")) {
|
|
159
|
-
process.stderr.write(
|
|
160
|
-
`${JSON.stringify({
|
|
161
|
-
error: `Cannot connect to BambooHR API: ${message}`,
|
|
162
|
-
hint: "Verify that BAMBOO_COMPANY_DOMAIN is correct and you have internet connectivity."
|
|
163
|
-
})}
|
|
164
|
-
`
|
|
165
|
-
);
|
|
166
|
-
} else {
|
|
167
|
-
const responseData = err?.response?.data;
|
|
168
|
-
const detail = responseData && typeof responseData === "object" ? responseData : void 0;
|
|
169
|
-
process.stderr.write(
|
|
170
|
-
`${JSON.stringify({
|
|
171
|
-
error: message,
|
|
172
|
-
...axiosStatus !== void 0 && { statusCode: axiosStatus },
|
|
173
|
-
...detail && { detail }
|
|
174
|
-
})}
|
|
175
|
-
`
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
312
|
|
|
181
313
|
// src/utils/transformers/base.ts
|
|
182
314
|
function bamboohrBaseUrl() {
|
|
@@ -311,7 +443,7 @@ Examples:
|
|
|
311
443
|
|
|
312
444
|
// src/commands/employee/get.ts
|
|
313
445
|
function get(parent) {
|
|
314
|
-
parent.command("get [
|
|
446
|
+
parent.command("get [employeeId]").description("Get employee data by ID (default: current user)").option("--fields <fields>", "Comma-separated field names", "firstName,lastName,email,jobTitle").option("--include-future", "Include future-dated values from history fields").option("--with-photos", "Include the employee's signed photo URL and photoUploaded flag").addHelpText(
|
|
315
447
|
"after",
|
|
316
448
|
`
|
|
317
449
|
Examples:
|
|
@@ -320,16 +452,18 @@ Examples:
|
|
|
320
452
|
$ bamboohr employee get 123 --fields "firstName,lastName,department,hireDate"
|
|
321
453
|
$ bamboohr employee get 123 --include-future
|
|
322
454
|
$ bamboohr employee get 123 --with-photos`
|
|
323
|
-
).action(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
455
|
+
).action(
|
|
456
|
+
async (employeeId, opts) => {
|
|
457
|
+
const client = getClient();
|
|
458
|
+
const fields2 = opts.withPhotos ? mergeFields(opts.fields, ["photoUrl", "photoUploaded"]) : opts.fields;
|
|
459
|
+
const result = await client.employees.get({
|
|
460
|
+
id: employeeId ?? "0",
|
|
461
|
+
fields: fields2,
|
|
462
|
+
onlyCurrent: !opts.includeFuture
|
|
463
|
+
});
|
|
464
|
+
output(transformEmployee(result, { withPhotos: opts.withPhotos }));
|
|
465
|
+
}
|
|
466
|
+
);
|
|
333
467
|
}
|
|
334
468
|
function mergeFields(existing, extra) {
|
|
335
469
|
const set = new Set(
|
|
@@ -343,16 +477,16 @@ function mergeFields(existing, extra) {
|
|
|
343
477
|
import { Option } from "commander";
|
|
344
478
|
var GOAL_FILTERS = ["open", "closed", "all"];
|
|
345
479
|
function goals(parent) {
|
|
346
|
-
parent.command("goals <
|
|
480
|
+
parent.command("goals <employeeId>").description("Get employee performance goals").addOption(new Option("--filter <filter>", "Filter goals by status").choices(GOAL_FILTERS).default("all")).addHelpText(
|
|
347
481
|
"after",
|
|
348
482
|
`
|
|
349
483
|
Examples:
|
|
350
484
|
$ bamboohr employee goals 123
|
|
351
485
|
$ bamboohr employee goals 123 --filter open`
|
|
352
|
-
).action(async (
|
|
486
|
+
).action(async (employeeId, opts) => {
|
|
353
487
|
const client = getClient();
|
|
354
488
|
const result = await client.goals.get({
|
|
355
|
-
employeeId
|
|
489
|
+
employeeId,
|
|
356
490
|
filter: opts.filter === "all" ? void 0 : opts.filter
|
|
357
491
|
});
|
|
358
492
|
output(transformGoals(result));
|
|
@@ -364,16 +498,16 @@ import { writeFileSync as writeFileSync2 } from "fs";
|
|
|
364
498
|
import { Option as Option2 } from "commander";
|
|
365
499
|
var PHOTO_SIZES = ["original", "large", "medium", "small", "xs", "tiny"];
|
|
366
500
|
function photo(parent) {
|
|
367
|
-
parent.command("photo <
|
|
501
|
+
parent.command("photo <employeeId>").description("Download employee photo").requiredOption("--output <path>", "File path to save photo").addOption(new Option2("--size <size>", "Photo size").choices(PHOTO_SIZES).default("medium")).addHelpText(
|
|
368
502
|
"after",
|
|
369
503
|
`
|
|
370
504
|
Examples:
|
|
371
505
|
$ bamboohr employee photo 123 --output ./photo.jpg
|
|
372
506
|
$ bamboohr employee photo 123 --output ./photo.jpg --size large`
|
|
373
|
-
).action(async (
|
|
507
|
+
).action(async (employeeId, opts) => {
|
|
374
508
|
const client = getClient();
|
|
375
509
|
const buffer = await client.employees.getPhoto({
|
|
376
|
-
employeeId
|
|
510
|
+
employeeId,
|
|
377
511
|
size: opts.size
|
|
378
512
|
});
|
|
379
513
|
writeFileSync2(opts.output, buffer);
|
|
@@ -439,8 +573,8 @@ Examples:
|
|
|
439
573
|
}
|
|
440
574
|
|
|
441
575
|
// src/commands/employee/index.ts
|
|
442
|
-
function registerEmployeeCommands(
|
|
443
|
-
const employee =
|
|
576
|
+
function registerEmployeeCommands(program) {
|
|
577
|
+
const employee = program.command("employee").description("Employee operations").addHelpText(
|
|
444
578
|
"after",
|
|
445
579
|
`
|
|
446
580
|
Examples:
|
|
@@ -484,8 +618,8 @@ function list(parent) {
|
|
|
484
618
|
}
|
|
485
619
|
|
|
486
620
|
// src/commands/file/index.ts
|
|
487
|
-
function registerFileCommands(
|
|
488
|
-
const file =
|
|
621
|
+
function registerFileCommands(program) {
|
|
622
|
+
const file = program.command("file").description("Company file operations").addHelpText(
|
|
489
623
|
"after",
|
|
490
624
|
`
|
|
491
625
|
Examples:
|
|
@@ -537,8 +671,8 @@ function locations(parent) {
|
|
|
537
671
|
}
|
|
538
672
|
|
|
539
673
|
// src/commands/meta/index.ts
|
|
540
|
-
function registerMetaCommands(
|
|
541
|
-
const meta =
|
|
674
|
+
function registerMetaCommands(program) {
|
|
675
|
+
const meta = program.command("meta").description("Metadata operations").addHelpText(
|
|
542
676
|
"after",
|
|
543
677
|
`
|
|
544
678
|
Examples:
|
|
@@ -573,18 +707,6 @@ Examples:
|
|
|
573
707
|
|
|
574
708
|
// src/commands/timeoff/create.ts
|
|
575
709
|
import { Option as Option3 } from "commander";
|
|
576
|
-
|
|
577
|
-
// src/utils/cli.ts
|
|
578
|
-
import { InvalidArgumentError } from "commander";
|
|
579
|
-
function positiveInt(raw) {
|
|
580
|
-
const n = parseInt(raw, 10);
|
|
581
|
-
if (Number.isNaN(n) || n < 1) {
|
|
582
|
-
throw new InvalidArgumentError(`Must be a positive integer (got "${raw}")`);
|
|
583
|
-
}
|
|
584
|
-
return n;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// src/commands/timeoff/create.ts
|
|
588
710
|
function create(parent) {
|
|
589
711
|
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).requiredOption("--amount <amount>", "Total amount of time off (in days or hours depending on type)", parseFloat).addOption(
|
|
590
712
|
new Option3("--status <status>", "Request status").choices(["requested", "approved"]).default("requested")
|
|
@@ -618,7 +740,7 @@ Examples:
|
|
|
618
740
|
// src/commands/timeoff/requests.ts
|
|
619
741
|
import { Option as Option4 } from "commander";
|
|
620
742
|
function requests(parent) {
|
|
621
|
-
parent.command("requests").description("Get time off requests (defaults to current user)").option("--id <id>", "Specific request ID", positiveInt).addOption(new Option4("--action <action>", "Access level filter").choices(["view", "approve"])).option("--employee <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(
|
|
743
|
+
parent.command("requests").description("Get time off requests (defaults to current user)").option("--request-id <id>", "Specific request ID", positiveInt).addOption(new Option4("--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(
|
|
622
744
|
new Option4("--status <status>", "Filter by request status").choices([
|
|
623
745
|
"approved",
|
|
624
746
|
"denied",
|
|
@@ -626,32 +748,32 @@ function requests(parent) {
|
|
|
626
748
|
"requested",
|
|
627
749
|
"canceled"
|
|
628
750
|
])
|
|
629
|
-
).option("--type <typeId>", "Filter by time off type ID").addHelpText(
|
|
751
|
+
).option("--type-id <typeId>", "Filter by time off type ID").addHelpText(
|
|
630
752
|
"after",
|
|
631
753
|
`
|
|
632
754
|
Examples:
|
|
633
755
|
$ bamboohr timeoff requests
|
|
634
756
|
$ bamboohr timeoff requests --status requested
|
|
635
757
|
$ bamboohr timeoff requests --all --action approve --status requested
|
|
636
|
-
$ bamboohr timeoff requests --employee 123 --start-date 2026-01-01 --end-date 2026-03-31`
|
|
758
|
+
$ bamboohr timeoff requests --employee-id 123 --start-date 2026-01-01 --end-date 2026-03-31`
|
|
637
759
|
).action(
|
|
638
760
|
async (opts) => {
|
|
639
761
|
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
640
762
|
const start = opts.startDate ?? `${year}-01-01`;
|
|
641
763
|
const end = opts.endDate ?? `${year}-12-31`;
|
|
642
764
|
const client = getClient();
|
|
643
|
-
let employeeId = opts.
|
|
765
|
+
let employeeId = opts.employeeId;
|
|
644
766
|
if (!employeeId && !opts.all) {
|
|
645
767
|
employeeId = await resolveEmployeeId(client);
|
|
646
768
|
}
|
|
647
769
|
const result = await client.timeOff.getRequests({
|
|
648
|
-
id: opts.
|
|
770
|
+
id: opts.requestId,
|
|
649
771
|
action: opts.action,
|
|
650
772
|
employeeId,
|
|
651
773
|
start,
|
|
652
774
|
end,
|
|
653
775
|
status: opts.status,
|
|
654
|
-
type: opts.
|
|
776
|
+
type: opts.typeId
|
|
655
777
|
});
|
|
656
778
|
output(transformTimeOffRequests(result));
|
|
657
779
|
}
|
|
@@ -715,8 +837,8 @@ Examples:
|
|
|
715
837
|
}
|
|
716
838
|
|
|
717
839
|
// src/commands/timeoff/index.ts
|
|
718
|
-
function registerTimeoffCommands(
|
|
719
|
-
const timeoff =
|
|
840
|
+
function registerTimeoffCommands(program) {
|
|
841
|
+
const timeoff = program.command("timeoff").description("Time off operations").addHelpText(
|
|
720
842
|
"after",
|
|
721
843
|
`
|
|
722
844
|
Examples:
|
|
@@ -736,31 +858,22 @@ Examples:
|
|
|
736
858
|
updateStatus(timeoff);
|
|
737
859
|
}
|
|
738
860
|
|
|
739
|
-
// src/
|
|
740
|
-
function readPackageVersion() {
|
|
741
|
-
try {
|
|
742
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
743
|
-
const pkgPath = join2(here, "..", "package.json");
|
|
744
|
-
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
745
|
-
return pkg.version ?? "0.0.0";
|
|
746
|
-
} catch {
|
|
747
|
-
return "0.0.0";
|
|
748
|
-
}
|
|
749
|
-
}
|
|
861
|
+
// src/program.ts
|
|
750
862
|
var DIM = "\x1B[2m";
|
|
751
863
|
var RESET = "\x1B[0m";
|
|
752
|
-
|
|
753
|
-
program
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
864
|
+
function buildProgram() {
|
|
865
|
+
const program = new Command6();
|
|
866
|
+
program.name("bamboohr").description("BambooHR CLI").version(readPackageVersion(import.meta.url)).configureHelp({
|
|
867
|
+
styleTitle: (str) => styleText("bold", str),
|
|
868
|
+
styleUsage: (str) => styleText("dim", str),
|
|
869
|
+
styleCommandDescription: (str) => styleText("dim", str),
|
|
870
|
+
styleOptionDescription: (str) => styleText("dim", str),
|
|
871
|
+
styleSubcommandDescription: (str) => styleText("dim", str)
|
|
872
|
+
}).addHelpText("beforeAll", `
|
|
760
873
|
${styleText("bold", "bamboohr")} ${DIM}\u2014 BambooHR CLI${RESET}
|
|
761
874
|
`).addHelpText(
|
|
762
|
-
|
|
763
|
-
|
|
875
|
+
"after",
|
|
876
|
+
`
|
|
764
877
|
${styleText("bold", "Environment:")}
|
|
765
878
|
BAMBOO_TOKEN BambooHR API token ${DIM}(generate in BambooHR > Settings > API Keys)${RESET}
|
|
766
879
|
BAMBOO_COMPANY_DOMAIN Company subdomain ${DIM}(e.g., "mycompany" for mycompany.bamboohr.com)${RESET}
|
|
@@ -772,24 +885,55 @@ ${styleText("bold", "Examples:")}
|
|
|
772
885
|
${DIM}$${RESET} bamboohr employee photo 123 --output ./photo.jpg
|
|
773
886
|
${DIM}$${RESET} bamboohr timeoff types --requestable
|
|
774
887
|
${DIM}$${RESET} bamboohr timeoff create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1
|
|
775
|
-
${DIM}$${RESET} bamboohr timeoff create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1 --note "Doctor appointment"
|
|
776
888
|
${DIM}$${RESET} bamboohr timeoff requests --status requested --start-date 2026-01-01 --end-date 2026-12-31
|
|
777
889
|
${DIM}$${RESET} bamboohr timeoff whos-out
|
|
778
890
|
${DIM}$${RESET} bamboohr timeoff update-status 7890 --status approved
|
|
779
891
|
${DIM}$${RESET} bamboohr file list
|
|
780
892
|
${DIM}$${RESET} bamboohr meta fields
|
|
781
893
|
`
|
|
782
|
-
);
|
|
783
|
-
program.option("--pretty", "Pretty-print JSON output");
|
|
784
|
-
program.hook("preAction", (thisCommand) => {
|
|
785
|
-
|
|
786
|
-
});
|
|
787
|
-
registerEmployeeCommands(program);
|
|
788
|
-
registerTimeoffCommands(program);
|
|
789
|
-
registerFileCommands(program);
|
|
790
|
-
registerMetaCommands(program);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
894
|
+
);
|
|
895
|
+
program.option("--pretty", "Pretty-print JSON output");
|
|
896
|
+
program.hook("preAction", (thisCommand) => {
|
|
897
|
+
if (thisCommand.optsWithGlobals().pretty) setPretty(true);
|
|
898
|
+
});
|
|
899
|
+
registerEmployeeCommands(program);
|
|
900
|
+
registerTimeoffCommands(program);
|
|
901
|
+
registerFileCommands(program);
|
|
902
|
+
registerMetaCommands(program);
|
|
903
|
+
return program;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/utils/credentials.ts
|
|
907
|
+
function getCredentialInfo() {
|
|
908
|
+
const companyDomain = process.env.BAMBOO_COMPANY_DOMAIN;
|
|
909
|
+
const token = process.env.BAMBOO_TOKEN;
|
|
910
|
+
return {
|
|
911
|
+
environment: {
|
|
912
|
+
BAMBOO_COMPANY_DOMAIN: {
|
|
913
|
+
value: companyDomain ?? null,
|
|
914
|
+
description: 'Company subdomain (e.g., "mycompany" for mycompany.bamboohr.com)'
|
|
915
|
+
},
|
|
916
|
+
BAMBOO_TOKEN: {
|
|
917
|
+
value: token ? "<set>" : null,
|
|
918
|
+
description: "BambooHR API token"
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
tokenUrl: `https://${companyDomain ?? "<companyDomain>"}.bamboohr.com/app/settings/permissions/api_keys`,
|
|
922
|
+
hint: "Set these as environment variables in the process that runs this CLI (e.g. shell export, container env, or CI/secret store)."
|
|
923
|
+
};
|
|
795
924
|
}
|
|
925
|
+
|
|
926
|
+
// src/index.ts
|
|
927
|
+
await runCli(buildProgram(), {
|
|
928
|
+
credentialInfo: getCredentialInfo,
|
|
929
|
+
service: "BambooHR",
|
|
930
|
+
authRecovery: "Provide BAMBOO_TOKEN and BAMBOO_COMPANY_DOMAIN as environment variables to this process (shell export, container env, or CI secret). error.detail lists which are currently set and where to create a token.",
|
|
931
|
+
networkRecovery: "Verify BAMBOO_COMPANY_DOMAIN is correct and you have internet connectivity.",
|
|
932
|
+
// BambooHR returns HTTP 404 for invalid credentials as well as genuinely
|
|
933
|
+
// missing resources — it cannot distinguish them, so keep not_found but
|
|
934
|
+
// document the ambiguity in the recovery hint (design §4.9).
|
|
935
|
+
adapt: (normalized) => normalized.status === 404 ? {
|
|
936
|
+
...normalized,
|
|
937
|
+
recovery: "BambooHR returns 404 for invalid credentials too \u2014 verify BAMBOO_TOKEN and BAMBOO_COMPANY_DOMAIN, then confirm the resource id exists."
|
|
938
|
+
} : normalized
|
|
939
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bamboohr-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-g7be3698.7",
|
|
4
4
|
"publish": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"tsx": "^4.19.2",
|
|
23
23
|
"typescript": "^5.7.2",
|
|
24
24
|
"vitest": "^4.0.16",
|
|
25
|
+
"config-typescript": "0.0.0",
|
|
25
26
|
"cli-utils": "1.0.0",
|
|
26
|
-
"config-eslint": "0.0.0"
|
|
27
|
-
"config-typescript": "0.0.0"
|
|
27
|
+
"config-eslint": "0.0.0"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=22.0.0"
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
"lint": "eslint src",
|
|
36
36
|
"lint:fix": "eslint src --fix",
|
|
37
37
|
"test": "vitest run",
|
|
38
|
-
"test:integration": "vitest run
|
|
38
|
+
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
39
39
|
}
|
|
40
40
|
}
|