@kolatts/pncli 1.3.0 → 1.6.0

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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # pncli — The Paperwork Nightmare CLI
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/%40kolatts%2Fpncli?style=flat-square&color=cb3837&logo=npm)](https://www.npmjs.com/package/@kolatts/pncli)
4
+ [![docs](https://img.shields.io/badge/docs-getting%20started-FF5C39?style=flat-square)](https://kolatts.github.io/pncli/getting-started/)
4
5
 
5
6
  > One command does what three meetings couldn't.
6
7
 
@@ -26,7 +27,7 @@ pncli config init
26
27
  pncli config init --repo
27
28
  ```
28
29
 
29
- For workflow patterns, command examples, and agent integration, see [`copilot-instructions.md`](./copilot-instructions.md).
30
+ For workflow patterns, command examples, and agent integration, see the [Getting Started page](https://kolatts.github.io/pncli/getting-started/) or the raw [`copilot-instructions.md`](./copilot-instructions.md).
30
31
 
31
32
  ## Configuration
32
33
 
@@ -49,8 +50,7 @@ pncli uses a three-layer config system (highest priority wins):
49
50
  | `PNCLI_BITBUCKET_PAT` | Bitbucket personal access token |
50
51
  | `PNCLI_SONAR_BASE_URL` | SonarQube Server base URL |
51
52
  | `PNCLI_SONAR_TOKEN` | SonarQube personal access token |
52
- | `PNCLI_SDE_BASE_URL` | SDElements base URL |
53
- | `PNCLI_SDE_TOKEN` | SDElements API token |
53
+ | `PNCLI_SDE_CONNECTION` | SDElements connection string (`api-token@hostname`, e.g. `mytoken@myorg.sdelements.com`) |
54
54
  | `PNCLI_CONFIG_PATH` | Override global config file path |
55
55
 
56
56
  ## For AI Agents
@@ -88,6 +88,8 @@ This project uses Conventional Commits for automatic versioning:
88
88
  | SonarQube | ✅ Active | Server Web API |
89
89
  | SDElements | ✅ Active | REST API v2 (cloud + on-prem) |
90
90
  | Artifactory | 🔜 Coming | The nightmare never ends |
91
+ | Jenkins | 🔜 Coming | REST API |
92
+ | Azure DevOps Server | ✅ Active | REST API v7.1 (on-prem, Windows auth + PAT) |
91
93
 
92
94
  ## License
93
95
 
@@ -13,8 +13,37 @@ pncli is a CLI tool that provides structured JSON access to Jira, Bitbucket, Con
13
13
  - Run all commands from the repository root.
14
14
  - Project and repo are auto-detected from git remotes. You rarely need `--project` or `--repo` flags.
15
15
 
16
+ ## Provider Detection
17
+
18
+ Before running provider-specific commands, ask the user which tools this repository uses:
19
+
20
+ 1. **Work item tracking:** "Does this repo use **Jira** or **Azure DevOps** for work items and tickets?" — this determines whether to use `pncli jira ...` or `pncli ado work ...` commands.
21
+ 2. **Source control:** "Does this repo use **Bitbucket** or **Azure DevOps** for pull requests and code review?" — this determines whether to use `pncli bitbucket ...` or `pncli ado repo ...` commands.
22
+
23
+ Cache the answers for the session. If the user doesn't know, run `git remote -v` — if a URL contains `/_git/` it's Azure DevOps; if it contains `/scm/` it's Bitbucket.
24
+
25
+ ## Installing Skills
26
+
27
+ pncli ships with Claude Code skills — step-by-step workflow guides that agents can follow. To install them into your repo:
28
+
29
+ ```
30
+ pncli skills install
31
+ ```
32
+
33
+ This downloads the latest pncli skills from GitHub into `.claude/skills/` and also fetches `copilot-instructions.md` into the repo root. Only pncli-managed skills are replaced — any custom skills you've created in `.claude/skills/` are left untouched.
34
+
35
+ Use `--skip-instructions` to skip downloading `copilot-instructions.md`.
36
+
37
+ To see what's installed locally:
38
+
39
+ ```
40
+ pncli skills list
41
+ ```
42
+
16
43
  ## Common Workflows
17
44
 
45
+ > These workflows are also available as Claude Code skills — run `pncli skills install` to download them into your repo, or copy `.claude/skills/` into `~/.claude/skills/` for global access.
46
+
18
47
  ### Review a Pull Request
19
48
 
20
49
  1. Find the PR for the current branch:
@@ -189,7 +218,10 @@ pncli bitbucket decline-pr
189
218
  --id <pr-id> Pull request ID
190
219
 
191
220
  pncli bitbucket list-comments
192
- --pr <pr-id> Pull request ID
221
+ --pr <pr-id> Pull request ID
222
+ --no-with-replies Exclude replies; return top-level comments only
223
+ --inline-only Return only inline file comments
224
+ --general-only Return only general (non-inline) PR comments
193
225
 
194
226
  pncli bitbucket add-comment
195
227
  --pr <pr-id> Pull request ID
@@ -493,8 +525,39 @@ pncli config init
493
525
  pncli config show
494
526
 
495
527
  pncli config set
528
+ --repo Write to repo config (.pncli.json) instead of global config
496
529
 
497
530
  pncli config test
531
+
532
+ pncli config check
533
+ ```
534
+
535
+ ### Ado
536
+
537
+ ```
538
+ pncli ado whoami
539
+
540
+ pncli ado connection-data
541
+
542
+ pncli ado project
543
+
544
+ pncli ado work
545
+
546
+ pncli ado repo
547
+
548
+ pncli ado pipeline
549
+ ```
550
+
551
+ ### Skills
552
+
553
+ ```
554
+ pncli skills install
555
+ --target <dir> Target directory for skills (default: ".claude/skills")
556
+ --skip-instructions Skip downloading copilot-instructions.md (always written
557
+ to repo root)
558
+
559
+ pncli skills list
560
+ --target <dir> Skills directory to scan (default: ".claude/skills")
498
561
  ```
499
562
 
500
563
  <!-- COMMAND-REFERENCE:END -->
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/http.ts
4
+ import fs2 from "fs";
5
+
6
+ // src/lib/errors.ts
7
+ var PncliError = class extends Error {
8
+ status;
9
+ url;
10
+ constructor(message, status = 1, url) {
11
+ super(message);
12
+ this.name = "PncliError";
13
+ this.status = status;
14
+ this.url = url;
15
+ }
16
+ };
17
+
18
+ // src/lib/exitCodes.ts
19
+ var ExitCode = {
20
+ SUCCESS: 0,
21
+ GENERAL_ERROR: 1,
22
+ USAGE_ERROR: 2,
23
+ NETWORK_ERROR: 69,
24
+ // EX_UNAVAILABLE from sysexits.h
25
+ AUTH_ERROR: 77,
26
+ // EX_NOPERM from sysexits.h
27
+ CONFIG_ERROR: 78
28
+ // EX_CONFIG from sysexits.h
29
+ };
30
+ function exitCodeFromStatus(httpStatus) {
31
+ if (httpStatus === 401 || httpStatus === 403) return ExitCode.AUTH_ERROR;
32
+ if (httpStatus === 0) return ExitCode.NETWORK_ERROR;
33
+ return ExitCode.GENERAL_ERROR;
34
+ }
35
+
36
+ // src/lib/output.ts
37
+ import fs from "fs";
38
+ import chalk from "chalk";
39
+ var globalOptions = { pretty: false, verbose: false };
40
+ var globalUser = { email: void 0, userId: void 0 };
41
+ function setGlobalOptions(opts) {
42
+ globalOptions = opts;
43
+ }
44
+ function setGlobalUser(user) {
45
+ globalUser = user;
46
+ }
47
+ function buildMeta(service, action, startTime) {
48
+ return {
49
+ service,
50
+ action,
51
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
52
+ duration_ms: Date.now() - startTime,
53
+ user: globalUser.email ?? globalUser.userId ? globalUser : void 0
54
+ };
55
+ }
56
+ function success(data, service, action, startTime) {
57
+ const envelope = {
58
+ ok: true,
59
+ data,
60
+ meta: buildMeta(service, action, startTime)
61
+ };
62
+ process.stdout.write(
63
+ (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n"
64
+ );
65
+ }
66
+ function fail(err, service, action, startTime) {
67
+ const errorDetail = {
68
+ status: err instanceof PncliError ? err.status : 1,
69
+ message: err instanceof Error ? err.message : String(err),
70
+ url: err instanceof PncliError ? err.url ?? null : null
71
+ };
72
+ const envelope = {
73
+ ok: false,
74
+ error: errorDetail,
75
+ meta: buildMeta(service, action, startTime)
76
+ };
77
+ const msg = globalOptions.pretty ? chalk.red("\u2717 Error: ") + errorDetail.message : null;
78
+ if (msg) process.stderr.write(msg + "\n");
79
+ const output = (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n";
80
+ const exitCode = err instanceof PncliError ? exitCodeFromStatus(err.status) : ExitCode.GENERAL_ERROR;
81
+ try {
82
+ fs.writeSync(process.stdout.fd, output);
83
+ } catch (writeErr) {
84
+ if (writeErr.code !== "EPIPE") throw writeErr;
85
+ }
86
+ process.exitCode = exitCode;
87
+ throw new PncliError(errorDetail.message, errorDetail.status);
88
+ }
89
+ function log(message) {
90
+ if (globalOptions.verbose) {
91
+ process.stderr.write(
92
+ (globalOptions.pretty ? chalk.gray("\u25B8 ") : "") + message + "\n"
93
+ );
94
+ }
95
+ }
96
+ function warn(message) {
97
+ process.stderr.write(
98
+ (globalOptions.pretty ? chalk.yellow("\u26A0 ") : "") + message + "\n"
99
+ );
100
+ }
101
+
102
+ // src/lib/adoFetch.ts
103
+ async function buildAdoFetcher(config) {
104
+ const { pat, baseUrl } = config.ado;
105
+ if (!baseUrl) {
106
+ throw new PncliError("Azure DevOps baseUrl not configured. Run: pncli config init", 1);
107
+ }
108
+ if (!pat) {
109
+ throw new PncliError(
110
+ "Azure DevOps PAT not configured. Set a PAT via pncli config init or PNCLI_ADO_PAT env var.",
111
+ 1
112
+ );
113
+ }
114
+ const encoded = Buffer.from(":" + pat).toString("base64");
115
+ const authHeader = `Basic ${encoded}`;
116
+ return (url, init) => fetch(url, {
117
+ ...init,
118
+ headers: {
119
+ ...init?.headers,
120
+ "Authorization": authHeader,
121
+ "Content-Type": "application/json",
122
+ "Accept": "application/json"
123
+ }
124
+ });
125
+ }
126
+
127
+ // src/lib/http.ts
128
+ function buildUrl(base, path, params) {
129
+ const url = new URL(path, base.endsWith("/") ? base : base + "/");
130
+ if (params) {
131
+ for (const [key, val] of Object.entries(params)) {
132
+ if (val !== void 0) {
133
+ url.searchParams.set(key, String(val));
134
+ }
135
+ }
136
+ }
137
+ return url.toString();
138
+ }
139
+ async function fetchWithTimeout(url, init, timeoutMs, fetcher = fetch) {
140
+ const controller = new AbortController();
141
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
142
+ try {
143
+ return await fetcher(url, { ...init, signal: controller.signal });
144
+ } finally {
145
+ clearTimeout(timer);
146
+ }
147
+ }
148
+ async function request(url, init, timeoutMs, fetcher = fetch) {
149
+ let lastError;
150
+ for (let attempt = 0; attempt < 3; attempt++) {
151
+ let response;
152
+ try {
153
+ response = await fetchWithTimeout(url, init, timeoutMs, fetcher);
154
+ } catch (err) {
155
+ throw new PncliError(
156
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`,
157
+ 0,
158
+ url
159
+ );
160
+ }
161
+ if (response.status === 429) {
162
+ const retryAfter = response.headers.get("Retry-After");
163
+ const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
164
+ log(`Rate limited. Retrying after ${waitMs}ms...`);
165
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
166
+ lastError = new PncliError("Rate limited", 429, url);
167
+ continue;
168
+ }
169
+ if (!response.ok) {
170
+ let message = `HTTP ${response.status} ${response.statusText}`;
171
+ try {
172
+ const body = await response.text();
173
+ const parsed = JSON.parse(body);
174
+ const parts = [];
175
+ if (Array.isArray(parsed.errorMessages)) {
176
+ parts.push(...parsed.errorMessages.filter(Boolean));
177
+ }
178
+ if (parsed.errors && typeof parsed.errors === "object" && !Array.isArray(parsed.errors)) {
179
+ for (const [field, msg] of Object.entries(parsed.errors)) {
180
+ parts.push(`${field}: ${msg}`);
181
+ }
182
+ }
183
+ if (Array.isArray(parsed.errors)) {
184
+ for (const e of parsed.errors) {
185
+ if (e?.message) parts.push(e.message);
186
+ else if (e?.msg) parts.push(e.msg);
187
+ }
188
+ }
189
+ if (parts.length === 0 && parsed.message) {
190
+ parts.push(String(parsed.message));
191
+ }
192
+ if (parts.length > 0) message = parts.join("; ");
193
+ } catch {
194
+ }
195
+ throw new PncliError(message, response.status, url);
196
+ }
197
+ if (response.status === 204) {
198
+ return void 0;
199
+ }
200
+ const text = await response.text();
201
+ if (!text) return void 0;
202
+ return JSON.parse(text);
203
+ }
204
+ throw lastError ?? new PncliError("Request failed after retries", 1, url);
205
+ }
206
+ var HttpClient = class {
207
+ config;
208
+ dryRun;
209
+ adoFetcher = null;
210
+ constructor(config, dryRun = false) {
211
+ this.config = config;
212
+ this.dryRun = dryRun;
213
+ }
214
+ jiraHeaders() {
215
+ const { apiToken } = this.config.jira;
216
+ if (!apiToken) throw new PncliError("Jira credentials not configured. Run: pncli config init");
217
+ return {
218
+ "Authorization": `Bearer ${apiToken}`,
219
+ "Content-Type": "application/json",
220
+ "Accept": "application/json",
221
+ "Connection": "close"
222
+ };
223
+ }
224
+ bitbucketHeaders() {
225
+ const { pat } = this.config.bitbucket;
226
+ if (!pat) throw new PncliError("Bitbucket credentials not configured. Run: pncli config init");
227
+ return {
228
+ "Authorization": `Bearer ${pat}`,
229
+ "Content-Type": "application/json",
230
+ "Accept": "application/json",
231
+ "Connection": "close"
232
+ };
233
+ }
234
+ async jira(path, opts = {}) {
235
+ const baseUrl = this.config.jira.baseUrl;
236
+ if (!baseUrl) throw new PncliError("Jira baseUrl not configured. Run: pncli config init");
237
+ const url = buildUrl(baseUrl, path, opts.params);
238
+ const headers = this.jiraHeaders();
239
+ const init = {
240
+ method: opts.method ?? "GET",
241
+ headers,
242
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
243
+ };
244
+ if (this.dryRun) {
245
+ const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
246
+ const msg = `DRY RUN: ${init.method} ${url}
247
+ Headers: ${JSON.stringify(safeHeaders, null, 2)}
248
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
249
+ ` : "");
250
+ fs2.writeSync(process.stderr.fd, msg);
251
+ process.exitCode = ExitCode.SUCCESS;
252
+ throw new PncliError("dry-run", 0);
253
+ }
254
+ return request(url, init, opts.timeoutMs ?? 3e4);
255
+ }
256
+ async bitbucket(path, opts = {}) {
257
+ const baseUrl = this.config.bitbucket.baseUrl;
258
+ if (!baseUrl) throw new PncliError("Bitbucket baseUrl not configured. Run: pncli config init");
259
+ const url = buildUrl(baseUrl, path, opts.params);
260
+ const headers = this.bitbucketHeaders();
261
+ const init = {
262
+ method: opts.method ?? "GET",
263
+ headers,
264
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
265
+ };
266
+ if (this.dryRun) {
267
+ const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
268
+ const msg = `DRY RUN: ${init.method} ${url}
269
+ Headers: ${JSON.stringify(safeHeaders, null, 2)}
270
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
271
+ ` : "");
272
+ fs2.writeSync(process.stderr.fd, msg);
273
+ process.exitCode = ExitCode.SUCCESS;
274
+ throw new PncliError("dry-run", 0);
275
+ }
276
+ return request(url, init, opts.timeoutMs ?? 3e4);
277
+ }
278
+ confluenceHeaders() {
279
+ const { apiToken } = this.config.confluence;
280
+ if (!apiToken) throw new PncliError("Confluence credentials not configured. Run: pncli config init");
281
+ return {
282
+ "Authorization": `Bearer ${apiToken}`,
283
+ "Content-Type": "application/json",
284
+ "Accept": "application/json",
285
+ "Connection": "close"
286
+ };
287
+ }
288
+ async confluence(path, opts = {}) {
289
+ const baseUrl = this.config.confluence.baseUrl;
290
+ if (!baseUrl) throw new PncliError("Confluence baseUrl not configured. Run: pncli config init");
291
+ const url = buildUrl(baseUrl, path, opts.params);
292
+ const headers = this.confluenceHeaders();
293
+ const init = {
294
+ method: opts.method ?? "GET",
295
+ headers,
296
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
297
+ };
298
+ if (this.dryRun) {
299
+ const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
300
+ const msg = `DRY RUN: ${init.method} ${url}
301
+ Headers: ${JSON.stringify(safeHeaders, null, 2)}
302
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
303
+ ` : "");
304
+ fs2.writeSync(process.stderr.fd, msg);
305
+ process.exitCode = ExitCode.SUCCESS;
306
+ throw new PncliError("dry-run", 0);
307
+ }
308
+ return request(url, init, opts.timeoutMs ?? 3e4);
309
+ }
310
+ sdeHeaders() {
311
+ const { token } = this.config.sde;
312
+ if (!token) throw new PncliError("SDElements credentials not configured. Run: pncli config init");
313
+ return {
314
+ "Authorization": `Token ${token}`,
315
+ "Content-Type": "application/json",
316
+ "Accept": "application/json",
317
+ "Connection": "close"
318
+ };
319
+ }
320
+ async sde(path, opts = {}) {
321
+ const baseUrl = this.config.sde.baseUrl;
322
+ if (!baseUrl) throw new PncliError("SDElements baseUrl not configured. Run: pncli config init");
323
+ const url = buildUrl(baseUrl, path, opts.params);
324
+ const headers = this.sdeHeaders();
325
+ const init = {
326
+ method: opts.method ?? "GET",
327
+ headers,
328
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
329
+ };
330
+ if (this.dryRun) {
331
+ const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
332
+ const msg = `DRY RUN: ${init.method} ${url}
333
+ Headers: ${JSON.stringify(safeHeaders, null, 2)}
334
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
335
+ ` : "");
336
+ fs2.writeSync(process.stderr.fd, msg);
337
+ process.exitCode = ExitCode.SUCCESS;
338
+ throw new PncliError("dry-run", 0);
339
+ }
340
+ return request(url, init, opts.timeoutMs ?? 3e4);
341
+ }
342
+ async sdePaginate(fetchPage) {
343
+ const results = [];
344
+ let page = 1;
345
+ const pageSize = 100;
346
+ while (true) {
347
+ const response = await fetchPage(page, pageSize);
348
+ results.push(...response.results);
349
+ if (results.length >= response.count || response.results.length === 0) break;
350
+ page++;
351
+ }
352
+ return results;
353
+ }
354
+ async getAdoFetcher() {
355
+ if (!this.adoFetcher) {
356
+ this.adoFetcher = await buildAdoFetcher(this.config);
357
+ }
358
+ return this.adoFetcher;
359
+ }
360
+ async ado(path, opts = {}) {
361
+ const baseUrl = this.config.ado.baseUrl;
362
+ if (!baseUrl) throw new PncliError("Azure DevOps baseUrl not configured. Run: pncli config init");
363
+ const url = buildUrl(baseUrl, path, opts.params);
364
+ if (this.dryRun) {
365
+ const msg = `DRY RUN: ${opts.method ?? "GET"} ${url}
366
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
367
+ ` : "");
368
+ fs2.writeSync(process.stderr.fd, msg);
369
+ process.exitCode = ExitCode.SUCCESS;
370
+ throw new PncliError("dry-run", 0);
371
+ }
372
+ const fetcher = await this.getAdoFetcher();
373
+ const defaultHeaders = {
374
+ "Accept": "application/json",
375
+ "Content-Type": "application/json",
376
+ ...opts.headers
377
+ };
378
+ const init = {
379
+ method: opts.method ?? "GET",
380
+ headers: defaultHeaders,
381
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
382
+ };
383
+ return request(url, init, opts.timeoutMs ?? 3e4, fetcher);
384
+ }
385
+ async adoText(path, opts = {}) {
386
+ const baseUrl = this.config.ado.baseUrl;
387
+ if (!baseUrl) throw new PncliError("Azure DevOps baseUrl not configured. Run: pncli config init");
388
+ const url = buildUrl(baseUrl, path, opts.params);
389
+ if (this.dryRun) {
390
+ fs2.writeSync(process.stderr.fd, `DRY RUN: ${opts.method ?? "GET"} ${url}
391
+ `);
392
+ process.exitCode = ExitCode.SUCCESS;
393
+ throw new PncliError("dry-run", 0);
394
+ }
395
+ const fetcher = await this.getAdoFetcher();
396
+ const response = await fetchWithTimeout(url, {
397
+ method: opts.method ?? "GET",
398
+ headers: { "Accept": "text/plain", ...opts.headers }
399
+ }, opts.timeoutMs ?? 3e4, fetcher);
400
+ if (!response.ok) {
401
+ let message = `HTTP ${response.status} ${response.statusText}`;
402
+ try {
403
+ const parsed = JSON.parse(await response.text());
404
+ if (parsed.message) message = String(parsed.message);
405
+ } catch {
406
+ }
407
+ throw new PncliError(message, response.status, url);
408
+ }
409
+ return response.text();
410
+ }
411
+ async adoRaw(path, opts = {}) {
412
+ const baseUrl = this.config.ado.baseUrl;
413
+ if (!baseUrl) throw new PncliError("Azure DevOps baseUrl not configured. Run: pncli config init");
414
+ const url = buildUrl(baseUrl, path, opts.params);
415
+ if (this.dryRun) {
416
+ fs2.writeSync(process.stderr.fd, `DRY RUN: ${opts.method ?? "GET"} ${url}
417
+ `);
418
+ process.exitCode = ExitCode.SUCCESS;
419
+ throw new PncliError("dry-run", 0);
420
+ }
421
+ const fetcher = await this.getAdoFetcher();
422
+ const controller = new AbortController();
423
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 3e4);
424
+ let response;
425
+ try {
426
+ response = await fetcher(url, {
427
+ method: opts.method ?? "GET",
428
+ headers: { "Accept": "application/json", "Content-Type": "application/json", ...opts.headers },
429
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
430
+ signal: controller.signal
431
+ });
432
+ } finally {
433
+ clearTimeout(timer);
434
+ }
435
+ if (!response.ok) {
436
+ let message = `HTTP ${response.status} ${response.statusText}`;
437
+ try {
438
+ const parsed = JSON.parse(await response.text());
439
+ if (parsed.message) message = String(parsed.message);
440
+ } catch {
441
+ }
442
+ throw new PncliError(message, response.status, url);
443
+ }
444
+ const text = await response.text();
445
+ const data = text ? JSON.parse(text) : void 0;
446
+ return { data, headers: response.headers };
447
+ }
448
+ async adoPaginate(fetchPage) {
449
+ const results = [];
450
+ let token;
451
+ while (true) {
452
+ const { data, headers } = await fetchPage(token);
453
+ results.push(...data.value ?? []);
454
+ const next = headers.get("x-ms-continuationtoken");
455
+ if (!next) break;
456
+ token = next;
457
+ }
458
+ return results;
459
+ }
460
+ sonarHeaders() {
461
+ const { token } = this.config.sonar;
462
+ if (!token) throw new PncliError("SonarQube credentials not configured. Run: pncli config init");
463
+ return {
464
+ "Authorization": `Bearer ${token}`,
465
+ "Content-Type": "application/json",
466
+ "Accept": "application/json",
467
+ "Connection": "close"
468
+ };
469
+ }
470
+ async sonar(path, opts = {}) {
471
+ const baseUrl = this.config.sonar.baseUrl;
472
+ if (!baseUrl) throw new PncliError("SonarQube baseUrl not configured. Run: pncli config init");
473
+ const url = buildUrl(baseUrl, path, opts.params);
474
+ const headers = this.sonarHeaders();
475
+ const init = {
476
+ method: opts.method ?? "GET",
477
+ headers,
478
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
479
+ };
480
+ if (this.dryRun) {
481
+ const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
482
+ const msg = `DRY RUN: ${init.method} ${url}
483
+ Headers: ${JSON.stringify(safeHeaders, null, 2)}
484
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
485
+ ` : "");
486
+ fs2.writeSync(process.stderr.fd, msg);
487
+ process.exitCode = ExitCode.SUCCESS;
488
+ throw new PncliError("dry-run", 0);
489
+ }
490
+ return request(url, init, opts.timeoutMs ?? 3e4);
491
+ }
492
+ async sonarPaginate(fetchPage) {
493
+ const results = [];
494
+ let page = 1;
495
+ const pageSize = 500;
496
+ while (true) {
497
+ const response = await fetchPage(page, pageSize);
498
+ results.push(...response.items);
499
+ if (results.length >= response.total || response.items.length === 0) break;
500
+ page++;
501
+ }
502
+ return results;
503
+ }
504
+ async confluencePaginate(fetchPage) {
505
+ const results = [];
506
+ let start = 0;
507
+ const limit = 25;
508
+ while (true) {
509
+ const page = await fetchPage(start, limit);
510
+ results.push(...page.results);
511
+ if (!page._links.next) break;
512
+ start += page.size;
513
+ }
514
+ return results;
515
+ }
516
+ async paginate(fetchPage) {
517
+ const results = [];
518
+ let start = 0;
519
+ const limit = 100;
520
+ while (true) {
521
+ const page = await fetchPage(start, limit);
522
+ results.push(...page.values);
523
+ if (page.isLastPage) break;
524
+ start = page.nextPageStart ?? start + limit;
525
+ }
526
+ return results;
527
+ }
528
+ async jiraPaginate(fetchPage) {
529
+ const results = [];
530
+ let startAt = 0;
531
+ const maxResults = 100;
532
+ while (true) {
533
+ const page = await fetchPage(startAt, maxResults);
534
+ const items = page.issues ?? page.values ?? [];
535
+ results.push(...items);
536
+ startAt += items.length;
537
+ if (startAt >= page.total || items.length === 0) break;
538
+ }
539
+ return results;
540
+ }
541
+ };
542
+ function createHttpClient(config, dryRun = false) {
543
+ return new HttpClient(config, dryRun);
544
+ }
545
+
546
+ export {
547
+ PncliError,
548
+ ExitCode,
549
+ setGlobalOptions,
550
+ setGlobalUser,
551
+ success,
552
+ fail,
553
+ log,
554
+ warn,
555
+ HttpClient,
556
+ createHttpClient
557
+ };
558
+ //# sourceMappingURL=chunk-HWXXNMGE.js.map