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.
Files changed (2) hide show
  1. package/dist/index.js +321 -177
  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,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
- process.stderr.write(
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 [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(
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(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
- });
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 <id>").description("Get employee performance goals").addOption(new Option("--filter <filter>", "Filter goals by status").choices(GOAL_FILTERS).default("all")).addHelpText(
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 (id, opts) => {
486
+ ).action(async (employeeId, opts) => {
353
487
  const client = getClient();
354
488
  const result = await client.goals.get({
355
- employeeId: id,
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 <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(
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 (id, opts) => {
507
+ ).action(async (employeeId, opts) => {
374
508
  const client = getClient();
375
509
  const buffer = await client.employees.getPhoto({
376
- employeeId: id,
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(program2) {
443
- const employee = program2.command("employee").description("Employee operations").addHelpText(
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(program2) {
488
- const file = program2.command("file").description("Company file operations").addHelpText(
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(program2) {
541
- const meta = program2.command("meta").description("Metadata operations").addHelpText(
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.employee;
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.id,
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.type
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(program2) {
719
- const timeoff = program2.command("timeoff").description("Time off operations").addHelpText(
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/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
- }
861
+ // src/program.ts
750
862
  var DIM = "\x1B[2m";
751
863
  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", `
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
- "after",
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
- 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);
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": "1.0.17",
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 tests/integration"
38
+ "test:integration": "vitest run --config vitest.integration.config.ts"
39
39
  }
40
40
  }