jiradc-cli 1.0.31 → 1.0.32

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 +104 -12
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -263,10 +263,55 @@ var TYPE_EXIT = {
263
263
  auth: EXIT.AUTH,
264
264
  rate_limited: EXIT.GENERIC,
265
265
  server: EXIT.GENERIC,
266
+ timeout: EXIT.GENERIC,
266
267
  network: EXIT.GENERIC,
267
268
  unknown: EXIT.GENERIC
268
269
  };
269
- var NETWORK_CODES = ["ENOTFOUND", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"];
270
+ var TRANSPORT_CODES = {
271
+ /** Our own timeout fired: connected (or tried to), no response in time. */
272
+ readTimeout: ["ECONNABORTED"],
273
+ /** The OS gave up establishing the connection. */
274
+ connectTimeout: ["ETIMEDOUT"],
275
+ /** No such host, or DNS itself is unavailable. */
276
+ unresolved: ["ENOTFOUND", "EAI_AGAIN"],
277
+ /** Host is there, nothing is listening on that port. */
278
+ refused: ["ECONNREFUSED"],
279
+ /** Established, then died mid-flight. */
280
+ dropped: ["ECONNRESET", "EPIPE"]
281
+ };
282
+ function humanMs(ms) {
283
+ return ms % 1e3 === 0 ? `${ms / 1e3}s` : `${ms}ms`;
284
+ }
285
+ function targetHost(err) {
286
+ const config = err?.config;
287
+ const raw = config?.baseURL ?? config?.url;
288
+ if (raw === void 0 || raw === "")
289
+ return void 0;
290
+ try {
291
+ return new URL(raw).host;
292
+ } catch {
293
+ return void 0;
294
+ }
295
+ }
296
+ function configuredTimeoutMs(err) {
297
+ const t = err?.config?.timeout;
298
+ return typeof t === "number" && t > 0 ? t : void 0;
299
+ }
300
+ function retryAfterSeconds(err) {
301
+ const headers = err?.response?.headers;
302
+ const raw = headers?.["retry-after"];
303
+ if (raw === void 0 || raw === "")
304
+ return void 0;
305
+ const seconds = Number(raw);
306
+ if (Number.isFinite(seconds))
307
+ return Math.max(0, Math.round(seconds));
308
+ if (typeof raw !== "string")
309
+ return void 0;
310
+ const at = Date.parse(raw);
311
+ if (Number.isNaN(at))
312
+ return void 0;
313
+ return Math.max(0, Math.round((at - Date.now()) / 1e3));
314
+ }
270
315
  function httpStatus(err) {
271
316
  const e = err;
272
317
  return e?.response?.status ?? e?.statusCode;
@@ -366,21 +411,24 @@ function normalize(err, opts) {
366
411
  retryable: false,
367
412
  detail
368
413
  };
369
- case 429:
414
+ case 429: {
415
+ const wait = retryAfterSeconds(err);
370
416
  return {
371
417
  type: "rate_limited",
372
418
  status,
373
- message: "Rate limited",
374
- recovery: "Wait and retry the request.",
375
- retryable: true
419
+ message: `${opts.service} is rate limiting these requests`,
420
+ recovery: wait === void 0 ? `${opts.service} did not say how long to wait. Pause a few seconds before retrying, and make fewer requests at once.` : `${opts.service} asked for a ${wait}s pause before the next request. Wait at least that long, then retry.`,
421
+ retryable: true,
422
+ ...wait === void 0 ? {} : { detail: { retryAfterSeconds: wait } }
376
423
  };
424
+ }
377
425
  }
378
426
  if (status >= 500) {
379
427
  return {
380
428
  type: "server",
381
429
  status,
382
- message: `Server error (HTTP ${status})`,
383
- recovery: `${opts.service} returned an internal error; retry shortly.`,
430
+ message: status === 503 ? `${opts.service} returned 503 (service unavailable)` : `${opts.service} returned ${status}`,
431
+ recovery: status === 503 ? `${opts.service} is temporarily refusing work. Retry shortly.` : `${opts.service} failed to complete the request. Usually temporary, so retry. If it persists, the request itself may be at fault.`,
384
432
  retryable: true,
385
433
  detail
386
434
  };
@@ -395,15 +443,59 @@ function normalize(err, opts) {
395
443
  };
396
444
  }
397
445
  const code = errorCode(err);
398
- if (code && NETWORK_CODES.includes(code) || NETWORK_CODES.some((c) => message.includes(c))) {
446
+ const transport = classifyTransport(err, code, message, opts);
447
+ if (transport)
448
+ return transport;
449
+ return { type: "unknown", message, recovery: "Unexpected error; inspect the message.", retryable: false };
450
+ }
451
+ function classifyTransport(err, code, message, opts) {
452
+ const matches = (codes) => code !== void 0 && codes.includes(code) || codes.some((c) => message.includes(c));
453
+ const host = targetHost(err);
454
+ const where = host ?? opts.service;
455
+ const urlVar = opts.urlEnvVar ?? "the base-URL environment variable";
456
+ if (matches(TRANSPORT_CODES.readTimeout)) {
457
+ const limit = configuredTimeoutMs(err);
458
+ return {
459
+ type: "timeout",
460
+ message: `${opts.service} did not respond${limit === void 0 ? " in time" : ` within ${humanMs(limit)}`}`,
461
+ recovery: `The request reached ${opts.service} but no response came back in time. Usually the server is busy or the query is too large. Retry, or ask for less: fewer results per page, fewer fields, a narrower query.`,
462
+ retryable: true
463
+ };
464
+ }
465
+ if (matches(TRANSPORT_CODES.connectTimeout)) {
466
+ const limit = configuredTimeoutMs(err);
467
+ return {
468
+ type: "timeout",
469
+ message: `Could not open a connection to ${where}${limit === void 0 ? "" : ` within ${humanMs(limit)}`}`,
470
+ recovery: `The host did not accept a connection in time. If it is reachable at all it is likely overloaded, so retry. If this repeats, confirm ${urlVar} points at a host this machine can reach.`,
471
+ retryable: true
472
+ };
473
+ }
474
+ if (matches(TRANSPORT_CODES.unresolved)) {
475
+ return {
476
+ type: "network",
477
+ message: `The host ${where} does not resolve`,
478
+ recovery: `${urlVar} points at a hostname that cannot be looked up from this machine. Check it for a typo. Retrying will not help until it changes.`,
479
+ retryable: false
480
+ };
481
+ }
482
+ if (matches(TRANSPORT_CODES.refused)) {
399
483
  return {
400
484
  type: "network",
401
- message: `Cannot connect to ${opts.service}: ${message}`,
402
- recovery: opts.networkRecovery ?? "Verify the *_URL is correct, the server is reachable, and you are on the VPN if required.",
485
+ message: `Nothing accepted a connection at ${where}`,
486
+ recovery: `The host resolved but refused the connection, usually a wrong port or a service that is not running. Retrying will not help until that changes.`,
487
+ retryable: false
488
+ };
489
+ }
490
+ if (matches(TRANSPORT_CODES.dropped)) {
491
+ return {
492
+ type: "network",
493
+ message: `The connection to ${opts.service} closed before a response arrived`,
494
+ recovery: `The connection dropped mid-request. This is usually a blip, so retry.`,
403
495
  retryable: true
404
496
  };
405
497
  }
406
- return { type: "unknown", message, recovery: "Unexpected error; inspect the message.", retryable: false };
498
+ return void 0;
407
499
  }
408
500
  function classifyError(err, opts) {
409
501
  const base = normalize(err, opts);
@@ -4033,5 +4125,5 @@ await runCli(buildProgram(), {
4033
4125
  credentialInfo: getCredentialInfo,
4034
4126
  service: "Jira",
4035
4127
  authRecovery: "Set the JIRA_URL and JIRA_TOKEN environment variables.",
4036
- networkRecovery: "Verify JIRA_URL is correct, the server is reachable, and you are on the VPN if required."
4128
+ urlEnvVar: "JIRA_URL"
4037
4129
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiradc-cli",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "publish": true,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "commander": "^13.1.0",
15
15
  "zod": "^3.25.67",
16
- "jira-data-center-client": "1.0.41"
16
+ "jira-data-center-client": "1.0.42"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "24.10.4",
@@ -32,6 +32,8 @@
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsup",
35
+ "check-types": "tsc --noEmit -p tsconfig.json",
36
+ "clean": "rm -rf dist",
35
37
  "dev": "tsx src/index.ts",
36
38
  "lint": "eslint src tests",
37
39
  "lint:fix": "eslint src tests --fix",