bamboohr-cli 1.0.17 → 2.0.0-g40cd2b1.10

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.
Files changed (2) hide show
  1. package/dist/index.js +436 -306
  2. 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,263 @@ 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/builders.js
72
+ var EXAMPLES = /* @__PURE__ */ new WeakSet();
73
+ function commandPath(cmd) {
74
+ const parts = [];
75
+ for (let c = cmd; c; c = c.parent)
76
+ parts.unshift(c.name());
77
+ return parts.join(" ");
78
+ }
79
+ function exampleLine(path, entry) {
80
+ if (typeof entry === "string")
81
+ return entry ? ` ${path} ${entry}` : ` ${path}`;
82
+ if ("raw" in entry)
83
+ return ` ${entry.raw}`;
84
+ const [args, comment] = entry;
85
+ const line = args ? `${path} ${args}` : path;
86
+ return ` ${line} # ${comment}`;
87
+ }
88
+ function examples(cmd, entries) {
89
+ EXAMPLES.add(cmd);
90
+ return cmd.addHelpText("after", () => {
91
+ const path = commandPath(cmd);
92
+ return `
93
+ Examples:
94
+ ${entries.map((e) => exampleLine(path, e)).join("\n")}`;
95
+ });
96
+ }
97
+
98
+ // ../../cli-utils/dist/errors.js
99
+ import { CommanderError } from "commander";
100
+ var EXIT = {
101
+ SUCCESS: 0,
102
+ GENERIC: 1,
103
+ USAGE: 2,
104
+ NOT_FOUND: 3,
105
+ FORBIDDEN: 4,
106
+ CONFLICT: 5,
107
+ AUTH: 6
108
+ };
109
+ var CliAuthError = class extends Error {
110
+ constructor(message) {
111
+ super(message);
112
+ this.name = "CliAuthError";
113
+ }
114
+ };
115
+ var TYPE_EXIT = {
116
+ usage: EXIT.USAGE,
117
+ not_found: EXIT.NOT_FOUND,
118
+ forbidden: EXIT.FORBIDDEN,
119
+ conflict: EXIT.CONFLICT,
120
+ auth: EXIT.AUTH,
121
+ rate_limited: EXIT.GENERIC,
122
+ server: EXIT.GENERIC,
123
+ network: EXIT.GENERIC,
124
+ unknown: EXIT.GENERIC
125
+ };
126
+ var NETWORK_CODES = ["ENOTFOUND", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"];
127
+ function httpStatus(err) {
128
+ const e = err;
129
+ return e?.response?.status ?? e?.statusCode;
130
+ }
131
+ function responseDetail(err) {
132
+ const e = err;
133
+ const data = e?.response?.data;
134
+ if (data && typeof data === "object")
135
+ return data;
136
+ const body = e?.body;
137
+ if (typeof body === "string") {
138
+ try {
139
+ const parsed = JSON.parse(body);
140
+ return parsed.error ?? parsed;
141
+ } catch {
142
+ return void 0;
143
+ }
144
+ }
145
+ if (body && typeof body === "object") {
146
+ return body.error ?? body;
147
+ }
148
+ return void 0;
149
+ }
150
+ function errorCode(err) {
151
+ return err?.code;
152
+ }
153
+ function normalize(err, opts) {
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ if (err instanceof CommanderError) {
156
+ return {
157
+ type: "usage",
158
+ message: message || "Invalid command usage",
159
+ recovery: "Check the command syntax and flags; run the command with --help.",
160
+ retryable: false
161
+ };
162
+ }
163
+ if (err instanceof CliAuthError) {
164
+ return { type: "auth", message: message || "Missing credentials", recovery: opts.authRecovery, retryable: false };
165
+ }
166
+ const status = httpStatus(err);
167
+ const detail = responseDetail(err);
168
+ if (status !== void 0) {
169
+ switch (status) {
170
+ case 400:
171
+ return {
172
+ type: "usage",
173
+ status,
174
+ message: "Bad request (HTTP 400)",
175
+ recovery: "Check parameter values (ids, keys, query syntax) against the API.",
176
+ retryable: false,
177
+ detail
178
+ };
179
+ case 401:
180
+ return {
181
+ type: "auth",
182
+ status,
183
+ message: "Authentication failed (HTTP 401)",
184
+ recovery: opts.authRecovery,
185
+ retryable: false
186
+ };
187
+ case 403:
188
+ return {
189
+ type: "forbidden",
190
+ status,
191
+ message: "Forbidden (HTTP 403)",
192
+ recovery: `Your ${opts.service} account lacks permission for this operation; check token scope and resource permissions.`,
193
+ retryable: false,
194
+ detail
195
+ };
196
+ case 404:
197
+ return {
198
+ type: "not_found",
199
+ status,
200
+ message: "Not found (HTTP 404)",
201
+ recovery: "Verify the id/key exists and that you have access to it.",
202
+ retryable: false,
203
+ detail
204
+ };
205
+ case 409:
206
+ return {
207
+ type: "conflict",
208
+ status,
209
+ message: "Conflict (HTTP 409)",
210
+ recovery: "The resource changed or already exists; re-fetch current state and retry.",
211
+ retryable: false,
212
+ detail
213
+ };
214
+ case 429:
215
+ return {
216
+ type: "rate_limited",
217
+ status,
218
+ message: "Rate limited (HTTP 429)",
219
+ recovery: "Wait and retry the request.",
220
+ retryable: true
221
+ };
222
+ }
223
+ if (status >= 500) {
224
+ return {
225
+ type: "server",
226
+ status,
227
+ message: `Server error (HTTP ${status})`,
228
+ recovery: `${opts.service} returned an internal error; retry shortly.`,
229
+ retryable: true,
230
+ detail
231
+ };
232
+ }
233
+ return {
234
+ type: "unknown",
235
+ status,
236
+ message: `${opts.service} error (HTTP ${status}): ${message}`,
237
+ recovery: "Inspect the detail field for the API response.",
238
+ retryable: false,
239
+ detail
240
+ };
241
+ }
242
+ const code = errorCode(err);
243
+ if (code && NETWORK_CODES.includes(code) || NETWORK_CODES.some((c) => message.includes(c))) {
244
+ return {
245
+ type: "network",
246
+ message: `Cannot connect to ${opts.service}: ${message}`,
247
+ recovery: opts.networkRecovery ?? "Verify the *_URL is correct, the server is reachable, and you are on the VPN if required.",
248
+ retryable: true
249
+ };
250
+ }
251
+ return { type: "unknown", message, recovery: "Unexpected error; inspect the message.", retryable: false };
252
+ }
253
+ function classifyError(err, opts) {
254
+ const base = normalize(err, opts);
255
+ const n = opts.adapt ? opts.adapt(base, err) : base;
256
+ if (n.type === "auth" && n.detail === void 0 && opts.credentialInfo) {
257
+ n.detail = opts.credentialInfo();
258
+ }
259
+ return {
260
+ envelope: {
261
+ error: {
262
+ type: n.type,
263
+ message: n.message,
264
+ recovery: n.recovery,
265
+ retryable: n.retryable,
266
+ ...n.detail !== void 0 ? { detail: n.detail } : {}
267
+ }
268
+ },
269
+ exitCode: TYPE_EXIT[n.type]
270
+ };
271
+ }
272
+ function createErrorHandler(opts) {
273
+ return (err) => {
274
+ const { envelope, exitCode } = classifyError(err, opts);
275
+ process.stderr.write(`${JSON.stringify(envelope)}
276
+ `);
277
+ return process.exit(exitCode);
278
+ };
279
+ }
280
+ function isCleanCommanderExit(err) {
281
+ return err instanceof CommanderError && (err.exitCode === 0 || err.code === "commander.helpDisplayed" || err.code === "commander.help" || err.code === "commander.version");
282
+ }
283
+ function routeErrors(cmd) {
284
+ cmd.exitOverride();
285
+ cmd.configureOutput({ writeErr: () => void 0 });
286
+ cmd.commands.forEach(routeErrors);
287
+ }
288
+ async function runCli(program, opts) {
289
+ routeErrors(program);
290
+ try {
291
+ await program.parseAsync();
292
+ } catch (err) {
293
+ if (isCleanCommanderExit(err)) {
294
+ process.exit(err.exitCode ?? 0);
295
+ }
296
+ createErrorHandler(opts)(err);
297
+ }
298
+ }
299
+
300
+ // src/program.ts
301
+ import { styleText } from "util";
302
+ import { Command as Command6 } from "commander";
303
+
54
304
  // src/utils/cached.ts
55
305
  var CACHE_NAME = "bamboohr";
56
306
  var TWELVE_HOURS = 432e5;
@@ -67,41 +317,12 @@ function getMetaFields(client) {
67
317
 
68
318
  // src/utils/client.ts
69
319
  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
320
  function getClient() {
93
321
  const apiToken = process.env.BAMBOO_TOKEN;
94
322
  const companyDomain = process.env.BAMBOO_COMPANY_DOMAIN;
95
323
  if (!apiToken || !companyDomain) {
96
324
  const missing = [...!apiToken ? ["BAMBOO_TOKEN"] : [], ...!companyDomain ? ["BAMBOO_COMPANY_DOMAIN"] : []];
97
- process.stderr.write(
98
- `${JSON.stringify({
99
- error: `Missing required environment variables: ${missing.join(", ")}`,
100
- ...getCredentialInfo()
101
- })}
102
- `
103
- );
104
- process.exit(1);
325
+ throw new CliAuthError(`Missing required environment variables: ${missing.join(", ")}`);
105
326
  }
106
327
  return new BambooHRClient({ apiToken, companyDomain });
107
328
  }
@@ -115,68 +336,6 @@ function output(data) {
115
336
  process.stdout.write(`${JSON.stringify(data, null, prettyPrint ? 2 : void 0)}
116
337
  `);
117
338
  }
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
339
 
181
340
  // src/utils/transformers/base.ts
182
341
  function bamboohrBaseUrl() {
@@ -296,13 +455,9 @@ function transformFields(fields2) {
296
455
 
297
456
  // src/commands/employee/directory.ts
298
457
  function directory(parent) {
299
- parent.command("directory").description("Get the company employee directory").option("--with-photos", "Include each employee's signed photo URL and photoUploaded flag (large)").addHelpText(
300
- "after",
301
- `
302
- Examples:
303
- $ bamboohr employee directory
304
- $ bamboohr employee directory --with-photos`
305
- ).action(async (opts) => {
458
+ const cmd = parent.command("directory").description("Get the company employee directory").option("--with-photos", "Include each employee's signed photo URL and photoUploaded flag (large)");
459
+ examples(cmd, ["", "--with-photos"]);
460
+ cmd.action(async (opts) => {
306
461
  const client = getClient();
307
462
  const result = await getDirectory(client);
308
463
  output(transformEmployeeDirectory(result, { withPhotos: opts.withPhotos }));
@@ -311,25 +466,26 @@ Examples:
311
466
 
312
467
  // src/commands/employee/get.ts
313
468
  function get(parent) {
314
- parent.command("get [id]").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
- "after",
316
- `
317
- Examples:
318
- $ bamboohr employee get
319
- $ bamboohr employee get 123
320
- $ bamboohr employee get 123 --fields "firstName,lastName,department,hireDate"
321
- $ bamboohr employee get 123 --include-future
322
- $ bamboohr employee get 123 --with-photos`
323
- ).action(async (id, opts) => {
324
- const client = getClient();
325
- const fields2 = opts.withPhotos ? mergeFields(opts.fields, ["photoUrl", "photoUploaded"]) : opts.fields;
326
- const result = await client.employees.get({
327
- id: id ?? "0",
328
- fields: fields2,
329
- onlyCurrent: !opts.includeFuture
330
- });
331
- output(transformEmployee(result, { withPhotos: opts.withPhotos }));
332
- });
469
+ const cmd = 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");
470
+ examples(cmd, [
471
+ "",
472
+ "123",
473
+ ['123 --fields "firstName,lastName,department,hireDate"', "custom fields"],
474
+ "123 --include-future",
475
+ "123 --with-photos"
476
+ ]);
477
+ cmd.action(
478
+ async (employeeId, opts) => {
479
+ const client = getClient();
480
+ const fields2 = opts.withPhotos ? mergeFields(opts.fields, ["photoUrl", "photoUploaded"]) : opts.fields;
481
+ const result = await client.employees.get({
482
+ id: employeeId ?? "0",
483
+ fields: fields2,
484
+ onlyCurrent: !opts.includeFuture
485
+ });
486
+ output(transformEmployee(result, { withPhotos: opts.withPhotos }));
487
+ }
488
+ );
333
489
  }
334
490
  function mergeFields(existing, extra) {
335
491
  const set = new Set(
@@ -343,16 +499,12 @@ function mergeFields(existing, extra) {
343
499
  import { Option } from "commander";
344
500
  var GOAL_FILTERS = ["open", "closed", "all"];
345
501
  function goals(parent) {
346
- parent.command("goals <id>").description("Get employee performance goals").addOption(new Option("--filter <filter>", "Filter goals by status").choices(GOAL_FILTERS).default("all")).addHelpText(
347
- "after",
348
- `
349
- Examples:
350
- $ bamboohr employee goals 123
351
- $ bamboohr employee goals 123 --filter open`
352
- ).action(async (id, opts) => {
502
+ const cmd = parent.command("goals <employeeId>").description("Get employee performance goals").addOption(new Option("--filter <filter>", "Filter goals by status").choices(GOAL_FILTERS).default("all"));
503
+ examples(cmd, ["123", ["123 --filter open", "only open goals"], ["123 --filter closed", "only closed goals"]]);
504
+ cmd.action(async (employeeId, opts) => {
353
505
  const client = getClient();
354
506
  const result = await client.goals.get({
355
- employeeId: id,
507
+ employeeId,
356
508
  filter: opts.filter === "all" ? void 0 : opts.filter
357
509
  });
358
510
  output(transformGoals(result));
@@ -364,16 +516,12 @@ import { writeFileSync as writeFileSync2 } from "fs";
364
516
  import { Option as Option2 } from "commander";
365
517
  var PHOTO_SIZES = ["original", "large", "medium", "small", "xs", "tiny"];
366
518
  function photo(parent) {
367
- parent.command("photo <id>").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
- "after",
369
- `
370
- Examples:
371
- $ bamboohr employee photo 123 --output ./photo.jpg
372
- $ bamboohr employee photo 123 --output ./photo.jpg --size large`
373
- ).action(async (id, opts) => {
519
+ const cmd = 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"));
520
+ examples(cmd, [["123 --output ./photo.jpg", "medium size (default)"], "123 --output ./photo.jpg --size large"]);
521
+ cmd.action(async (employeeId, opts) => {
374
522
  const client = getClient();
375
523
  const buffer = await client.employees.getPhoto({
376
- employeeId: id,
524
+ employeeId,
377
525
  size: opts.size
378
526
  });
379
527
  writeFileSync2(opts.output, buffer);
@@ -390,16 +538,15 @@ async function resolveEmployeeId(client, employeeId) {
390
538
 
391
539
  // src/commands/employee/search.ts
392
540
  function search(parent) {
393
- parent.command("search [query]").description("Search and filter employees").option("--supervisor <name>", 'Filter by supervisor name (use "me" for current user)').option("--department <name>", "Filter by department").option("--location <name>", "Filter by location").option("--division <name>", "Filter by division").option("--with-photos", "Include each result's signed photo URL and photoUploaded flag").addHelpText(
394
- "after",
395
- `
396
- Examples:
397
- bamboohr employee search john
398
- bamboohr employee search --supervisor me
399
- bamboohr employee search --department "R&D" --location Sofia
400
- bamboohr employee search --supervisor "Borislav Mihaylov"
401
- bamboohr employee search john --with-photos`
402
- ).action(
541
+ const cmd = parent.command("search [query]").description("Search and filter employees").option("--supervisor <name>", 'Filter by supervisor name (use "me" for current user)').option("--department <name>", "Filter by department").option("--location <name>", "Filter by location").option("--division <name>", "Filter by division").option("--with-photos", "Include each result's signed photo URL and photoUploaded flag");
542
+ examples(cmd, [
543
+ "john",
544
+ "--supervisor me",
545
+ ['--department "R&D" --location Sofia', "filter by dept and location"],
546
+ '--supervisor "Borislav Mihaylov"',
547
+ "john --with-photos"
548
+ ]);
549
+ cmd.action(
403
550
  async (query, opts) => {
404
551
  const client = getClient();
405
552
  const dir = await getDirectory(client);
@@ -439,18 +586,15 @@ Examples:
439
586
  }
440
587
 
441
588
  // src/commands/employee/index.ts
442
- function registerEmployeeCommands(program2) {
443
- const employee = program2.command("employee").description("Employee operations").addHelpText(
444
- "after",
445
- `
446
- Examples:
447
- $ bamboohr employee get
448
- $ bamboohr employee get 123 --fields "firstName,lastName,department"
449
- $ bamboohr employee directory
450
- $ bamboohr employee photo 123 --output ./photo.jpg
451
- $ bamboohr employee goals 123 --filter open
452
- `
453
- );
589
+ function registerEmployeeCommands(program) {
590
+ const employee = program.command("employee").description("Employee operations");
591
+ examples(employee, [
592
+ "get",
593
+ ['get 123 --fields "firstName,lastName,department"', "custom fields"],
594
+ "directory",
595
+ ["photo 123 --output ./photo.jpg", "download photo"],
596
+ ["goals 123 --filter open", "open goals"]
597
+ ]);
454
598
  get(employee);
455
599
  search(employee);
456
600
  directory(employee);
@@ -461,12 +605,9 @@ Examples:
461
605
  // src/commands/file/get.ts
462
606
  import { writeFileSync as writeFileSync3 } from "fs";
463
607
  function get2(parent) {
464
- parent.command("get <fileId>").description("Download a company file").requiredOption("--output <path>", "File path to save to").addHelpText(
465
- "after",
466
- `
467
- Examples:
468
- $ bamboohr file get 42 --output ./handbook.pdf`
469
- ).action(async (fileId, opts) => {
608
+ const cmd = parent.command("get <fileId>").description("Download a company file").requiredOption("--output <path>", "File path to save to");
609
+ examples(cmd, ["42 --output ./handbook.pdf"]);
610
+ cmd.action(async (fileId, opts) => {
470
611
  const client = getClient();
471
612
  const buffer = await client.files.get({ fileId });
472
613
  writeFileSync3(opts.output, buffer);
@@ -476,7 +617,9 @@ Examples:
476
617
 
477
618
  // src/commands/file/list.ts
478
619
  function list(parent) {
479
- parent.command("list").description("List all company files and categories").addHelpText("after", "\nExamples:\n $ bamboohr file list").action(async () => {
620
+ const cmd = parent.command("list").description("List all company files and categories");
621
+ examples(cmd, [""]);
622
+ cmd.action(async () => {
480
623
  const client = getClient();
481
624
  const result = await client.files.list();
482
625
  output(transformCompanyFiles(result));
@@ -484,22 +627,18 @@ function list(parent) {
484
627
  }
485
628
 
486
629
  // src/commands/file/index.ts
487
- function registerFileCommands(program2) {
488
- const file = program2.command("file").description("Company file operations").addHelpText(
489
- "after",
490
- `
491
- Examples:
492
- $ bamboohr file list
493
- $ bamboohr file get 42 --output ./handbook.pdf
494
- `
495
- );
630
+ function registerFileCommands(program) {
631
+ const file = program.command("file").description("Company file operations");
632
+ examples(file, ["list", ["get 42 --output ./handbook.pdf", "download a file"]]);
496
633
  list(file);
497
634
  get2(file);
498
635
  }
499
636
 
500
637
  // src/commands/meta/departments.ts
501
638
  function departments(parent) {
502
- parent.command("departments").description("List all departments").addHelpText("after", "\nExamples:\n bamboohr meta departments").action(async () => {
639
+ const cmd = parent.command("departments").description("List all departments");
640
+ examples(cmd, [""]);
641
+ cmd.action(async () => {
503
642
  const client = getClient();
504
643
  const dir = await getDirectory(client);
505
644
  const unique = [...new Set(dir.employees.map((e) => e.department).filter(Boolean))].sort();
@@ -509,7 +648,9 @@ function departments(parent) {
509
648
 
510
649
  // src/commands/meta/divisions.ts
511
650
  function divisions(parent) {
512
- parent.command("divisions").description("List all divisions").addHelpText("after", "\nExamples:\n bamboohr meta divisions").action(async () => {
651
+ const cmd = parent.command("divisions").description("List all divisions");
652
+ examples(cmd, [""]);
653
+ cmd.action(async () => {
513
654
  const client = getClient();
514
655
  const dir = await getDirectory(client);
515
656
  const unique = [...new Set(dir.employees.map((e) => e.division).filter(Boolean))].sort();
@@ -519,7 +660,9 @@ function divisions(parent) {
519
660
 
520
661
  // src/commands/meta/fields.ts
521
662
  function fields(parent) {
522
- parent.command("fields").description("List all available BambooHR fields").addHelpText("after", "\nExamples:\n $ bamboohr meta fields").action(async () => {
663
+ const cmd = parent.command("fields").description("List all available BambooHR fields");
664
+ examples(cmd, [""]);
665
+ cmd.action(async () => {
523
666
  const client = getClient();
524
667
  const result = await getMetaFields(client);
525
668
  output(transformFields(result));
@@ -528,7 +671,9 @@ function fields(parent) {
528
671
 
529
672
  // src/commands/meta/locations.ts
530
673
  function locations(parent) {
531
- parent.command("locations").description("List all office locations").addHelpText("after", "\nExamples:\n bamboohr meta locations").action(async () => {
674
+ const cmd = parent.command("locations").description("List all office locations");
675
+ examples(cmd, [""]);
676
+ cmd.action(async () => {
532
677
  const client = getClient();
533
678
  const dir = await getDirectory(client);
534
679
  const unique = [...new Set(dir.employees.map((e) => e.location).filter(Boolean))].sort();
@@ -537,17 +682,9 @@ function locations(parent) {
537
682
  }
538
683
 
539
684
  // src/commands/meta/index.ts
540
- function registerMetaCommands(program2) {
541
- const meta = program2.command("meta").description("Metadata operations").addHelpText(
542
- "after",
543
- `
544
- Examples:
545
- $ bamboohr meta fields
546
- $ bamboohr meta departments
547
- $ bamboohr meta locations
548
- $ bamboohr meta divisions
549
- `
550
- );
685
+ function registerMetaCommands(program) {
686
+ const meta = program.command("meta").description("Metadata operations");
687
+ examples(meta, ["fields", "departments", "locations", "divisions"]);
551
688
  fields(meta);
552
689
  departments(meta);
553
690
  locations(meta);
@@ -556,14 +693,9 @@ Examples:
556
693
 
557
694
  // src/commands/timeoff/balance.ts
558
695
  function balance(parent) {
559
- parent.command("balance [employeeId]").description("Estimate time off balance (defaults to current user)").option("--date <date>", "Future date to estimate balance for (YYYY-MM-DD)").addHelpText(
560
- "after",
561
- `
562
- Examples:
563
- $ bamboohr timeoff balance
564
- $ bamboohr timeoff balance 123
565
- $ bamboohr timeoff balance --date 2026-06-30`
566
- ).action(async (employeeId, opts) => {
696
+ const cmd = parent.command("balance [employeeId]").description("Estimate time off balance (defaults to current user)").option("--date <date>", "Future date to estimate balance for (YYYY-MM-DD)");
697
+ examples(cmd, ["", "123", ["--date 2026-06-30", "balance at a future date"]]);
698
+ cmd.action(async (employeeId, opts) => {
567
699
  const client = getClient();
568
700
  const id = await resolveEmployeeId(client, employeeId);
569
701
  const result = await client.timeOff.getBalance({ employeeId: id, date: opts.date });
@@ -573,28 +705,18 @@ Examples:
573
705
 
574
706
  // src/commands/timeoff/create.ts
575
707
  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
708
  function create(parent) {
589
- 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(
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).requiredOption("--amount <amount>", "Total amount of time off (in days or hours depending on type)", parseFloat).addOption(
590
710
  new Option3("--status <status>", "Request status").choices(["requested", "approved"]).default("requested")
591
- ).option("--note <text>", "Note to include with the request").option("--dates <json>", `Per-day amounts as JSON array, e.g. '[{"ymd":"2026-03-21","amount":4}]' for half-day`).addHelpText(
592
- "after",
593
- `
594
- Examples:
595
- $ bamboohr timeoff create --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1
596
- $ bamboohr timeoff create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1 --note "Doctor appointment"`
597
- ).action(
711
+ ).option("--note <text>", "Note to include with the request").option("--dates <json>", `Per-day amounts as JSON array, e.g. '[{"ymd":"2026-03-21","amount":4}]' for half-day`);
712
+ examples(cmd, [
713
+ "--start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1",
714
+ [
715
+ '504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1 --note "Doctor appointment"',
716
+ "for another employee"
717
+ ]
718
+ ]);
719
+ cmd.action(
598
720
  async (employeeId, opts) => {
599
721
  const client = getClient();
600
722
  const id = await resolveEmployeeId(client, employeeId);
@@ -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
+ const cmd = 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,31 @@ function requests(parent) {
626
748
  "requested",
627
749
  "canceled"
628
750
  ])
629
- ).option("--type <typeId>", "Filter by time off type ID").addHelpText(
630
- "after",
631
- `
632
- Examples:
633
- $ bamboohr timeoff requests
634
- $ bamboohr timeoff requests --status requested
635
- $ bamboohr timeoff requests --all --action approve --status requested
636
- $ bamboohr timeoff requests --employee 123 --start-date 2026-01-01 --end-date 2026-03-31`
637
- ).action(
751
+ ).option("--type-id <typeId>", "Filter by time off type ID");
752
+ examples(cmd, [
753
+ "",
754
+ "--status requested",
755
+ ["--all --action approve --status requested", "all pending approvals"],
756
+ ["--employee-id 123 --start-date 2026-01-01 --end-date 2026-03-31", "Q1 for employee 123"]
757
+ ]);
758
+ cmd.action(
638
759
  async (opts) => {
639
760
  const year = (/* @__PURE__ */ new Date()).getFullYear();
640
761
  const start = opts.startDate ?? `${year}-01-01`;
641
762
  const end = opts.endDate ?? `${year}-12-31`;
642
763
  const client = getClient();
643
- let employeeId = opts.employee;
764
+ let employeeId = opts.employeeId;
644
765
  if (!employeeId && !opts.all) {
645
766
  employeeId = await resolveEmployeeId(client);
646
767
  }
647
768
  const result = await client.timeOff.getRequests({
648
- id: opts.id,
769
+ id: opts.requestId,
649
770
  action: opts.action,
650
771
  employeeId,
651
772
  start,
652
773
  end,
653
774
  status: opts.status,
654
- type: opts.type
775
+ type: opts.typeId
655
776
  });
656
777
  output(transformTimeOffRequests(result));
657
778
  }
@@ -660,13 +781,9 @@ Examples:
660
781
 
661
782
  // src/commands/timeoff/types.ts
662
783
  function types(parent) {
663
- parent.command("types").description("List available time off types").option("--requestable", "Only show types the user can request").addHelpText(
664
- "after",
665
- `
666
- Examples:
667
- $ bamboohr timeoff types
668
- $ bamboohr timeoff types --requestable`
669
- ).action(async (opts) => {
784
+ const cmd = parent.command("types").description("List available time off types").option("--requestable", "Only show types the user can request");
785
+ examples(cmd, ["", ["--requestable", "only types the current user can request"]]);
786
+ cmd.action(async (opts) => {
670
787
  const client = getClient();
671
788
  const result = opts.requestable ? await client.timeOff.getTypes({ mode: "request" }) : await getTimeOffTypes(client);
672
789
  output(transformTimeOffTypesResponse(result));
@@ -676,16 +793,15 @@ Examples:
676
793
  // src/commands/timeoff/update-status.ts
677
794
  import { Option as Option5 } from "commander";
678
795
  function updateStatus(parent) {
679
- parent.command("update-status <requestId>").description("Update time off request status (approve, deny, cancel)").addOption(
796
+ const cmd = parent.command("update-status <requestId>").description("Update time off request status (approve, deny, cancel)").addOption(
680
797
  new Option5("--status <status>", "New status").choices(["approved", "denied", "canceled"]).makeOptionMandatory()
681
- ).option("--note <text>", "Note to attach to the status change").addHelpText(
682
- "after",
683
- `
684
- Examples:
685
- $ bamboohr timeoff update-status 7890 --status approved
686
- $ bamboohr timeoff update-status 7890 --status denied --note "Team at capacity that week"
687
- $ bamboohr timeoff update-status 7890 --status canceled`
688
- ).action(async (requestId, opts) => {
798
+ ).option("--note <text>", "Note to attach to the status change");
799
+ examples(cmd, [
800
+ "7890 --status approved",
801
+ ['7890 --status denied --note "Team at capacity that week"', "deny with note"],
802
+ "7890 --status canceled"
803
+ ]);
804
+ cmd.action(async (requestId, opts) => {
689
805
  const client = getClient();
690
806
  const result = await client.timeOff.updateRequestStatus({
691
807
  requestId,
@@ -698,13 +814,9 @@ Examples:
698
814
 
699
815
  // src/commands/timeoff/whos-out.ts
700
816
  function whosOut(parent) {
701
- parent.command("whos-out").description("View who's out \u2014 upcoming time off and holidays").option("--start-date <date>", "Start date (YYYY-MM-DD, defaults to today)").option("--end-date <date>", "End date (YYYY-MM-DD, defaults to today)").addHelpText(
702
- "after",
703
- `
704
- Examples:
705
- $ bamboohr timeoff whos-out
706
- $ bamboohr timeoff whos-out --start-date 2026-03-20 --end-date 2026-04-03`
707
- ).action(async (opts) => {
817
+ const cmd = parent.command("whos-out").description("View who's out \u2014 upcoming time off and holidays").option("--start-date <date>", "Start date (YYYY-MM-DD, defaults to today)").option("--end-date <date>", "End date (YYYY-MM-DD, defaults to today)");
818
+ examples(cmd, ["", ["--start-date 2026-03-20 --end-date 2026-04-03", "two-week window"]]);
819
+ cmd.action(async (opts) => {
708
820
  const client = getClient();
709
821
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
710
822
  const start = opts.startDate ?? today;
@@ -715,19 +827,16 @@ Examples:
715
827
  }
716
828
 
717
829
  // src/commands/timeoff/index.ts
718
- function registerTimeoffCommands(program2) {
719
- const timeoff = program2.command("timeoff").description("Time off operations").addHelpText(
720
- "after",
721
- `
722
- Examples:
723
- $ bamboohr timeoff types --requestable
724
- $ bamboohr timeoff create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1
725
- $ bamboohr timeoff requests --status requested --start-date 2026-01-01 --end-date 2026-12-31
726
- $ bamboohr timeoff balance 504
727
- $ bamboohr timeoff whos-out
728
- $ bamboohr timeoff update-status 7890 --status approved
729
- `
730
- );
830
+ function registerTimeoffCommands(program) {
831
+ const timeoff = program.command("timeoff").description("Time off operations");
832
+ examples(timeoff, [
833
+ "types --requestable",
834
+ "create 504 --start-date 2026-04-01 --end-date 2026-04-01 --type-id 83 --amount 1",
835
+ ["requests --status requested --start-date 2026-01-01 --end-date 2026-12-31", "pending requests in date range"],
836
+ "balance 504",
837
+ "whos-out",
838
+ "update-status 7890 --status approved"
839
+ ]);
731
840
  types(timeoff);
732
841
  create(timeoff);
733
842
  requests(timeoff);
@@ -736,31 +845,22 @@ Examples:
736
845
  updateStatus(timeoff);
737
846
  }
738
847
 
739
- // src/index.ts
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
- }
848
+ // src/program.ts
750
849
  var DIM = "\x1B[2m";
751
850
  var RESET = "\x1B[0m";
752
- var program = new Command6();
753
- program.name("bamboohr").description("BambooHR CLI").version(readPackageVersion()).configureHelp({
754
- styleTitle: (str) => styleText("bold", str),
755
- styleUsage: (str) => styleText("dim", str),
756
- styleCommandDescription: (str) => styleText("dim", str),
757
- styleOptionDescription: (str) => styleText("dim", str),
758
- styleSubcommandDescription: (str) => styleText("dim", str)
759
- }).addHelpText("beforeAll", `
851
+ function buildProgram() {
852
+ const program = new Command6();
853
+ program.name("bamboohr").description("BambooHR CLI").version(readPackageVersion(import.meta.url)).configureHelp({
854
+ styleTitle: (str) => styleText("bold", str),
855
+ styleUsage: (str) => styleText("dim", str),
856
+ styleCommandDescription: (str) => styleText("dim", str),
857
+ styleOptionDescription: (str) => styleText("dim", str),
858
+ styleSubcommandDescription: (str) => styleText("dim", str)
859
+ }).addHelpText("beforeAll", `
760
860
  ${styleText("bold", "bamboohr")} ${DIM}\u2014 BambooHR CLI${RESET}
761
861
  `).addHelpText(
762
- "after",
763
- `
862
+ "after",
863
+ `
764
864
  ${styleText("bold", "Environment:")}
765
865
  BAMBOO_TOKEN BambooHR API token ${DIM}(generate in BambooHR > Settings > API Keys)${RESET}
766
866
  BAMBOO_COMPANY_DOMAIN Company subdomain ${DIM}(e.g., "mycompany" for mycompany.bamboohr.com)${RESET}
@@ -772,24 +872,54 @@ ${styleText("bold", "Examples:")}
772
872
  ${DIM}$${RESET} bamboohr employee photo 123 --output ./photo.jpg
773
873
  ${DIM}$${RESET} bamboohr timeoff types --requestable
774
874
  ${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
875
  ${DIM}$${RESET} bamboohr timeoff requests --status requested --start-date 2026-01-01 --end-date 2026-12-31
777
876
  ${DIM}$${RESET} bamboohr timeoff whos-out
778
877
  ${DIM}$${RESET} bamboohr timeoff update-status 7890 --status approved
779
878
  ${DIM}$${RESET} bamboohr file list
780
879
  ${DIM}$${RESET} bamboohr meta fields
781
880
  `
782
- );
783
- program.option("--pretty", "Pretty-print JSON output");
784
- program.hook("preAction", (thisCommand) => {
785
- if (thisCommand.optsWithGlobals().pretty) setPretty(true);
786
- });
787
- registerEmployeeCommands(program);
788
- registerTimeoffCommands(program);
789
- registerFileCommands(program);
790
- registerMetaCommands(program);
791
- try {
792
- await program.parseAsync();
793
- } catch (err) {
794
- handleError(err);
881
+ );
882
+ program.option("--pretty", "Pretty-print JSON output");
883
+ program.hook("preAction", (thisCommand) => {
884
+ if (thisCommand.optsWithGlobals().pretty) setPretty(true);
885
+ });
886
+ registerEmployeeCommands(program);
887
+ registerTimeoffCommands(program);
888
+ registerFileCommands(program);
889
+ registerMetaCommands(program);
890
+ return program;
795
891
  }
892
+
893
+ // src/utils/credentials.ts
894
+ function getCredentialInfo() {
895
+ const companyDomain = process.env.BAMBOO_COMPANY_DOMAIN;
896
+ const token = process.env.BAMBOO_TOKEN;
897
+ return {
898
+ environment: {
899
+ BAMBOO_COMPANY_DOMAIN: {
900
+ value: companyDomain ?? null,
901
+ description: 'Company subdomain (e.g., "mycompany" for mycompany.bamboohr.com)'
902
+ },
903
+ BAMBOO_TOKEN: {
904
+ value: token ? "<set>" : null,
905
+ description: "BambooHR API token"
906
+ }
907
+ },
908
+ tokenUrl: `https://${companyDomain ?? "<companyDomain>"}.bamboohr.com/app/settings/permissions/api_keys`
909
+ };
910
+ }
911
+
912
+ // src/index.ts
913
+ await runCli(buildProgram(), {
914
+ credentialInfo: getCredentialInfo,
915
+ service: "BambooHR",
916
+ authRecovery: "Set the BAMBOO_TOKEN and BAMBOO_COMPANY_DOMAIN environment variables.",
917
+ networkRecovery: "Verify BAMBOO_COMPANY_DOMAIN is correct and you have internet connectivity.",
918
+ // BambooHR returns HTTP 404 for invalid credentials as well as genuinely
919
+ // missing resources — it cannot distinguish them, so keep not_found but
920
+ // document the ambiguity in the recovery hint (design §4.9).
921
+ adapt: (normalized) => normalized.status === 404 ? {
922
+ ...normalized,
923
+ recovery: "BambooHR returns 404 for invalid credentials too \u2014 verify BAMBOO_TOKEN and BAMBOO_COMPANY_DOMAIN, then confirm the resource id exists."
924
+ } : normalized
925
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bamboohr-cli",
3
- "version": "1.0.17",
3
+ "version": "2.0.0-g40cd2b1.10",
4
4
  "publish": true,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,8 +23,8 @@
23
23
  "typescript": "^5.7.2",
24
24
  "vitest": "^4.0.16",
25
25
  "cli-utils": "1.0.0",
26
- "config-eslint": "0.0.0",
27
- "config-typescript": "0.0.0"
26
+ "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 tests/integration"
38
+ "test:integration": "vitest run --config vitest.integration.config.ts"
39
39
  }
40
40
  }