@kolatts/pncli 1.3.0 → 1.4.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/dist/cli.js CHANGED
@@ -1,276 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getGlobalConfigPath,
4
+ loadConfig,
5
+ maskConfig,
6
+ setConfigValue,
7
+ writeGlobalConfig,
8
+ writeRepoConfig
9
+ } from "./chunk-JORIICS2.js";
10
+ import {
11
+ ExitCode,
12
+ PncliError,
13
+ createHttpClient,
14
+ fail,
15
+ log,
16
+ setGlobalOptions,
17
+ setGlobalUser,
18
+ success,
19
+ warn
20
+ } from "./chunk-HWXXNMGE.js";
21
+ import {
22
+ AdoCoreClient
23
+ } from "./chunk-KT3XINLJ.js";
2
24
 
3
25
  // src/cli.ts
4
26
  import { Command } from "commander";
5
27
  import { createRequire } from "module";
6
28
 
7
- // src/lib/output.ts
8
- import fs from "fs";
9
- import chalk from "chalk";
10
-
11
- // src/lib/errors.ts
12
- var PncliError = class extends Error {
13
- status;
14
- url;
15
- constructor(message, status = 1, url) {
16
- super(message);
17
- this.name = "PncliError";
18
- this.status = status;
19
- this.url = url;
20
- }
21
- };
22
-
23
- // src/lib/exitCodes.ts
24
- var ExitCode = {
25
- SUCCESS: 0,
26
- GENERAL_ERROR: 1,
27
- USAGE_ERROR: 2,
28
- NETWORK_ERROR: 69,
29
- // EX_UNAVAILABLE from sysexits.h
30
- AUTH_ERROR: 77,
31
- // EX_NOPERM from sysexits.h
32
- CONFIG_ERROR: 78
33
- // EX_CONFIG from sysexits.h
34
- };
35
- function exitCodeFromStatus(httpStatus) {
36
- if (httpStatus === 401 || httpStatus === 403) return ExitCode.AUTH_ERROR;
37
- if (httpStatus === 0) return ExitCode.NETWORK_ERROR;
38
- return ExitCode.GENERAL_ERROR;
39
- }
40
-
41
- // src/lib/output.ts
42
- var globalOptions = { pretty: false, verbose: false };
43
- var globalUser = { email: void 0, userId: void 0 };
44
- function setGlobalOptions(opts) {
45
- globalOptions = opts;
46
- }
47
- function setGlobalUser(user) {
48
- globalUser = user;
49
- }
50
- function buildMeta(service, action, startTime) {
51
- return {
52
- service,
53
- action,
54
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
55
- duration_ms: Date.now() - startTime,
56
- user: globalUser.email ?? globalUser.userId ? globalUser : void 0
57
- };
58
- }
59
- function success(data, service, action, startTime) {
60
- const envelope = {
61
- ok: true,
62
- data,
63
- meta: buildMeta(service, action, startTime)
64
- };
65
- process.stdout.write(
66
- (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n"
67
- );
68
- }
69
- function fail(err, service, action, startTime) {
70
- const errorDetail = {
71
- status: err instanceof PncliError ? err.status : 1,
72
- message: err instanceof Error ? err.message : String(err),
73
- url: err instanceof PncliError ? err.url ?? null : null
74
- };
75
- const envelope = {
76
- ok: false,
77
- error: errorDetail,
78
- meta: buildMeta(service, action, startTime)
79
- };
80
- const msg = globalOptions.pretty ? chalk.red("\u2717 Error: ") + errorDetail.message : null;
81
- if (msg) process.stderr.write(msg + "\n");
82
- const output = (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n";
83
- const exitCode = err instanceof PncliError ? exitCodeFromStatus(err.status) : ExitCode.GENERAL_ERROR;
84
- try {
85
- fs.writeSync(process.stdout.fd, output);
86
- } catch (writeErr) {
87
- if (writeErr.code !== "EPIPE") throw writeErr;
88
- }
89
- process.exitCode = exitCode;
90
- throw new PncliError(errorDetail.message, errorDetail.status);
91
- }
92
- function log(message) {
93
- if (globalOptions.verbose) {
94
- process.stderr.write(
95
- (globalOptions.pretty ? chalk.gray("\u25B8 ") : "") + message + "\n"
96
- );
97
- }
98
- }
99
- function warn(message) {
100
- process.stderr.write(
101
- (globalOptions.pretty ? chalk.yellow("\u26A0 ") : "") + message + "\n"
102
- );
103
- }
104
-
105
- // src/lib/config.ts
106
- import fs2 from "fs";
107
- import os from "os";
108
- import path from "path";
109
- import { execSync } from "child_process";
110
- var ENV_KEYS = {
111
- EMAIL: "PNCLI_EMAIL",
112
- USERID: "PNCLI_USERID",
113
- JIRA_BASE_URL: "PNCLI_JIRA_BASE_URL",
114
- JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
115
- BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
116
- BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
117
- CONFLUENCE_BASE_URL: "PNCLI_CONFLUENCE_BASE_URL",
118
- CONFLUENCE_API_TOKEN: "PNCLI_CONFLUENCE_API_TOKEN",
119
- ARTIFACTORY_BASE_URL: "PNCLI_ARTIFACTORY_BASE_URL",
120
- ARTIFACTORY_TOKEN: "PNCLI_ARTIFACTORY_TOKEN",
121
- ARTIFACTORY_REPO_NPM: "PNCLI_ARTIFACTORY_REPO_NPM",
122
- ARTIFACTORY_REPO_NUGET: "PNCLI_ARTIFACTORY_REPO_NUGET",
123
- ARTIFACTORY_REPO_MAVEN: "PNCLI_ARTIFACTORY_REPO_MAVEN",
124
- SONAR_BASE_URL: "PNCLI_SONAR_BASE_URL",
125
- SONAR_TOKEN: "PNCLI_SONAR_TOKEN",
126
- SDE_BASE_URL: "PNCLI_SDE_BASE_URL",
127
- SDE_TOKEN: "PNCLI_SDE_TOKEN",
128
- CONFIG_PATH: "PNCLI_CONFIG_PATH"
129
- };
130
- function getGlobalConfigPath(overridePath) {
131
- if (overridePath) return overridePath;
132
- const envPath = process.env[ENV_KEYS.CONFIG_PATH];
133
- if (envPath) return envPath;
134
- return path.join(os.homedir(), ".pncli", "config.json");
135
- }
136
- function loadJsonFile(filePath) {
137
- try {
138
- const content = fs2.readFileSync(filePath, "utf8");
139
- return JSON.parse(content);
140
- } catch {
141
- return null;
142
- }
143
- }
144
- function getRepoRoot() {
145
- try {
146
- return execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
147
- } catch {
148
- return null;
149
- }
150
- }
151
- function mergeCustomFields(global, repo) {
152
- const map = /* @__PURE__ */ new Map();
153
- for (const f of global ?? []) map.set(f.id, f);
154
- for (const f of repo ?? []) map.set(f.id, f);
155
- return Array.from(map.values());
156
- }
157
- function mergeDefaults(global, repo) {
158
- return {
159
- jira: { ...global?.jira, ...repo?.jira },
160
- bitbucket: { ...global?.bitbucket, ...repo?.bitbucket },
161
- sonar: { ...global?.sonar, ...repo?.sonar },
162
- sde: { ...global?.sde, ...repo?.sde }
163
- };
164
- }
165
- function loadConfig(opts = {}) {
166
- const globalConfigPath = getGlobalConfigPath(opts.configPath);
167
- const globalConfig = loadJsonFile(globalConfigPath) ?? {};
168
- const repoRoot = getRepoRoot();
169
- let repoConfig = {};
170
- if (repoRoot) {
171
- repoConfig = loadJsonFile(path.join(repoRoot, ".pncli.json")) ?? {};
172
- }
173
- const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);
174
- return {
175
- user: {
176
- email: process.env[ENV_KEYS.EMAIL] ?? globalConfig.user?.email,
177
- userId: process.env[ENV_KEYS.USERID] ?? globalConfig.user?.userId
178
- },
179
- jira: {
180
- baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,
181
- apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,
182
- customFields: mergeCustomFields(globalConfig.jira?.customFields, repoConfig.jira?.customFields)
183
- },
184
- bitbucket: {
185
- baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
186
- pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
187
- },
188
- confluence: {
189
- baseUrl: process.env[ENV_KEYS.CONFLUENCE_BASE_URL] ?? globalConfig.confluence?.baseUrl,
190
- apiToken: process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken ?? process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken
191
- },
192
- artifactory: {
193
- baseUrl: process.env[ENV_KEYS.ARTIFACTORY_BASE_URL] ?? globalConfig.artifactory?.baseUrl,
194
- token: process.env[ENV_KEYS.ARTIFACTORY_TOKEN] ?? globalConfig.artifactory?.token,
195
- npmRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NPM] ?? globalConfig.artifactory?.npmRepo,
196
- nugetRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NUGET] ?? globalConfig.artifactory?.nugetRepo,
197
- mavenRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_MAVEN] ?? globalConfig.artifactory?.mavenRepo
198
- },
199
- sonar: {
200
- baseUrl: process.env[ENV_KEYS.SONAR_BASE_URL] ?? globalConfig.sonar?.baseUrl,
201
- token: process.env[ENV_KEYS.SONAR_TOKEN] ?? globalConfig.sonar?.token
202
- },
203
- sde: {
204
- baseUrl: process.env[ENV_KEYS.SDE_BASE_URL] ?? globalConfig.sde?.baseUrl,
205
- token: process.env[ENV_KEYS.SDE_TOKEN] ?? globalConfig.sde?.token
206
- },
207
- defaults: mergedDefaults
208
- };
209
- }
210
- function writeGlobalConfig(config, configPath) {
211
- const filePath = getGlobalConfigPath(configPath);
212
- const dir = path.dirname(filePath);
213
- if (!fs2.existsSync(dir)) {
214
- fs2.mkdirSync(dir, { recursive: true });
215
- }
216
- fs2.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
217
- }
218
- function writeRepoConfig(config) {
219
- const repoRoot = getRepoRoot();
220
- const targetDir = repoRoot ?? process.cwd();
221
- fs2.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
222
- }
223
- function setConfigValue(key, value, configPath) {
224
- const filePath = getGlobalConfigPath(configPath);
225
- const existing = loadJsonFile(filePath) ?? {};
226
- const parts = key.split(".");
227
- let current = existing;
228
- for (let i = 0; i < parts.length - 1; i++) {
229
- const part = parts[i];
230
- if (typeof current[part] !== "object" || current[part] === null) {
231
- current[part] = {};
232
- }
233
- current = current[part];
234
- }
235
- current[parts[parts.length - 1]] = value;
236
- writeGlobalConfig(existing, configPath);
237
- }
238
- function maskConfig(config) {
239
- return {
240
- ...config,
241
- jira: {
242
- ...config.jira,
243
- apiToken: config.jira.apiToken ? "***" : void 0,
244
- customFields: config.jira.customFields
245
- },
246
- bitbucket: {
247
- ...config.bitbucket,
248
- pat: config.bitbucket.pat ? "***" : void 0
249
- },
250
- confluence: {
251
- ...config.confluence,
252
- apiToken: config.confluence.apiToken ? "***" : void 0
253
- },
254
- artifactory: {
255
- ...config.artifactory,
256
- token: config.artifactory.token ? "***" : void 0
257
- },
258
- sonar: {
259
- ...config.sonar,
260
- token: config.sonar.token ? "***" : void 0
261
- },
262
- sde: {
263
- ...config.sde,
264
- token: config.sde.token ? "***" : void 0
265
- }
266
- };
267
- }
268
-
269
29
  // src/services/git/client.ts
270
- import { execSync as execSync2, execFileSync } from "child_process";
30
+ import { execSync, execFileSync } from "child_process";
271
31
  var DIFF_LINE_LIMIT = 5e3;
272
32
  function exec(cmd, cwd) {
273
- return execSync2(cmd, { encoding: "utf8", cwd }).trim();
33
+ return execSync(cmd, { encoding: "utf8", cwd }).trim();
274
34
  }
275
35
  function getStatus(root) {
276
36
  const output = exec("git status --porcelain", root);
@@ -402,17 +162,17 @@ function getBranches(root) {
402
162
  }
403
163
 
404
164
  // src/lib/git-context.ts
405
- import { execSync as execSync3 } from "child_process";
406
- function getRepoRoot2() {
165
+ import { execSync as execSync2 } from "child_process";
166
+ function getRepoRoot() {
407
167
  try {
408
- return execSync3("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
168
+ return execSync2("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
409
169
  } catch {
410
170
  return null;
411
171
  }
412
172
  }
413
173
  function getCurrentBranch(repoRoot) {
414
174
  try {
415
- return execSync3("git rev-parse --abbrev-ref HEAD", {
175
+ return execSync2("git rev-parse --abbrev-ref HEAD", {
416
176
  encoding: "utf8",
417
177
  cwd: repoRoot
418
178
  }).trim();
@@ -439,16 +199,59 @@ function parseRemote(remoteUrl, bitbucketBaseUrl) {
439
199
  }
440
200
  return null;
441
201
  }
202
+ function parseAdoRemote(remoteUrl, adoBaseUrl) {
203
+ if (!adoBaseUrl) return null;
204
+ let baseHostname;
205
+ try {
206
+ const normalizedBase = /^https?:\/\//.test(adoBaseUrl) ? adoBaseUrl : `https://${adoBaseUrl}`;
207
+ baseHostname = new URL(normalizedBase).hostname.toLowerCase();
208
+ } catch {
209
+ baseHostname = adoBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "").split(/[:/]/)[0].toLowerCase();
210
+ }
211
+ let path5 = remoteUrl;
212
+ let remoteHostname = "";
213
+ const sshProtoMatch = path5.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
214
+ if (sshProtoMatch) {
215
+ remoteHostname = sshProtoMatch[1].toLowerCase();
216
+ path5 = sshProtoMatch[2];
217
+ } else {
218
+ const gitAtMatch = path5.match(/^git@([^:]+)(?::\d+)?:(.*)/);
219
+ if (gitAtMatch) {
220
+ remoteHostname = gitAtMatch[1].toLowerCase();
221
+ path5 = "/" + gitAtMatch[2];
222
+ } else {
223
+ const httpsMatch = path5.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
224
+ if (httpsMatch) {
225
+ remoteHostname = httpsMatch[1].toLowerCase();
226
+ path5 = httpsMatch[2];
227
+ }
228
+ }
229
+ }
230
+ if (!remoteHostname || remoteHostname !== baseHostname) return null;
231
+ path5 = path5.replace(/\.git$/, "");
232
+ const gitIdx = path5.indexOf("/_git/");
233
+ const sshIdx = path5.indexOf("/_ssh/");
234
+ const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
235
+ if (markerIdx === -1) return null;
236
+ const repo = path5.slice(markerIdx + 6);
237
+ if (!repo) return null;
238
+ const before = path5.slice(1, markerIdx);
239
+ const parts = before.split("/").filter(Boolean);
240
+ if (parts.length < 2) return null;
241
+ const project = parts[parts.length - 1];
242
+ const collection = parts[parts.length - 2];
243
+ return { collection, project, repo };
244
+ }
442
245
  function getRemoteUrls(repoRoot) {
443
246
  try {
444
- const output = execSync3("git remote -v", { encoding: "utf8", cwd: repoRoot });
247
+ const output = execSync2("git remote -v", { encoding: "utf8", cwd: repoRoot });
445
248
  return output.split("\n").filter((line) => line.includes("(fetch)")).map((line) => line.split(" ")[1]?.split(" ")[0] ?? "").filter(Boolean);
446
249
  } catch {
447
250
  return [];
448
251
  }
449
252
  }
450
253
  function getGitContext(config) {
451
- const root = getRepoRoot2();
254
+ const root = getRepoRoot();
452
255
  if (!root) return null;
453
256
  const branch = getCurrentBranch(root) ?? "unknown";
454
257
  const remoteUrls = getRemoteUrls(root);
@@ -462,320 +265,15 @@ function getGitContext(config) {
462
265
  break;
463
266
  }
464
267
  }
465
- return { root, branch, project, repo };
466
- }
467
-
468
- // src/lib/http.ts
469
- import fs3 from "fs";
470
- function buildUrl(base, path6, params) {
471
- const url = new URL(path6, base.endsWith("/") ? base : base + "/");
472
- if (params) {
473
- for (const [key, val] of Object.entries(params)) {
474
- if (val !== void 0) {
475
- url.searchParams.set(key, String(val));
476
- }
477
- }
478
- }
479
- return url.toString();
480
- }
481
- async function fetchWithTimeout(url, init, timeoutMs) {
482
- const controller = new AbortController();
483
- const timer = setTimeout(() => controller.abort(), timeoutMs);
484
- try {
485
- return await fetch(url, { ...init, signal: controller.signal });
486
- } finally {
487
- clearTimeout(timer);
488
- }
489
- }
490
- async function request(url, init, timeoutMs) {
491
- let lastError;
492
- for (let attempt = 0; attempt < 3; attempt++) {
493
- let response;
494
- try {
495
- response = await fetchWithTimeout(url, init, timeoutMs);
496
- } catch (err) {
497
- throw new PncliError(
498
- `Request failed: ${err instanceof Error ? err.message : String(err)}`,
499
- 0,
500
- url
501
- );
502
- }
503
- if (response.status === 429) {
504
- const retryAfter = response.headers.get("Retry-After");
505
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
506
- log(`Rate limited. Retrying after ${waitMs}ms...`);
507
- await new Promise((resolve) => setTimeout(resolve, waitMs));
508
- lastError = new PncliError("Rate limited", 429, url);
509
- continue;
510
- }
511
- if (!response.ok) {
512
- let message = `HTTP ${response.status} ${response.statusText}`;
513
- try {
514
- const body = await response.text();
515
- const parsed = JSON.parse(body);
516
- const parts = [];
517
- if (Array.isArray(parsed.errorMessages)) {
518
- parts.push(...parsed.errorMessages.filter(Boolean));
519
- }
520
- if (parsed.errors && typeof parsed.errors === "object" && !Array.isArray(parsed.errors)) {
521
- for (const [field, msg] of Object.entries(parsed.errors)) {
522
- parts.push(`${field}: ${msg}`);
523
- }
524
- }
525
- if (Array.isArray(parsed.errors)) {
526
- for (const e of parsed.errors) {
527
- if (e?.message) parts.push(e.message);
528
- else if (e?.msg) parts.push(e.msg);
529
- }
530
- }
531
- if (parts.length === 0 && parsed.message) {
532
- parts.push(String(parsed.message));
533
- }
534
- if (parts.length > 0) message = parts.join("; ");
535
- } catch {
536
- }
537
- throw new PncliError(message, response.status, url);
538
- }
539
- if (response.status === 204) {
540
- return void 0;
541
- }
542
- const text = await response.text();
543
- if (!text) return void 0;
544
- return JSON.parse(text);
545
- }
546
- throw lastError ?? new PncliError("Request failed after retries", 1, url);
547
- }
548
- var HttpClient = class {
549
- config;
550
- dryRun;
551
- constructor(config, dryRun = false) {
552
- this.config = config;
553
- this.dryRun = dryRun;
554
- }
555
- jiraHeaders() {
556
- const { apiToken } = this.config.jira;
557
- if (!apiToken) throw new PncliError("Jira credentials not configured. Run: pncli config init");
558
- return {
559
- "Authorization": `Bearer ${apiToken}`,
560
- "Content-Type": "application/json",
561
- "Accept": "application/json",
562
- "Connection": "close"
563
- };
564
- }
565
- bitbucketHeaders() {
566
- const { pat } = this.config.bitbucket;
567
- if (!pat) throw new PncliError("Bitbucket credentials not configured. Run: pncli config init");
568
- return {
569
- "Authorization": `Bearer ${pat}`,
570
- "Content-Type": "application/json",
571
- "Accept": "application/json",
572
- "Connection": "close"
573
- };
574
- }
575
- async jira(path6, opts = {}) {
576
- const baseUrl = this.config.jira.baseUrl;
577
- if (!baseUrl) throw new PncliError("Jira baseUrl not configured. Run: pncli config init");
578
- const url = buildUrl(baseUrl, path6, opts.params);
579
- const headers = this.jiraHeaders();
580
- const init = {
581
- method: opts.method ?? "GET",
582
- headers,
583
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
584
- };
585
- if (this.dryRun) {
586
- const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
587
- const msg = `DRY RUN: ${init.method} ${url}
588
- Headers: ${JSON.stringify(safeHeaders, null, 2)}
589
- ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
590
- ` : "");
591
- fs3.writeSync(process.stderr.fd, msg);
592
- process.exitCode = ExitCode.SUCCESS;
593
- throw new PncliError("dry-run", 0);
594
- }
595
- return request(url, init, opts.timeoutMs ?? 3e4);
596
- }
597
- async bitbucket(path6, opts = {}) {
598
- const baseUrl = this.config.bitbucket.baseUrl;
599
- if (!baseUrl) throw new PncliError("Bitbucket baseUrl not configured. Run: pncli config init");
600
- const url = buildUrl(baseUrl, path6, opts.params);
601
- const headers = this.bitbucketHeaders();
602
- const init = {
603
- method: opts.method ?? "GET",
604
- headers,
605
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
606
- };
607
- if (this.dryRun) {
608
- const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
609
- const msg = `DRY RUN: ${init.method} ${url}
610
- Headers: ${JSON.stringify(safeHeaders, null, 2)}
611
- ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
612
- ` : "");
613
- fs3.writeSync(process.stderr.fd, msg);
614
- process.exitCode = ExitCode.SUCCESS;
615
- throw new PncliError("dry-run", 0);
616
- }
617
- return request(url, init, opts.timeoutMs ?? 3e4);
618
- }
619
- confluenceHeaders() {
620
- const { apiToken } = this.config.confluence;
621
- if (!apiToken) throw new PncliError("Confluence credentials not configured. Run: pncli config init");
622
- return {
623
- "Authorization": `Bearer ${apiToken}`,
624
- "Content-Type": "application/json",
625
- "Accept": "application/json",
626
- "Connection": "close"
627
- };
628
- }
629
- async confluence(path6, opts = {}) {
630
- const baseUrl = this.config.confluence.baseUrl;
631
- if (!baseUrl) throw new PncliError("Confluence baseUrl not configured. Run: pncli config init");
632
- const url = buildUrl(baseUrl, path6, opts.params);
633
- const headers = this.confluenceHeaders();
634
- const init = {
635
- method: opts.method ?? "GET",
636
- headers,
637
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
638
- };
639
- if (this.dryRun) {
640
- const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
641
- const msg = `DRY RUN: ${init.method} ${url}
642
- Headers: ${JSON.stringify(safeHeaders, null, 2)}
643
- ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
644
- ` : "");
645
- fs3.writeSync(process.stderr.fd, msg);
646
- process.exitCode = ExitCode.SUCCESS;
647
- throw new PncliError("dry-run", 0);
648
- }
649
- return request(url, init, opts.timeoutMs ?? 3e4);
650
- }
651
- sdeHeaders() {
652
- const { token } = this.config.sde;
653
- if (!token) throw new PncliError("SDElements credentials not configured. Run: pncli config init");
654
- return {
655
- "Authorization": `Token ${token}`,
656
- "Content-Type": "application/json",
657
- "Accept": "application/json",
658
- "Connection": "close"
659
- };
660
- }
661
- async sde(path6, opts = {}) {
662
- const baseUrl = this.config.sde.baseUrl;
663
- if (!baseUrl) throw new PncliError("SDElements baseUrl not configured. Run: pncli config init");
664
- const url = buildUrl(baseUrl, path6, opts.params);
665
- const headers = this.sdeHeaders();
666
- const init = {
667
- method: opts.method ?? "GET",
668
- headers,
669
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
670
- };
671
- if (this.dryRun) {
672
- const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
673
- const msg = `DRY RUN: ${init.method} ${url}
674
- Headers: ${JSON.stringify(safeHeaders, null, 2)}
675
- ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
676
- ` : "");
677
- fs3.writeSync(process.stderr.fd, msg);
678
- process.exitCode = ExitCode.SUCCESS;
679
- throw new PncliError("dry-run", 0);
680
- }
681
- return request(url, init, opts.timeoutMs ?? 3e4);
682
- }
683
- async sdePaginate(fetchPage) {
684
- const results = [];
685
- let page = 1;
686
- const pageSize = 100;
687
- while (true) {
688
- const response = await fetchPage(page, pageSize);
689
- results.push(...response.results);
690
- if (results.length >= response.count || response.results.length === 0) break;
691
- page++;
268
+ let adoContext = null;
269
+ for (const url of remoteUrls) {
270
+ const parsed = parseAdoRemote(url, config.ado.baseUrl);
271
+ if (parsed) {
272
+ adoContext = parsed;
273
+ break;
692
274
  }
693
- return results;
694
- }
695
- sonarHeaders() {
696
- const { token } = this.config.sonar;
697
- if (!token) throw new PncliError("SonarQube credentials not configured. Run: pncli config init");
698
- return {
699
- "Authorization": `Bearer ${token}`,
700
- "Content-Type": "application/json",
701
- "Accept": "application/json",
702
- "Connection": "close"
703
- };
704
- }
705
- async sonar(path6, opts = {}) {
706
- const baseUrl = this.config.sonar.baseUrl;
707
- if (!baseUrl) throw new PncliError("SonarQube baseUrl not configured. Run: pncli config init");
708
- const url = buildUrl(baseUrl, path6, opts.params);
709
- const headers = this.sonarHeaders();
710
- const init = {
711
- method: opts.method ?? "GET",
712
- headers,
713
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
714
- };
715
- if (this.dryRun) {
716
- const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
717
- const msg = `DRY RUN: ${init.method} ${url}
718
- Headers: ${JSON.stringify(safeHeaders, null, 2)}
719
- ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
720
- ` : "");
721
- fs3.writeSync(process.stderr.fd, msg);
722
- process.exitCode = ExitCode.SUCCESS;
723
- throw new PncliError("dry-run", 0);
724
- }
725
- return request(url, init, opts.timeoutMs ?? 3e4);
726
- }
727
- async sonarPaginate(fetchPage) {
728
- const results = [];
729
- let page = 1;
730
- const pageSize = 500;
731
- while (true) {
732
- const response = await fetchPage(page, pageSize);
733
- results.push(...response.items);
734
- if (results.length >= response.total || response.items.length === 0) break;
735
- page++;
736
- }
737
- return results;
738
- }
739
- async confluencePaginate(fetchPage) {
740
- const results = [];
741
- let start = 0;
742
- const limit = 25;
743
- while (true) {
744
- const page = await fetchPage(start, limit);
745
- results.push(...page.results);
746
- if (!page._links.next) break;
747
- start += page.size;
748
- }
749
- return results;
750
- }
751
- async paginate(fetchPage) {
752
- const results = [];
753
- let start = 0;
754
- const limit = 100;
755
- while (true) {
756
- const page = await fetchPage(start, limit);
757
- results.push(...page.values);
758
- if (page.isLastPage) break;
759
- start = page.nextPageStart ?? start + limit;
760
- }
761
- return results;
762
- }
763
- async jiraPaginate(fetchPage) {
764
- const results = [];
765
- let startAt = 0;
766
- const maxResults = 100;
767
- while (true) {
768
- const page = await fetchPage(startAt, maxResults);
769
- const items = page.issues ?? page.values ?? [];
770
- results.push(...items);
771
- startAt += items.length;
772
- if (startAt >= page.total || items.length === 0) break;
773
- }
774
- return results;
775
275
  }
776
- };
777
- function createHttpClient(config, dryRun = false) {
778
- return new HttpClient(config, dryRun);
276
+ return { root, branch, project, repo, ado: adoContext };
779
277
  }
780
278
 
781
279
  // src/services/bitbucket/client.ts
@@ -860,14 +358,29 @@ var BitbucketClient = class {
860
358
  { method: "POST", params: { version } }
861
359
  );
862
360
  }
863
- async listComments(project, repo, prId) {
361
+ async listComments(project, repo, prId, opts = {}) {
362
+ const { withReplies = true, inlineOnly = false, generalOnly = false } = opts;
864
363
  const activities = await this.http.paginate(
865
364
  (start, limit) => this.http.bitbucket(
866
365
  `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/activities`,
867
366
  { params: { start, limit } }
868
367
  )
869
368
  );
870
- return activities.filter((a) => a.action === "COMMENTED" && a.comment).map((a) => a.comment);
369
+ const topLevel = activities.filter((a) => a.action === "COMMENTED" && a.comment).map((a) => a.comment);
370
+ if (!withReplies) {
371
+ return topLevel.filter((c) => !inlineOnly || !!c.anchor).filter((c) => !generalOnly || !c.anchor);
372
+ }
373
+ function flatten(comment, parentId) {
374
+ const node = {
375
+ ...comment,
376
+ comments: void 0,
377
+ ...parentId !== void 0 ? { parentId } : {}
378
+ };
379
+ const replies = (comment.comments ?? []).flatMap((c) => flatten(c, comment.id));
380
+ return [node, ...replies];
381
+ }
382
+ const flat = topLevel.flatMap((c) => flatten(c));
383
+ return flat.filter((c) => !inlineOnly || !!c.anchor).filter((c) => !generalOnly || !c.anchor).sort((a, b) => a.createdDate - b.createdDate);
871
384
  }
872
385
  async addComment(project, repo, prId, text) {
873
386
  return this.http.bitbucket(
@@ -968,7 +481,7 @@ var BitbucketClient = class {
968
481
 
969
482
  // src/services/git/commands.ts
970
483
  function requireRepoRoot() {
971
- const root = getRepoRoot2();
484
+ const root = getRepoRoot();
972
485
  if (!root) throw new PncliError("Not a git repository", 1);
973
486
  return root;
974
487
  }
@@ -1489,11 +1002,15 @@ function registerBitbucketCommands(program2) {
1489
1002
  fail(err, "bitbucket", "decline-pr", start);
1490
1003
  }
1491
1004
  });
1492
- bb.command("list-comments").description("List comments on a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
1005
+ bb.command("list-comments").description("List comments on a pull request (includes threaded replies by default)").requiredOption("--pr <pr-id>", "Pull request ID").option("--no-with-replies", "Exclude replies; return top-level comments only").option("--inline-only", "Return only inline file comments").option("--general-only", "Return only general (non-inline) PR comments").action(async (opts) => {
1493
1006
  const start = Date.now();
1494
1007
  try {
1495
1008
  const { client, project, repo } = getClient2(program2);
1496
- const data = await client.listComments(project, repo, parseInt(opts.pr, 10));
1009
+ const data = await client.listComments(project, repo, parseInt(opts.pr, 10), {
1010
+ withReplies: opts.withReplies,
1011
+ inlineOnly: opts.inlineOnly,
1012
+ generalOnly: opts.generalOnly
1013
+ });
1497
1014
  success(data, "bitbucket", "list-comments", start);
1498
1015
  } catch (err) {
1499
1016
  fail(err, "bitbucket", "list-comments", start);
@@ -2522,24 +2039,24 @@ function registerSdeCommands(program2) {
2522
2039
  }
2523
2040
 
2524
2041
  // src/services/deps/parsers/index.ts
2525
- import fs4 from "fs";
2526
- import path5 from "path";
2527
- import { execSync as execSync4, execFileSync as execFileSync2 } from "child_process";
2042
+ import fs from "fs";
2043
+ import path4 from "path";
2044
+ import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
2528
2045
 
2529
2046
  // src/services/deps/parsers/npm.ts
2530
- import path2 from "path";
2047
+ import path from "path";
2531
2048
  function findNpmManifests(files) {
2532
2049
  const lockFiles = new Set(
2533
2050
  files.filter((f) => {
2534
- const base = path2.basename(f);
2051
+ const base = path.basename(f);
2535
2052
  return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
2536
2053
  })
2537
2054
  );
2538
2055
  const manifests = [];
2539
2056
  for (const file of files) {
2540
- if (path2.basename(file) !== "package.json") continue;
2057
+ if (path.basename(file) !== "package.json") continue;
2541
2058
  if (file.split("/").includes("node_modules")) continue;
2542
- const dir = path2.dirname(file);
2059
+ const dir = path.dirname(file);
2543
2060
  let lockFile;
2544
2061
  for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
2545
2062
  const candidate = dir === "." ? lf : `${dir}/${lf}`;
@@ -2554,7 +2071,7 @@ function findNpmManifests(files) {
2554
2071
  }
2555
2072
  function parseNpmPackages(content, manifest, opts, lockContent) {
2556
2073
  if (lockContent && manifest.lockFile) {
2557
- const base = path2.basename(manifest.lockFile);
2074
+ const base = path.basename(manifest.lockFile);
2558
2075
  if (base === "package-lock.json") {
2559
2076
  const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
2560
2077
  if (pkgs.length > 0) return pkgs;
@@ -2702,26 +2219,26 @@ function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
2702
2219
  }
2703
2220
 
2704
2221
  // src/services/deps/parsers/nuget.ts
2705
- import path3 from "path";
2222
+ import path2 from "path";
2706
2223
  var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
2707
2224
  function findNugetManifests(files) {
2708
2225
  const lockFiles = /* @__PURE__ */ new Map();
2709
2226
  for (const f of files) {
2710
- if (path3.basename(f) === "packages.lock.json") {
2711
- lockFiles.set(path3.dirname(f), f);
2227
+ if (path2.basename(f) === "packages.lock.json") {
2228
+ lockFiles.set(path2.dirname(f), f);
2712
2229
  }
2713
2230
  }
2714
2231
  const manifests = [];
2715
2232
  for (const file of files) {
2716
- const base = path3.basename(file);
2717
- const ext = path3.extname(file);
2233
+ const base = path2.basename(file);
2234
+ const ext = path2.extname(file);
2718
2235
  if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
2719
- const dir = path3.dirname(file);
2236
+ const dir = path2.dirname(file);
2720
2237
  const lockFile = lockFiles.get(dir);
2721
2238
  manifests.push({ file, ecosystem: "nuget", lockFile });
2722
2239
  }
2723
2240
  if (base === "Directory.Packages.props") {
2724
- const dir = path3.dirname(file);
2241
+ const dir = path2.dirname(file);
2725
2242
  manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
2726
2243
  }
2727
2244
  }
@@ -2740,7 +2257,7 @@ function findNugetManifests(files) {
2740
2257
  return deduped;
2741
2258
  }
2742
2259
  function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
2743
- const base = path3.basename(manifest.file);
2260
+ const base = path2.basename(manifest.file);
2744
2261
  if (lockContent) {
2745
2262
  return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
2746
2263
  }
@@ -2861,14 +2378,14 @@ function parsePackagesLock(content, filePath, opts) {
2861
2378
  }
2862
2379
 
2863
2380
  // src/services/deps/parsers/maven.ts
2864
- import path4 from "path";
2381
+ import path3 from "path";
2865
2382
  function findMavenManifests(files) {
2866
- const lockFiles = new Set(files.filter((f) => path4.basename(f) === "gradle.lockfile"));
2383
+ const lockFiles = new Set(files.filter((f) => path3.basename(f) === "gradle.lockfile"));
2867
2384
  const manifests = [];
2868
2385
  for (const file of files) {
2869
- const base = path4.basename(file);
2386
+ const base = path3.basename(file);
2870
2387
  if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
2871
- const dir = path4.dirname(file);
2388
+ const dir = path3.dirname(file);
2872
2389
  const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
2873
2390
  const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
2874
2391
  manifests.push({ file, ecosystem: "maven", lockFile });
@@ -2877,7 +2394,7 @@ function findMavenManifests(files) {
2877
2394
  return manifests;
2878
2395
  }
2879
2396
  function parseMavenPackages(content, manifest, opts, lockContent) {
2880
- const base = path4.basename(manifest.file);
2397
+ const base = path3.basename(manifest.file);
2881
2398
  if (lockContent && manifest.lockFile) {
2882
2399
  return parseGradleLock(lockContent, manifest.lockFile, opts);
2883
2400
  }
@@ -3026,7 +2543,7 @@ function parseGradleLock(content, filePath, opts) {
3026
2543
  // src/services/deps/parsers/index.ts
3027
2544
  function getRepoFiles(repoRoot) {
3028
2545
  try {
3029
- const out = execSync4("git ls-files --cached --others --exclude-standard", {
2546
+ const out = execSync3("git ls-files --cached --others --exclude-standard", {
3030
2547
  encoding: "utf8",
3031
2548
  cwd: repoRoot,
3032
2549
  maxBuffer: 10 * 1024 * 1024
@@ -3038,7 +2555,7 @@ function getRepoFiles(repoRoot) {
3038
2555
  }
3039
2556
  function readFile(repoRoot, relPath) {
3040
2557
  try {
3041
- return fs4.readFileSync(path5.join(repoRoot, relPath), "utf8");
2558
+ return fs.readFileSync(path4.join(repoRoot, relPath), "utf8");
3042
2559
  } catch {
3043
2560
  return null;
3044
2561
  }
@@ -3056,13 +2573,13 @@ function readFileAtRef(repoRoot, ref, relPath) {
3056
2573
  }
3057
2574
  }
3058
2575
  function findPropsContent(manifestFile, readFn) {
3059
- let dir = path5.dirname(manifestFile);
2576
+ let dir = path4.dirname(manifestFile);
3060
2577
  for (let depth = 0; depth < 10; depth++) {
3061
2578
  const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
3062
2579
  const content = readFn(candidate);
3063
2580
  if (content) return content;
3064
2581
  if (dir === ".") break;
3065
- dir = path5.dirname(dir);
2582
+ dir = path4.dirname(dir);
3066
2583
  }
3067
2584
  return null;
3068
2585
  }
@@ -3149,7 +2666,7 @@ function buildScanData(manifests, packages) {
3149
2666
  // src/services/deps/scan.ts
3150
2667
  function runScan(config, opts) {
3151
2668
  void config;
3152
- const repoRoot = getRepoRoot2();
2669
+ const repoRoot = getRepoRoot();
3153
2670
  if (!repoRoot) {
3154
2671
  throw new PncliError("Not inside a git repository. deps commands require a git repo root.", 1);
3155
2672
  }
@@ -3196,7 +2713,7 @@ function isDowngrade(from, to) {
3196
2713
  // src/services/deps/diff.ts
3197
2714
  function runDiff(config, from, to, opts) {
3198
2715
  void config;
3199
- const repoRoot = getRepoRoot2();
2716
+ const repoRoot = getRepoRoot();
3200
2717
  if (!repoRoot) {
3201
2718
  throw new PncliError("Not inside a git repository.", 1);
3202
2719
  }
@@ -3252,7 +2769,7 @@ var OSV_ECOSYSTEM = {
3252
2769
  nuget: "NuGet",
3253
2770
  maven: "Maven"
3254
2771
  };
3255
- async function fetchWithTimeout2(url, init) {
2772
+ async function fetchWithTimeout(url, init) {
3256
2773
  const controller = new AbortController();
3257
2774
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
3258
2775
  try {
@@ -3263,7 +2780,7 @@ async function fetchWithTimeout2(url, init) {
3263
2780
  }
3264
2781
  async function checkOsvConnectivity() {
3265
2782
  try {
3266
- const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
2783
+ const res = await fetchWithTimeout(`${OSV_URL}/v1/querybatch`, {
3267
2784
  method: "POST",
3268
2785
  headers: { "Content-Type": "application/json" },
3269
2786
  body: JSON.stringify({ queries: [] })
@@ -3335,7 +2852,7 @@ function mapVuln(vuln, pkg2) {
3335
2852
  };
3336
2853
  }
3337
2854
  async function queryBatch(queries, packages) {
3338
- const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
2855
+ const res = await fetchWithTimeout(`${OSV_URL}/v1/querybatch`, {
3339
2856
  method: "POST",
3340
2857
  headers: { "Content-Type": "application/json" },
3341
2858
  body: JSON.stringify({ queries })
@@ -3377,7 +2894,7 @@ async function checkPackagesForVulns(packages) {
3377
2894
 
3378
2895
  // src/services/deps/clients/artifactory.ts
3379
2896
  var TIMEOUT_MS2 = 15e3;
3380
- async function fetchWithTimeout3(url, token) {
2897
+ async function fetchWithTimeout2(url, token) {
3381
2898
  const controller = new AbortController();
3382
2899
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
3383
2900
  try {
@@ -3415,7 +2932,7 @@ async function checkArtifactoryConnectivity(config) {
3415
2932
  };
3416
2933
  }
3417
2934
  try {
3418
- const res = await fetchWithTimeout3(`${config.baseUrl}/api/system/ping`, config.token);
2935
+ const res = await fetchWithTimeout2(`${config.baseUrl}/api/system/ping`, config.token);
3419
2936
  if (res.status === 401 || res.status === 403) {
3420
2937
  return {
3421
2938
  reachable: true,
@@ -3465,7 +2982,7 @@ function repoForEcosystem(config, eco) {
3465
2982
  async function getLatestNpm(baseUrl, token, repoName, packageName) {
3466
2983
  try {
3467
2984
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
3468
- const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
2985
+ const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3469
2986
  if (!res.ok) return null;
3470
2987
  const data = await res.json();
3471
2988
  return data["dist-tags"]?.latest ?? null;
@@ -3476,7 +2993,7 @@ async function getLatestNpm(baseUrl, token, repoName, packageName) {
3476
2993
  async function getLatestNuget(baseUrl, token, repoName, packageName) {
3477
2994
  try {
3478
2995
  const lower = packageName.toLowerCase();
3479
- const res = await fetchWithTimeout3(
2996
+ const res = await fetchWithTimeout2(
3480
2997
  `${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
3481
2998
  token
3482
2999
  );
@@ -3492,7 +3009,7 @@ async function getLatestMaven(baseUrl, token, repoName, packageName) {
3492
3009
  const [groupId, artifactId] = packageName.split(":");
3493
3010
  if (!groupId || !artifactId) return null;
3494
3011
  try {
3495
- const res = await fetchWithTimeout3(
3012
+ const res = await fetchWithTimeout2(
3496
3013
  `${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
3497
3014
  token
3498
3015
  );
@@ -3542,7 +3059,7 @@ async function getOutdatedPackages(packages, config, filterType) {
3542
3059
  async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3543
3060
  try {
3544
3061
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
3545
- const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3062
+ const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3546
3063
  if (!res.ok) return null;
3547
3064
  const data = await res.json();
3548
3065
  if (typeof data.license === "string") return data.license;
@@ -3555,7 +3072,7 @@ async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3555
3072
  async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
3556
3073
  try {
3557
3074
  const lower = packageName.toLowerCase();
3558
- const res = await fetchWithTimeout3(
3075
+ const res = await fetchWithTimeout2(
3559
3076
  `${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
3560
3077
  token
3561
3078
  );
@@ -3651,7 +3168,7 @@ async function buildConnectivityData(config) {
3651
3168
 
3652
3169
  // src/services/deps/frisk.ts
3653
3170
  async function runFrisk(config, opts) {
3654
- const repoRoot = getRepoRoot2();
3171
+ const repoRoot = getRepoRoot();
3655
3172
  if (!repoRoot) {
3656
3173
  throw new PncliError("Not inside a git repository.", 1);
3657
3174
  }
@@ -3681,7 +3198,7 @@ async function runFrisk(config, opts) {
3681
3198
 
3682
3199
  // src/services/deps/outdated.ts
3683
3200
  async function runOutdated(config, opts, filterType) {
3684
- const repoRoot = getRepoRoot2();
3201
+ const repoRoot = getRepoRoot();
3685
3202
  if (!repoRoot) {
3686
3203
  throw new PncliError("Not inside a git repository.", 1);
3687
3204
  }
@@ -3713,7 +3230,7 @@ async function runOutdated(config, opts, filterType) {
3713
3230
 
3714
3231
  // src/services/deps/license-check.ts
3715
3232
  async function runLicenseCheck(config, opts) {
3716
- const repoRoot = getRepoRoot2();
3233
+ const repoRoot = getRepoRoot();
3717
3234
  if (!repoRoot) {
3718
3235
  throw new PncliError("Not inside a git repository.", 1);
3719
3236
  }
@@ -3843,7 +3360,144 @@ function registerDepsCommands(program2) {
3843
3360
 
3844
3361
  // src/services/config/commands.ts
3845
3362
  import { input, password, confirm } from "@inquirer/prompts";
3846
- import fs5 from "fs";
3363
+
3364
+ // src/services/ado/client/work.ts
3365
+ var API6 = "7.1";
3366
+ var API_PREVIEW = "7.1-preview.4";
3367
+ var AdoWorkClient = class {
3368
+ constructor(http) {
3369
+ this.http = http;
3370
+ }
3371
+ http;
3372
+ async getWorkItem(collection, id) {
3373
+ return this.http.ado(
3374
+ `/${encodeURIComponent(collection)}/_apis/wit/workitems/${id}?api-version=${API6}&$expand=all`
3375
+ );
3376
+ }
3377
+ async createWorkItem(collection, project, type, patch) {
3378
+ return this.http.ado(
3379
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitems/${encodeURIComponent("$" + type)}?api-version=${API6}`,
3380
+ { method: "POST", body: patch, headers: { "Content-Type": "application/json-patch+json" } }
3381
+ );
3382
+ }
3383
+ async updateWorkItem(collection, id, patch) {
3384
+ return this.http.ado(
3385
+ `/${encodeURIComponent(collection)}/_apis/wit/workitems/${id}?api-version=${API6}`,
3386
+ { method: "PATCH", body: patch, headers: { "Content-Type": "application/json-patch+json" } }
3387
+ );
3388
+ }
3389
+ async queryWiql(collection, project, wiql) {
3390
+ return this.http.ado(
3391
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/wiql?api-version=${API6}`,
3392
+ { method: "POST", body: { query: wiql } }
3393
+ );
3394
+ }
3395
+ async listComments(collection, project, workItemId) {
3396
+ const result = await this.http.ado(
3397
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?api-version=${API_PREVIEW}`
3398
+ );
3399
+ return result.value ?? [];
3400
+ }
3401
+ async addComment(collection, project, workItemId, text) {
3402
+ return this.http.ado(
3403
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?api-version=${API_PREVIEW}`,
3404
+ { method: "POST", body: { text } }
3405
+ );
3406
+ }
3407
+ async listWorkItemTypes(collection, project) {
3408
+ const result = await this.http.ado(
3409
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes?api-version=${API6}`
3410
+ );
3411
+ return result.value ?? [];
3412
+ }
3413
+ async listTypeStates(collection, project, type) {
3414
+ const result = await this.http.ado(
3415
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes/${encodeURIComponent(type)}/states?api-version=${API6}`
3416
+ );
3417
+ return result.value ?? [];
3418
+ }
3419
+ async listFields(collection, project) {
3420
+ const scope = project ? `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}` : `/${encodeURIComponent(collection)}`;
3421
+ const result = await this.http.ado(
3422
+ `${scope}/_apis/wit/fields?api-version=${API6}`
3423
+ );
3424
+ return result.value ?? [];
3425
+ }
3426
+ async listTypeFields(collection, project, type) {
3427
+ const result = await this.http.ado(
3428
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes/${encodeURIComponent(type)}/fields?api-version=${API6}`
3429
+ );
3430
+ return result.value ?? [];
3431
+ }
3432
+ };
3433
+
3434
+ // src/services/ado/discovery.ts
3435
+ async function discoverFields(client, collection, project) {
3436
+ const fields = await client.listFields(collection, project);
3437
+ return fields.map(toFieldMeta);
3438
+ }
3439
+ async function discoverTypeFields(client, collection, project, type) {
3440
+ const fields = await client.listTypeFields(collection, project, type);
3441
+ return fields.map(toFieldMeta);
3442
+ }
3443
+ function toFieldMeta(f) {
3444
+ return {
3445
+ referenceName: f.referenceName,
3446
+ name: f.name,
3447
+ type: f.type,
3448
+ readOnly: f.readOnly,
3449
+ ...f.picklistId ? { picklistId: f.picklistId } : {}
3450
+ };
3451
+ }
3452
+ async function discoverTypes(client, collection, project) {
3453
+ const types = await client.listWorkItemTypes(collection, project);
3454
+ const result = [];
3455
+ for (const t of types) {
3456
+ const states = await client.listTypeStates(collection, project, t.name);
3457
+ const requiredFields = (t.fields ?? []).filter((f) => f.alwaysRequired).map((f) => f.referenceName);
3458
+ result.push({
3459
+ name: t.name,
3460
+ states: states.map((s) => s.name),
3461
+ requiredFields
3462
+ });
3463
+ }
3464
+ return result;
3465
+ }
3466
+ function buildDefaultAliases(fields) {
3467
+ const refNames = new Set(fields.map((f) => f.referenceName));
3468
+ const candidates = [
3469
+ ["title", "System.Title"],
3470
+ ["state", "System.State"],
3471
+ ["assignedto", "System.AssignedTo"],
3472
+ ["assigned-to", "System.AssignedTo"],
3473
+ ["description", "System.Description"],
3474
+ ["areapath", "System.AreaPath"],
3475
+ ["area-path", "System.AreaPath"],
3476
+ ["iterationpath", "System.IterationPath"],
3477
+ ["iteration", "System.IterationPath"],
3478
+ ["tags", "System.Tags"],
3479
+ ["priority", "Microsoft.VSTS.Common.Priority"],
3480
+ ["severity", "Microsoft.VSTS.Common.Severity"],
3481
+ ["effort", "Microsoft.VSTS.Scheduling.Effort"],
3482
+ ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
3483
+ ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
3484
+ ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
3485
+ ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
3486
+ ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
3487
+ ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
3488
+ ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
3489
+ ];
3490
+ const aliases = {};
3491
+ for (const [alias, ref] of candidates) {
3492
+ if (refNames.has(ref)) {
3493
+ aliases[alias] = ref;
3494
+ }
3495
+ }
3496
+ return aliases;
3497
+ }
3498
+
3499
+ // src/services/config/commands.ts
3500
+ import fs2 from "fs";
3847
3501
  function registerConfigCommands(program2) {
3848
3502
  const config = program2.command("config").description("Manage pncli configuration");
3849
3503
  config.command("init").description("Interactive setup wizard").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
@@ -3939,6 +3593,18 @@ function registerConfigCommands(program2) {
3939
3593
  } else {
3940
3594
  results.sde = { ok: null, message: "not configured" };
3941
3595
  }
3596
+ if (cfg.ado.baseUrl) {
3597
+ try {
3598
+ const collection = cfg.defaults.ado?.collection;
3599
+ const path5 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1` : "/_apis/projectCollections?api-version=7.1";
3600
+ await http.ado(path5);
3601
+ results.ado = { ok: true, message: "connected" };
3602
+ } catch (err) {
3603
+ results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
3604
+ }
3605
+ } else {
3606
+ results.ado = { ok: null, message: "not configured" };
3607
+ }
3942
3608
  success(results, "config", "test", start);
3943
3609
  } catch (err) {
3944
3610
  fail(err, "config", "test", start);
@@ -4036,13 +3702,105 @@ async function initGlobalConfig(start) {
4036
3702
  let sdeBaseUrl = "";
4037
3703
  let sdeToken = "";
4038
3704
  if (useSde) {
4039
- sdeBaseUrl = await input({
4040
- message: "SDElements base URL\n Cloud-hosted: https://your-org.sdelements.com\n On-premise: https://sde.your-company.com\n URL: ",
3705
+ process.stderr.write(" Connection string format: <api-token>@<base-url>\n");
3706
+ const useSdeConnectionString = await confirm({
3707
+ message: "Do you have a connection string to paste (token@url)?",
3708
+ default: false
3709
+ });
3710
+ if (useSdeConnectionString) {
3711
+ const sdeConnection = await password({
3712
+ message: "SDElements connection string (api-token@hostname, e.g. mytoken@myorg.sdelements.com):"
3713
+ });
3714
+ const atIdx = sdeConnection.indexOf("@");
3715
+ if (atIdx > 0) {
3716
+ sdeToken = sdeConnection.slice(0, atIdx);
3717
+ sdeBaseUrl = sdeConnection.slice(atIdx + 1);
3718
+ }
3719
+ } else {
3720
+ sdeBaseUrl = await input({
3721
+ message: "SDElements hostname\n Cloud-hosted: your-org.sdelements.com\n On-premise: sde.your-company.com\n Host: ",
3722
+ default: ""
3723
+ });
3724
+ sdeToken = await password({
3725
+ message: "SDElements API token:"
3726
+ });
3727
+ }
3728
+ }
3729
+ process.stderr.write("\n\u2500\u2500 Azure DevOps Server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
3730
+ const useAdo = await confirm({
3731
+ message: "Configure Azure DevOps Server for work items, repos, and pipelines?",
3732
+ default: false
3733
+ });
3734
+ let adoBaseUrl = "";
3735
+ let adoPat = "";
3736
+ let adoCollection = "";
3737
+ let adoProject = "";
3738
+ let adoFieldAliases = {};
3739
+ let adoDiscoveredFields = [];
3740
+ let adoDiscoveredTypes = [];
3741
+ if (useAdo) {
3742
+ adoBaseUrl = await input({
3743
+ message: "Azure DevOps Server base URL (e.g. https://tfs.company.com or https://devops.company.com/tfs):",
3744
+ default: ""
3745
+ });
3746
+ adoPat = await password({
3747
+ message: "Azure DevOps personal access token:"
3748
+ });
3749
+ adoCollection = await input({
3750
+ message: "Default collection name (e.g. DefaultCollection):",
4041
3751
  default: ""
4042
3752
  });
4043
- sdeToken = await password({
4044
- message: "SDElements API token:"
3753
+ adoProject = await input({
3754
+ message: "Default team project name:",
3755
+ default: ""
4045
3756
  });
3757
+ if (adoBaseUrl && adoCollection) {
3758
+ process.stderr.write("\n Verifying connection...\n");
3759
+ try {
3760
+ const tempConfig = {
3761
+ ...loadConfig(),
3762
+ ado: {
3763
+ baseUrl: adoBaseUrl,
3764
+ pat: adoPat || void 0,
3765
+ fieldAliases: {},
3766
+ discoveredFields: [],
3767
+ discoveredTypes: []
3768
+ }
3769
+ };
3770
+ const tempHttp = createHttpClient(tempConfig);
3771
+ const tempCore = new AdoCoreClient(tempHttp);
3772
+ await tempCore.getConnectionData(adoCollection);
3773
+ process.stderr.write(" Connected.\n");
3774
+ if (adoProject) {
3775
+ const doDiscover = await confirm({
3776
+ message: "Discover work item types and fields from this collection now? (recommended)",
3777
+ default: true
3778
+ });
3779
+ if (doDiscover) {
3780
+ process.stderr.write(" Fetching work item types and fields...\n");
3781
+ const tempWork = new AdoWorkClient(tempHttp);
3782
+ adoDiscoveredFields = await discoverFields(tempWork, adoCollection);
3783
+ process.stderr.write(` Found ${adoDiscoveredFields.length} fields.
3784
+ `);
3785
+ adoDiscoveredTypes = await discoverTypes(tempWork, adoCollection, adoProject);
3786
+ process.stderr.write(` Found ${adoDiscoveredTypes.length} work item types.
3787
+ `);
3788
+ const doAliases = await confirm({
3789
+ message: 'Save friendly aliases for common fields? (e.g. "priority" \u2192 reference name)',
3790
+ default: true
3791
+ });
3792
+ if (doAliases) {
3793
+ adoFieldAliases = buildDefaultAliases(adoDiscoveredFields);
3794
+ process.stderr.write(` Generated ${Object.keys(adoFieldAliases).length} aliases.
3795
+ `);
3796
+ }
3797
+ }
3798
+ }
3799
+ } catch (err) {
3800
+ warn(`Could not connect to Azure DevOps: ${err instanceof Error ? err.message : String(err)}`);
3801
+ warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
3802
+ }
3803
+ }
4046
3804
  }
4047
3805
  process.stderr.write("\n\u2500\u2500 Defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
4048
3806
  const jiraProject = await input({
@@ -4101,10 +3859,18 @@ async function initGlobalConfig(start) {
4101
3859
  token: sonarToken || void 0
4102
3860
  }
4103
3861
  } : {},
4104
- ...useSde ? {
3862
+ ...useSde && sdeToken && sdeBaseUrl ? {
4105
3863
  sde: {
4106
- baseUrl: sdeBaseUrl || void 0,
4107
- token: sdeToken || void 0
3864
+ connection: `${sdeToken}@${sdeBaseUrl}`
3865
+ }
3866
+ } : {},
3867
+ ...useAdo && adoBaseUrl ? {
3868
+ ado: {
3869
+ baseUrl: adoBaseUrl,
3870
+ ...adoPat ? { pat: adoPat } : {},
3871
+ ...Object.keys(adoFieldAliases).length ? { fieldAliases: adoFieldAliases } : {},
3872
+ ...adoDiscoveredFields.length ? { discoveredFields: adoDiscoveredFields } : {},
3873
+ ...adoDiscoveredTypes.length ? { discoveredTypes: adoDiscoveredTypes } : {}
4108
3874
  }
4109
3875
  } : {},
4110
3876
  defaults: {
@@ -4120,7 +3886,13 @@ async function initGlobalConfig(start) {
4120
3886
  sde: {
4121
3887
  project: sdeProject || void 0
4122
3888
  }
4123
- } : {}
3889
+ } : {},
3890
+ ...useAdo && (adoCollection || adoProject) ? {
3891
+ ado: {
3892
+ collection: adoCollection || void 0,
3893
+ project: adoProject || void 0
3894
+ }
3895
+ } : {}
4124
3896
  }
4125
3897
  });
4126
3898
  const configPath = getGlobalConfigPath();
@@ -4145,6 +3917,18 @@ async function initRepoConfig(start) {
4145
3917
  message: "Default target branch for PRs:",
4146
3918
  default: "main"
4147
3919
  });
3920
+ const adoRepoCollection = await input({
3921
+ message: "Default Azure DevOps collection for this repo (leave blank to use global):",
3922
+ default: ""
3923
+ });
3924
+ const adoRepoProject = await input({
3925
+ message: "Default Azure DevOps project for this repo (leave blank to use global):",
3926
+ default: ""
3927
+ });
3928
+ const adoRepoRepo = await input({
3929
+ message: "Default Azure DevOps repo name for this repo (leave blank to auto-detect):",
3930
+ default: ""
3931
+ });
4148
3932
  const confirmed = await confirm({
4149
3933
  message: "Write config to .pncli.json in repo root?",
4150
3934
  default: true
@@ -4154,7 +3938,7 @@ async function initRepoConfig(start) {
4154
3938
  process.exitCode = ExitCode.SUCCESS;
4155
3939
  return;
4156
3940
  }
4157
- if (fs5.existsSync(".pncli.json")) {
3941
+ if (fs2.existsSync(".pncli.json")) {
4158
3942
  const overwrite = await confirm({
4159
3943
  message: ".pncli.json already exists. Overwrite?",
4160
3944
  default: false
@@ -4174,12 +3958,946 @@ async function initRepoConfig(start) {
4174
3958
  },
4175
3959
  bitbucket: {
4176
3960
  targetBranch: targetBranch || void 0
4177
- }
3961
+ },
3962
+ ...adoRepoCollection || adoRepoProject || adoRepoRepo ? {
3963
+ ado: {
3964
+ collection: adoRepoCollection || void 0,
3965
+ project: adoRepoProject || void 0,
3966
+ repo: adoRepoRepo || void 0
3967
+ }
3968
+ } : {}
4178
3969
  }
4179
3970
  });
4180
3971
  success({ written: ".pncli.json" }, "config", "init", start);
4181
3972
  }
4182
3973
 
3974
+ // src/services/ado/client/git.ts
3975
+ var API7 = "7.1";
3976
+ var AdoGitClient = class {
3977
+ constructor(http) {
3978
+ this.http = http;
3979
+ }
3980
+ http;
3981
+ // ── Repositories ──────────────────────────────────────────────────
3982
+ async listRepos(collection, project) {
3983
+ const result = await this.http.ado(
3984
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories?api-version=${API7}`
3985
+ );
3986
+ return result.value ?? [];
3987
+ }
3988
+ async getRepo(collection, project, repoId) {
3989
+ return this.http.ado(
3990
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}?api-version=${API7}`
3991
+ );
3992
+ }
3993
+ // ── Pull Requests ─────────────────────────────────────────────────
3994
+ async listPRs(collection, project, repo, opts = {}) {
3995
+ return this.http.adoPaginate(async (token) => {
3996
+ const params = {
3997
+ "api-version": API7,
3998
+ "searchCriteria.status": opts.status ?? "active",
3999
+ ...opts.creatorAlias ? { "searchCriteria.creatorId": opts.creatorAlias } : {},
4000
+ ...opts.reviewerAlias ? { "searchCriteria.reviewerId": opts.reviewerAlias } : {},
4001
+ ...token ? { continuationToken: token } : {}
4002
+ };
4003
+ return this.http.adoRaw(
4004
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`,
4005
+ { params }
4006
+ );
4007
+ });
4008
+ }
4009
+ async getPR(collection, project, repo, prId) {
4010
+ return this.http.ado(
4011
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`
4012
+ );
4013
+ }
4014
+ async createPR(collection, project, repo, body) {
4015
+ return this.http.ado(
4016
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests?api-version=${API7}`,
4017
+ { method: "POST", body }
4018
+ );
4019
+ }
4020
+ async updatePR(collection, project, repo, prId, body) {
4021
+ return this.http.ado(
4022
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`,
4023
+ { method: "PATCH", body }
4024
+ );
4025
+ }
4026
+ async completePR(collection, project, repo, prId, lastMergeSourceCommit, completionOptions) {
4027
+ return this.http.ado(
4028
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`,
4029
+ {
4030
+ method: "PATCH",
4031
+ body: {
4032
+ status: "completed",
4033
+ lastMergeSourceCommit: { commitId: lastMergeSourceCommit },
4034
+ completionOptions
4035
+ }
4036
+ }
4037
+ );
4038
+ }
4039
+ async abandonPR(collection, project, repo, prId) {
4040
+ return this.http.ado(
4041
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`,
4042
+ { method: "PATCH", body: { status: "abandoned" } }
4043
+ );
4044
+ }
4045
+ // ── Reviewers / Votes ─────────────────────────────────────────────
4046
+ async setPRVote(collection, project, repo, prId, reviewerId, vote) {
4047
+ return this.http.ado(
4048
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/reviewers/${encodeURIComponent(reviewerId)}?api-version=${API7}`,
4049
+ { method: "PUT", body: { vote } }
4050
+ );
4051
+ }
4052
+ async listReviewers(collection, project, repo, prId) {
4053
+ const pr = await this.getPR(collection, project, repo, prId);
4054
+ return pr.reviewers;
4055
+ }
4056
+ // ── Threads / Comments ────────────────────────────────────────────
4057
+ async listThreads(collection, project, repo, prId) {
4058
+ const result = await this.http.ado(
4059
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads?api-version=${API7}`
4060
+ );
4061
+ return result.value ?? [];
4062
+ }
4063
+ async createThread(collection, project, repo, prId, body) {
4064
+ return this.http.ado(
4065
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads?api-version=${API7}`,
4066
+ { method: "POST", body }
4067
+ );
4068
+ }
4069
+ async addCommentToThread(collection, project, repo, prId, threadId, content) {
4070
+ return this.http.ado(
4071
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads/${threadId}/comments?api-version=${API7}`,
4072
+ { method: "POST", body: { content, commentType: "text" } }
4073
+ );
4074
+ }
4075
+ async updateThread(collection, project, repo, prId, threadId, status) {
4076
+ return this.http.ado(
4077
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads/${threadId}?api-version=${API7}`,
4078
+ { method: "PATCH", body: { status } }
4079
+ );
4080
+ }
4081
+ async deleteComment(collection, project, repo, prId, threadId, commentId) {
4082
+ await this.http.ado(
4083
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads/${threadId}/comments/${commentId}?api-version=${API7}`,
4084
+ { method: "DELETE" }
4085
+ );
4086
+ }
4087
+ // ── Diffs / Files ─────────────────────────────────────────────────
4088
+ async listPRChanges(collection, project, repo, prId) {
4089
+ const iterations = await this.http.ado(
4090
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/iterations?api-version=${API7}`
4091
+ );
4092
+ const latestId = (iterations.value ?? []).at(-1)?.id;
4093
+ if (!latestId) return [];
4094
+ const changes = await this.http.ado(
4095
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/iterations/${latestId}/changes?api-version=${API7}`
4096
+ );
4097
+ return changes.changeEntries ?? [];
4098
+ }
4099
+ };
4100
+
4101
+ // src/services/ado/client/build.ts
4102
+ var API8 = "7.1";
4103
+ var AdoBuildClient = class {
4104
+ constructor(http) {
4105
+ this.http = http;
4106
+ }
4107
+ http;
4108
+ // ── Definitions ───────────────────────────────────────────────────
4109
+ async listDefinitions(collection, project) {
4110
+ return this.http.adoPaginate(async (token) => {
4111
+ const params = {
4112
+ "api-version": API8,
4113
+ ...token ? { continuationToken: token } : {}
4114
+ };
4115
+ return this.http.adoRaw(
4116
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/definitions`,
4117
+ { params }
4118
+ );
4119
+ });
4120
+ }
4121
+ async getDefinition(collection, project, id) {
4122
+ return this.http.ado(
4123
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/definitions/${id}?api-version=${API8}`
4124
+ );
4125
+ }
4126
+ // ── Builds (runs) ─────────────────────────────────────────────────
4127
+ async queueBuild(collection, project, definitionId, opts = {}) {
4128
+ const body = {
4129
+ definition: { id: definitionId },
4130
+ ...opts.sourceBranch ? { sourceBranch: opts.sourceBranch } : {},
4131
+ ...opts.parameters ? { parameters: opts.parameters } : {}
4132
+ };
4133
+ return this.http.ado(
4134
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds?api-version=${API8}`,
4135
+ { method: "POST", body }
4136
+ );
4137
+ }
4138
+ async listBuilds(collection, project, opts = {}) {
4139
+ return this.http.adoPaginate(async (token) => {
4140
+ const params = {
4141
+ "api-version": API8,
4142
+ ...opts.definitionIds?.length ? { definitionIds: opts.definitionIds.join(",") } : {},
4143
+ ...opts.branchName ? { branchName: opts.branchName } : {},
4144
+ ...opts.statusFilter ? { statusFilter: opts.statusFilter } : {},
4145
+ ...opts.top ? { "$top": opts.top } : {},
4146
+ ...token ? { continuationToken: token } : {}
4147
+ };
4148
+ return this.http.adoRaw(
4149
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds`,
4150
+ { params }
4151
+ );
4152
+ });
4153
+ }
4154
+ async getBuild(collection, project, buildId) {
4155
+ return this.http.ado(
4156
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}?api-version=${API8}`
4157
+ );
4158
+ }
4159
+ async cancelBuild(collection, project, buildId) {
4160
+ return this.http.ado(
4161
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}?api-version=${API8}`,
4162
+ { method: "PATCH", body: { status: "cancelling" } }
4163
+ );
4164
+ }
4165
+ // ── Logs ──────────────────────────────────────────────────────────
4166
+ async listLogs(collection, project, buildId) {
4167
+ const result = await this.http.ado(
4168
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/logs?api-version=${API8}`
4169
+ );
4170
+ return result.value ?? [];
4171
+ }
4172
+ async getLog(collection, project, buildId, logId) {
4173
+ return this.http.adoText(
4174
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/logs/${logId}?api-version=${API8}`
4175
+ );
4176
+ }
4177
+ };
4178
+
4179
+ // src/services/ado/helpers.ts
4180
+ function getAdoContext(program2, requireRepo = false) {
4181
+ const opts = program2.optsWithGlobals();
4182
+ const config = loadConfig({ configPath: opts.config });
4183
+ const http = createHttpClient(config, Boolean(opts.dryRun));
4184
+ const ctx = getGitContext(config);
4185
+ const collection = opts.collection ?? opts.organization ?? ctx?.ado?.collection ?? config.defaults.ado?.collection ?? "";
4186
+ const project = opts.project ?? ctx?.ado?.project ?? config.defaults.ado?.project ?? "";
4187
+ const repo = opts.repo ?? ctx?.ado?.repo ?? config.defaults.ado?.repo ?? "";
4188
+ if (!collection) {
4189
+ throw new PncliError(
4190
+ "Could not determine Azure DevOps collection. Pass --collection, or run pncli config init.",
4191
+ 1
4192
+ );
4193
+ }
4194
+ if (!project) {
4195
+ throw new PncliError(
4196
+ "Could not determine Azure DevOps project. Pass --project, or run pncli config init.",
4197
+ 1
4198
+ );
4199
+ }
4200
+ if (requireRepo && !repo) {
4201
+ throw new PncliError(
4202
+ "Could not determine Azure DevOps repo. Pass --repo, or run from inside a checked-out ADO repo.",
4203
+ 1
4204
+ );
4205
+ }
4206
+ return {
4207
+ collection,
4208
+ project,
4209
+ repo,
4210
+ coreClient: new AdoCoreClient(http),
4211
+ workClient: new AdoWorkClient(http),
4212
+ gitClient: new AdoGitClient(http),
4213
+ buildClient: new AdoBuildClient(http)
4214
+ };
4215
+ }
4216
+ function parseFieldArgs2(fields) {
4217
+ const result = {};
4218
+ for (const f of fields) {
4219
+ const eq = f.indexOf("=");
4220
+ if (eq <= 0) throw new PncliError(`Invalid --field value "${f}". Expected format: name=value`, 1);
4221
+ result[f.slice(0, eq)] = f.slice(eq + 1);
4222
+ }
4223
+ return result;
4224
+ }
4225
+ function buildFieldPatch(fields, fieldAliases) {
4226
+ return Object.entries(fields).map(([key, value]) => {
4227
+ const refName = fieldAliases[key.toLowerCase()] ?? fieldAliases[key] ?? key;
4228
+ return { op: "add", path: `/fields/${refName}`, value };
4229
+ });
4230
+ }
4231
+ function fieldArgsToPatch(fieldArgs, fieldAliases) {
4232
+ return buildFieldPatch(parseFieldArgs2(fieldArgs), fieldAliases);
4233
+ }
4234
+ var PR_VOTE = {
4235
+ APPROVE: 10,
4236
+ APPROVE_WITH_SUGGESTIONS: 5,
4237
+ NO_VOTE: 0,
4238
+ WAIT_FOR_AUTHOR: -5,
4239
+ REJECT: -10
4240
+ };
4241
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed"]);
4242
+ function isBuildTerminal(build) {
4243
+ return TERMINAL_STATUSES.has(build.status);
4244
+ }
4245
+ async function pollUntilTerminal(fetchStatus, isTerminal, opts) {
4246
+ const deadline = Date.now() + opts.timeoutSec * 1e3;
4247
+ let status = await fetchStatus();
4248
+ while (!isTerminal(status)) {
4249
+ if (Date.now() >= deadline) {
4250
+ throw new PncliError(
4251
+ `Timed out waiting for completion after ${opts.timeoutSec}s`,
4252
+ 1
4253
+ );
4254
+ }
4255
+ log(`Waiting ${opts.pollSec}s...`);
4256
+ await new Promise((resolve) => setTimeout(resolve, opts.pollSec * 1e3));
4257
+ status = await fetchStatus();
4258
+ }
4259
+ return status;
4260
+ }
4261
+
4262
+ // src/services/ado/commands/project.ts
4263
+ function registerAdoProjectCommands(ado) {
4264
+ const project = ado.command("project").description("Azure DevOps project and collection operations");
4265
+ project.command("list-collections").description("List all project collections on the server").action(async () => {
4266
+ const start = Date.now();
4267
+ try {
4268
+ const { coreClient } = getAdoContext(ado);
4269
+ const data = await coreClient.listCollections();
4270
+ success(data, "ado", "project-list-collections", start);
4271
+ } catch (err) {
4272
+ fail(err, "ado", "project-list-collections", start);
4273
+ }
4274
+ });
4275
+ project.command("list").description("List team projects in a collection").action(async () => {
4276
+ const start = Date.now();
4277
+ try {
4278
+ const { collection, coreClient } = getAdoContext(ado);
4279
+ const data = await coreClient.listProjects(collection);
4280
+ success(data, "ado", "project-list", start);
4281
+ } catch (err) {
4282
+ fail(err, "ado", "project-list", start);
4283
+ }
4284
+ });
4285
+ project.command("get").description("Get a team project by name").requiredOption("--name <project>", "Project name").action(async (opts) => {
4286
+ const start = Date.now();
4287
+ try {
4288
+ const { collection, coreClient } = getAdoContext(ado);
4289
+ const data = await coreClient.getProject(collection, opts.name);
4290
+ success(data, "ado", "project-get", start);
4291
+ } catch (err) {
4292
+ fail(err, "ado", "project-get", start);
4293
+ }
4294
+ });
4295
+ }
4296
+
4297
+ // src/services/ado/commands/work.ts
4298
+ function registerAdoWorkCommands(ado) {
4299
+ const work = ado.command("work").description("Azure DevOps work item operations");
4300
+ work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
4301
+ const start = Date.now();
4302
+ try {
4303
+ const { collection, workClient } = getAdoContext(ado);
4304
+ const data = await workClient.getWorkItem(collection, parseInt(opts.id, 10));
4305
+ success(data, "ado", "work-get", start);
4306
+ } catch (err) {
4307
+ fail(err, "ado", "work-get", start);
4308
+ }
4309
+ });
4310
+ work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
4311
+ acc.push(v);
4312
+ return acc;
4313
+ }, []).action(async (opts) => {
4314
+ const start = Date.now();
4315
+ try {
4316
+ const globalOpts = ado.optsWithGlobals();
4317
+ const config = loadConfig({ configPath: globalOpts.config });
4318
+ const { collection, project, workClient } = getAdoContext(ado);
4319
+ const builtIn = {
4320
+ "System.Title": opts.title,
4321
+ ...opts.description ? { "System.Description": opts.description } : {},
4322
+ ...opts.assignee ? { "System.AssignedTo": opts.assignee } : {},
4323
+ ...opts.priority ? { "Microsoft.VSTS.Common.Priority": opts.priority } : {}
4324
+ };
4325
+ const extra = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
4326
+ const patch = [
4327
+ ...Object.entries(builtIn).map(([k, v]) => ({ op: "add", path: `/fields/${k}`, value: v })),
4328
+ ...extra
4329
+ ];
4330
+ const data = await workClient.createWorkItem(collection, project, opts.type, patch);
4331
+ success(data, "ado", "work-create", start);
4332
+ } catch (err) {
4333
+ fail(err, "ado", "work-create", start);
4334
+ }
4335
+ });
4336
+ work.command("update").description("Update a work item field").requiredOption("--id <n>", "Work item ID").option("--field <name=value>", "Field to update (repeatable)", (v, acc) => {
4337
+ acc.push(v);
4338
+ return acc;
4339
+ }, []).action(async (opts) => {
4340
+ const start = Date.now();
4341
+ try {
4342
+ const globalOpts = ado.optsWithGlobals();
4343
+ const config = loadConfig({ configPath: globalOpts.config });
4344
+ const { collection, workClient } = getAdoContext(ado);
4345
+ const patch = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
4346
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4347
+ success(data, "ado", "work-update", start);
4348
+ } catch (err) {
4349
+ fail(err, "ado", "work-update", start);
4350
+ }
4351
+ });
4352
+ work.command("transition").description("Set the state of a work item").requiredOption("--id <n>", "Work item ID").requiredOption("--state <state>", "New state (e.g. Active, Resolved, Closed)").action(async (opts) => {
4353
+ const start = Date.now();
4354
+ try {
4355
+ const { collection, workClient } = getAdoContext(ado);
4356
+ const patch = [{ op: "add", path: "/fields/System.State", value: opts.state }];
4357
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4358
+ success(data, "ado", "work-transition", start);
4359
+ } catch (err) {
4360
+ fail(err, "ado", "work-transition", start);
4361
+ }
4362
+ });
4363
+ work.command("assign").description("Assign a work item to a user").requiredOption("--id <n>", "Work item ID").requiredOption("--to <user>", "User display name or email").action(async (opts) => {
4364
+ const start = Date.now();
4365
+ try {
4366
+ const { collection, workClient } = getAdoContext(ado);
4367
+ const patch = [{ op: "add", path: "/fields/System.AssignedTo", value: opts.to }];
4368
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4369
+ success(data, "ado", "work-assign", start);
4370
+ } catch (err) {
4371
+ fail(err, "ado", "work-assign", start);
4372
+ }
4373
+ });
4374
+ work.command("link").description("Link two work items").requiredOption("--id <a>", "Source work item ID").requiredOption("--to <b>", "Target work item ID").option("--type <rel>", "Link type (related|parent|child|duplicate|duplicate-of)", "related").action(async (opts) => {
4375
+ const start = Date.now();
4376
+ try {
4377
+ const globalOpts = ado.optsWithGlobals();
4378
+ const config = loadConfig({ configPath: globalOpts.config });
4379
+ const { collection, workClient } = getAdoContext(ado);
4380
+ const relMap = {
4381
+ related: "System.LinkTypes.Related",
4382
+ parent: "System.LinkTypes.Hierarchy-Reverse",
4383
+ child: "System.LinkTypes.Hierarchy-Forward",
4384
+ duplicate: "System.LinkTypes.Duplicate-Forward",
4385
+ "duplicate-of": "System.LinkTypes.Duplicate-Reverse"
4386
+ };
4387
+ const rel = relMap[opts.type] ?? opts.type;
4388
+ const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
4389
+ const targetUrl = `${baseUrl}/${encodeURIComponent(collection)}/_apis/wit/workitems/${opts.to}`;
4390
+ const patch = [{ op: "add", path: "/relations/-", value: { rel, url: targetUrl } }];
4391
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4392
+ success(data, "ado", "work-link", start);
4393
+ } catch (err) {
4394
+ fail(err, "ado", "work-link", start);
4395
+ }
4396
+ });
4397
+ work.command("search").description("Run a WIQL query").requiredOption("--wiql <query>", "WIQL query string").action(async (opts) => {
4398
+ const start = Date.now();
4399
+ try {
4400
+ const { collection, project, workClient } = getAdoContext(ado);
4401
+ const data = await workClient.queryWiql(collection, project, opts.wiql);
4402
+ success(data, "ado", "work-search", start);
4403
+ } catch (err) {
4404
+ fail(err, "ado", "work-search", start);
4405
+ }
4406
+ });
4407
+ work.command("list-comments").description("List comments on a work item").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
4408
+ const start = Date.now();
4409
+ try {
4410
+ const { collection, project, workClient } = getAdoContext(ado);
4411
+ const data = await workClient.listComments(collection, project, parseInt(opts.id, 10));
4412
+ success(data, "ado", "work-list-comments", start);
4413
+ } catch (err) {
4414
+ fail(err, "ado", "work-list-comments", start);
4415
+ }
4416
+ });
4417
+ work.command("add-comment").description("Add a comment to a work item").requiredOption("--id <n>", "Work item ID").requiredOption("--body <text>", "Comment text").action(async (opts) => {
4418
+ const start = Date.now();
4419
+ try {
4420
+ const { collection, project, workClient } = getAdoContext(ado);
4421
+ const data = await workClient.addComment(collection, project, parseInt(opts.id, 10), opts.body);
4422
+ success(data, "ado", "work-add-comment", start);
4423
+ } catch (err) {
4424
+ fail(err, "ado", "work-add-comment", start);
4425
+ }
4426
+ });
4427
+ work.command("types").description("List work item types in the project").option("--discover", "Fetch from server (always true for this command)").option("--save", "Save discovered types to ~/.pncli/config.json").action(async (opts) => {
4428
+ const start = Date.now();
4429
+ try {
4430
+ const { collection, project, workClient } = getAdoContext(ado);
4431
+ const globalOpts = ado.optsWithGlobals();
4432
+ const types = await discoverTypes(workClient, collection, project);
4433
+ if (opts.save) {
4434
+ const globalConfigPath = getGlobalConfigPath(globalOpts.config);
4435
+ const fs3 = await import("fs");
4436
+ const existing = JSON.parse(fs3.readFileSync(globalConfigPath, "utf8") || "{}");
4437
+ existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
4438
+ fs3.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4439
+ warn(`Saved ${types.length} types to ${globalConfigPath}`);
4440
+ }
4441
+ success(types, "ado", "work-types", start);
4442
+ } catch (err) {
4443
+ fail(err, "ado", "work-types", start);
4444
+ }
4445
+ });
4446
+ work.command("list-states").description("List valid states for a work item type").requiredOption("--type <type>", "Work item type name (e.g. Bug)").action(async (opts) => {
4447
+ const start = Date.now();
4448
+ try {
4449
+ const { collection, project, workClient } = getAdoContext(ado);
4450
+ const data = await workClient.listTypeStates(collection, project, opts.type);
4451
+ success(data, "ado", "work-list-states", start);
4452
+ } catch (err) {
4453
+ fail(err, "ado", "work-list-states", start);
4454
+ }
4455
+ });
4456
+ work.command("fields").description("List work item fields available in the collection").option("--type <type>", "Scope to fields for a specific work item type (e.g. Bug)").option("--custom-only", "Exclude System.* and Microsoft.VSTS.* fields").option("--discover", "Fetch from server (always true for this command)").option("--save", "Save discovered fields and aliases to ~/.pncli/config.json").action(async (opts) => {
4457
+ const start = Date.now();
4458
+ try {
4459
+ const { collection, project, workClient } = getAdoContext(ado);
4460
+ const globalOpts = ado.optsWithGlobals();
4461
+ const allFields = opts.type ? await discoverTypeFields(workClient, collection, project, opts.type) : await discoverFields(workClient, collection);
4462
+ const fields = opts.customOnly ? allFields.filter(
4463
+ (f) => !f.referenceName.startsWith("System.") && !f.referenceName.startsWith("Microsoft.VSTS.")
4464
+ ) : allFields;
4465
+ if (opts.save) {
4466
+ const globalConfigPath = getGlobalConfigPath(globalOpts.config);
4467
+ const fs3 = await import("fs");
4468
+ const existing = JSON.parse(fs3.readFileSync(globalConfigPath, "utf8") || "{}");
4469
+ const aliases = buildDefaultAliases(allFields);
4470
+ existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
4471
+ fs3.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4472
+ warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
4473
+ }
4474
+ success(fields, "ado", "work-fields", start);
4475
+ } catch (err) {
4476
+ fail(err, "ado", "work-fields", start);
4477
+ }
4478
+ });
4479
+ }
4480
+
4481
+ // src/services/ado/commands/repo.ts
4482
+ function flattenThreadComments(threads, filterInlineOnly, filterGeneralOnly) {
4483
+ const flat = [];
4484
+ for (const thread of threads) {
4485
+ if (thread.isDeleted) continue;
4486
+ if (filterInlineOnly && !thread.threadContext) continue;
4487
+ if (filterGeneralOnly && thread.threadContext) continue;
4488
+ for (const comment of thread.comments) {
4489
+ if (comment.isDeleted) continue;
4490
+ flat.push({ ...comment, threadId: thread.id, threadContext: thread.threadContext ?? null });
4491
+ }
4492
+ }
4493
+ return flat.sort((a, b) => a.publishedDate.localeCompare(b.publishedDate));
4494
+ }
4495
+ function registerAdoRepoCommands(ado) {
4496
+ const repo = ado.command("repo").description("Azure DevOps repository and pull request operations");
4497
+ repo.command("list").description("List git repositories in the project").action(async () => {
4498
+ const start = Date.now();
4499
+ try {
4500
+ const { collection, project, gitClient } = getAdoContext(ado);
4501
+ const data = await gitClient.listRepos(collection, project);
4502
+ success(data, "ado", "repo-list", start);
4503
+ } catch (err) {
4504
+ fail(err, "ado", "repo-list", start);
4505
+ }
4506
+ });
4507
+ repo.command("get").description("Get a specific repository").action(async () => {
4508
+ const start = Date.now();
4509
+ try {
4510
+ const { collection, project, repo: repoName, gitClient } = getAdoContext(ado, true);
4511
+ const data = await gitClient.getRepo(collection, project, repoName);
4512
+ success(data, "ado", "repo-get", start);
4513
+ } catch (err) {
4514
+ fail(err, "ado", "repo-get", start);
4515
+ }
4516
+ });
4517
+ repo.command("list-prs").description("List pull requests").option("--state <state>", "PR state: active|abandoned|completed|all", "active").option("--creator <alias>", "Filter by creator").option("--reviewer <alias>", "Filter by reviewer").action(async (opts) => {
4518
+ const start = Date.now();
4519
+ try {
4520
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4521
+ const data = await gitClient.listPRs(collection, project, repo2, {
4522
+ status: opts.state,
4523
+ creatorAlias: opts.creator,
4524
+ reviewerAlias: opts.reviewer
4525
+ });
4526
+ success(data, "ado", "repo-list-prs", start);
4527
+ } catch (err) {
4528
+ fail(err, "ado", "repo-list-prs", start);
4529
+ }
4530
+ });
4531
+ repo.command("get-pr").description("Get a pull request by ID").requiredOption("--id <n>", "Pull request ID").action(async (opts) => {
4532
+ const start = Date.now();
4533
+ try {
4534
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4535
+ const data = await gitClient.getPR(collection, project, repo2, parseInt(opts.id, 10));
4536
+ success(data, "ado", "repo-get-pr", start);
4537
+ } catch (err) {
4538
+ fail(err, "ado", "repo-get-pr", start);
4539
+ }
4540
+ });
4541
+ repo.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--source <branch>", "Source branch").option("--target <branch>", "Target branch (default: main)", "main").option("--description <text>", "PR description").option("--reviewers <ids>", "Comma-separated reviewer IDs or display names").action(async (opts) => {
4542
+ const start = Date.now();
4543
+ try {
4544
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4545
+ const reviewers = opts.reviewers ? opts.reviewers.split(",").map((id) => ({ id: id.trim() })) : [];
4546
+ const data = await gitClient.createPR(collection, project, repo2, {
4547
+ title: opts.title,
4548
+ description: opts.description,
4549
+ sourceRefName: `refs/heads/${opts.source}`,
4550
+ targetRefName: `refs/heads/${opts.target}`,
4551
+ reviewers
4552
+ });
4553
+ success(data, "ado", "repo-create-pr", start);
4554
+ } catch (err) {
4555
+ fail(err, "ado", "repo-create-pr", start);
4556
+ }
4557
+ });
4558
+ repo.command("update-pr").description("Update a pull request").requiredOption("--id <n>", "Pull request ID").option("--title <title>", "New title").option("--description <text>", "New description").option("--reviewers <ids>", "Comma-separated reviewer IDs").action(async (opts) => {
4559
+ const start = Date.now();
4560
+ try {
4561
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4562
+ const reviewers = opts.reviewers ? opts.reviewers.split(",").map((id) => ({ id: id.trim() })) : void 0;
4563
+ const body = {};
4564
+ if (opts.title) body.title = opts.title;
4565
+ if (opts.description !== void 0) body.description = opts.description;
4566
+ if (reviewers) body.reviewers = reviewers;
4567
+ const data = await gitClient.updatePR(collection, project, repo2, parseInt(opts.id, 10), body);
4568
+ success(data, "ado", "repo-update-pr", start);
4569
+ } catch (err) {
4570
+ fail(err, "ado", "repo-update-pr", start);
4571
+ }
4572
+ });
4573
+ repo.command("merge-pr").description("Complete (merge) a pull request").requiredOption("--id <n>", "Pull request ID").option("--strategy <s>", "Merge strategy: noFastForward|squash|rebase|rebaseMerge", "noFastForward").option("--delete-source", "Delete source branch after merge").action(async (opts) => {
4574
+ const start = Date.now();
4575
+ try {
4576
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4577
+ const prId = parseInt(opts.id, 10);
4578
+ const pr = await gitClient.getPR(collection, project, repo2, prId);
4579
+ const data = await gitClient.completePR(
4580
+ collection,
4581
+ project,
4582
+ repo2,
4583
+ prId,
4584
+ pr.lastMergeSourceCommit?.commitId ?? "",
4585
+ {
4586
+ mergeStrategy: opts.strategy,
4587
+ deleteSourceBranch: opts.deleteSource ?? false
4588
+ }
4589
+ );
4590
+ success(data, "ado", "repo-merge-pr", start);
4591
+ } catch (err) {
4592
+ fail(err, "ado", "repo-merge-pr", start);
4593
+ }
4594
+ });
4595
+ repo.command("abandon-pr").description("Abandon a pull request").requiredOption("--id <n>", "Pull request ID").action(async (opts) => {
4596
+ const start = Date.now();
4597
+ try {
4598
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4599
+ const data = await gitClient.abandonPR(collection, project, repo2, parseInt(opts.id, 10));
4600
+ success(data, "ado", "repo-abandon-pr", start);
4601
+ } catch (err) {
4602
+ fail(err, "ado", "repo-abandon-pr", start);
4603
+ }
4604
+ });
4605
+ repo.command("list-comments").description("List all comments on a pull request (general + inline)").requiredOption("--pr <n>", "Pull request ID").option("--inline-only", "Return only inline file comments").option("--general-only", "Return only general PR comments").action(async (opts) => {
4606
+ const start = Date.now();
4607
+ try {
4608
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4609
+ const threads = await gitClient.listThreads(collection, project, repo2, parseInt(opts.pr, 10));
4610
+ const flat = flattenThreadComments(threads, opts.inlineOnly ?? false, opts.generalOnly ?? false);
4611
+ success(flat, "ado", "repo-list-comments", start);
4612
+ } catch (err) {
4613
+ fail(err, "ado", "repo-list-comments", start);
4614
+ }
4615
+ });
4616
+ repo.command("add-comment").description("Add a general comment to a pull request").requiredOption("--pr <n>", "Pull request ID").requiredOption("--body <text>", "Comment text").action(async (opts) => {
4617
+ const start = Date.now();
4618
+ try {
4619
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4620
+ const data = await gitClient.createThread(collection, project, repo2, parseInt(opts.pr, 10), {
4621
+ comments: [{ content: opts.body, commentType: "text" }],
4622
+ status: "active"
4623
+ });
4624
+ success(data, "ado", "repo-add-comment", start);
4625
+ } catch (err) {
4626
+ fail(err, "ado", "repo-add-comment", start);
4627
+ }
4628
+ });
4629
+ repo.command("add-inline-comment").description("Add an inline comment on a file in a pull request").requiredOption("--pr <n>", "Pull request ID").requiredOption("--file <path>", "File path").requiredOption("--line <n>", "Line number").requiredOption("--body <text>", "Comment text").option("--line-type <side>", "Line side: left|right (default: right)", "right").action(async (opts) => {
4630
+ const start = Date.now();
4631
+ try {
4632
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4633
+ const lineNum = parseInt(opts.line, 10);
4634
+ const isRight = opts.lineType !== "left";
4635
+ const lineRange = isRight ? { rightFileStart: { line: lineNum, offset: 1 }, rightFileEnd: { line: lineNum, offset: 1 } } : { leftFileStart: { line: lineNum, offset: 1 }, leftFileEnd: { line: lineNum, offset: 1 } };
4636
+ const data = await gitClient.createThread(collection, project, repo2, parseInt(opts.pr, 10), {
4637
+ comments: [{ content: opts.body, commentType: "text" }],
4638
+ status: "active",
4639
+ threadContext: { filePath: opts.file, ...lineRange }
4640
+ });
4641
+ success(data, "ado", "repo-add-inline-comment", start);
4642
+ } catch (err) {
4643
+ fail(err, "ado", "repo-add-inline-comment", start);
4644
+ }
4645
+ });
4646
+ repo.command("reply-comment").description("Reply to a comment thread").requiredOption("--pr <n>", "Pull request ID").requiredOption("--thread-id <id>", "Thread ID").requiredOption("--body <text>", "Reply text").action(async (opts) => {
4647
+ const start = Date.now();
4648
+ try {
4649
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4650
+ const data = await gitClient.addCommentToThread(
4651
+ collection,
4652
+ project,
4653
+ repo2,
4654
+ parseInt(opts.pr, 10),
4655
+ parseInt(opts.threadId, 10),
4656
+ opts.body
4657
+ );
4658
+ success(data, "ado", "repo-reply-comment", start);
4659
+ } catch (err) {
4660
+ fail(err, "ado", "repo-reply-comment", start);
4661
+ }
4662
+ });
4663
+ repo.command("resolve-comment").description("Mark a comment thread as resolved (fixed)").requiredOption("--pr <n>", "Pull request ID").requiredOption("--thread-id <id>", "Thread ID").action(async (opts) => {
4664
+ const start = Date.now();
4665
+ try {
4666
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4667
+ const data = await gitClient.updateThread(
4668
+ collection,
4669
+ project,
4670
+ repo2,
4671
+ parseInt(opts.pr, 10),
4672
+ parseInt(opts.threadId, 10),
4673
+ "fixed"
4674
+ );
4675
+ success(data, "ado", "repo-resolve-comment", start);
4676
+ } catch (err) {
4677
+ fail(err, "ado", "repo-resolve-comment", start);
4678
+ }
4679
+ });
4680
+ repo.command("delete-comment").description("Delete a comment from a thread").requiredOption("--pr <n>", "Pull request ID").requiredOption("--thread-id <id>", "Thread ID").requiredOption("--comment-id <id>", "Comment ID").action(async (opts) => {
4681
+ const start = Date.now();
4682
+ try {
4683
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4684
+ await gitClient.deleteComment(
4685
+ collection,
4686
+ project,
4687
+ repo2,
4688
+ parseInt(opts.pr, 10),
4689
+ parseInt(opts.threadId, 10),
4690
+ parseInt(opts.commentId, 10)
4691
+ );
4692
+ success({ deleted: true }, "ado", "repo-delete-comment", start);
4693
+ } catch (err) {
4694
+ fail(err, "ado", "repo-delete-comment", start);
4695
+ }
4696
+ });
4697
+ repo.command("list-files").description("List files changed in a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4698
+ const start = Date.now();
4699
+ try {
4700
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4701
+ const data = await gitClient.listPRChanges(collection, project, repo2, parseInt(opts.pr, 10));
4702
+ success(data, "ado", "repo-list-files", start);
4703
+ } catch (err) {
4704
+ fail(err, "ado", "repo-list-files", start);
4705
+ }
4706
+ });
4707
+ repo.command("list-reviewers").description("List reviewers on a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4708
+ const start = Date.now();
4709
+ try {
4710
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4711
+ const data = await gitClient.listReviewers(collection, project, repo2, parseInt(opts.pr, 10));
4712
+ success(data, "ado", "repo-list-reviewers", start);
4713
+ } catch (err) {
4714
+ fail(err, "ado", "repo-list-reviewers", start);
4715
+ }
4716
+ });
4717
+ const voteCmd = (name, description, vote, action) => {
4718
+ repo.command(name).description(description).requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4719
+ const start = Date.now();
4720
+ try {
4721
+ const globalOpts = ado.optsWithGlobals();
4722
+ const { loadConfig: lc } = await import("./config-KECF7FJ2.js");
4723
+ const config = lc({ configPath: globalOpts.config });
4724
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4725
+ const { AdoCoreClient: AdoCoreClient2 } = await import("./core-AJYPJ2Z7.js");
4726
+ const { createHttpClient: createHttpClient2 } = await import("./http-FC24JSK7.js");
4727
+ const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
4728
+ const coreClient = new AdoCoreClient2(http);
4729
+ const me = await coreClient.getConnectionData(collection);
4730
+ const myId = me.authenticatedUser.id;
4731
+ const data = await gitClient.setPRVote(collection, project, repo2, parseInt(opts.pr, 10), myId, vote);
4732
+ success(data, "ado", action, start);
4733
+ } catch (err) {
4734
+ fail(err, "ado", action, start);
4735
+ }
4736
+ });
4737
+ };
4738
+ voteCmd("approve", "Approve a pull request", PR_VOTE.APPROVE, "repo-approve");
4739
+ voteCmd("unapprove", "Remove approval from a pull request", PR_VOTE.NO_VOTE, "repo-unapprove");
4740
+ voteCmd("wait-for-author", "Mark a pull request as waiting for author", PR_VOTE.WAIT_FOR_AUTHOR, "repo-wait-for-author");
4741
+ repo.command("list-builds").description("List builds associated with a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4742
+ const start = Date.now();
4743
+ try {
4744
+ const { collection, project, repo: repo2, gitClient, buildClient } = getAdoContext(ado, true);
4745
+ const pr = await gitClient.getPR(collection, project, repo2, parseInt(opts.pr, 10));
4746
+ const data = await buildClient.listBuilds(collection, project, {
4747
+ branchName: pr.sourceRefName
4748
+ });
4749
+ success(data, "ado", "repo-list-builds", start);
4750
+ } catch (err) {
4751
+ fail(err, "ado", "repo-list-builds", start);
4752
+ }
4753
+ });
4754
+ }
4755
+
4756
+ // src/services/ado/commands/pipeline.ts
4757
+ function registerAdoPipelineCommands(ado) {
4758
+ const pipeline = ado.command("pipeline").description("Azure DevOps pipeline (build) operations");
4759
+ pipeline.command("list").description("List pipeline definitions").action(async () => {
4760
+ const start = Date.now();
4761
+ try {
4762
+ const { collection, project, buildClient } = getAdoContext(ado);
4763
+ const data = await buildClient.listDefinitions(collection, project);
4764
+ success(data, "ado", "pipeline-list", start);
4765
+ } catch (err) {
4766
+ fail(err, "ado", "pipeline-list", start);
4767
+ }
4768
+ });
4769
+ pipeline.command("get").description("Get a pipeline definition by ID").requiredOption("--id <n>", "Pipeline definition ID").action(async (opts) => {
4770
+ const start = Date.now();
4771
+ try {
4772
+ const { collection, project, buildClient } = getAdoContext(ado);
4773
+ const data = await buildClient.getDefinition(collection, project, parseInt(opts.id, 10));
4774
+ success(data, "ado", "pipeline-get", start);
4775
+ } catch (err) {
4776
+ fail(err, "ado", "pipeline-get", start);
4777
+ }
4778
+ });
4779
+ pipeline.command("run").description("Queue a pipeline run").requiredOption("--id <n>", "Pipeline definition ID").option("--branch <ref>", "Source branch (e.g. refs/heads/main or main)").option("--parameter <k=v>", "Build parameter (repeatable)", (v, acc) => {
4780
+ acc.push(v);
4781
+ return acc;
4782
+ }, []).option("--wait", "Wait for the run to complete before returning").option("--timeout <s>", "Max wait time in seconds (default 600)", "600").option("--poll <s>", "Poll interval in seconds (default 10)", "10").action(async (opts) => {
4783
+ const start = Date.now();
4784
+ try {
4785
+ const { collection, project, buildClient } = getAdoContext(ado);
4786
+ const sourceBranch = opts.branch ? opts.branch.startsWith("refs/") ? opts.branch : `refs/heads/${opts.branch}` : void 0;
4787
+ for (const p of opts.parameter) {
4788
+ if (!p.includes("=")) throw new PncliError(`Invalid --parameter "${p}". Expected format: key=value`, 1);
4789
+ }
4790
+ const parameters = opts.parameter.length > 0 ? JSON.stringify(Object.fromEntries(opts.parameter.map((p) => {
4791
+ const eq = p.indexOf("=");
4792
+ return [p.slice(0, eq), p.slice(eq + 1)];
4793
+ }))) : void 0;
4794
+ let build = await buildClient.queueBuild(collection, project, parseInt(opts.id, 10), {
4795
+ sourceBranch,
4796
+ parameters
4797
+ });
4798
+ log(`Queued build #${build.id} (${build.buildNumber}) \u2014 status: ${build.status}`);
4799
+ if (opts.wait) {
4800
+ const timeoutSec = parseInt(opts.timeout, 10);
4801
+ const pollSec = parseInt(opts.poll, 10);
4802
+ if (isNaN(timeoutSec) || timeoutSec <= 0) throw new PncliError(`Invalid --timeout "${opts.timeout}". Expected a positive number of seconds.`, 1);
4803
+ if (isNaN(pollSec) || pollSec <= 0) throw new PncliError(`Invalid --poll "${opts.poll}". Expected a positive number of seconds.`, 1);
4804
+ log(`Waiting for build #${build.id} to complete (timeout ${timeoutSec}s, poll ${pollSec}s)...`);
4805
+ build = await pollUntilTerminal(
4806
+ () => buildClient.getBuild(collection, project, build.id),
4807
+ isBuildTerminal,
4808
+ { timeoutSec, pollSec }
4809
+ );
4810
+ if (build.result && build.result !== "succeeded") {
4811
+ process.exitCode = ExitCode.GENERAL_ERROR;
4812
+ }
4813
+ }
4814
+ success(build, "ado", "pipeline-run", start);
4815
+ } catch (err) {
4816
+ fail(err, "ado", "pipeline-run", start);
4817
+ }
4818
+ });
4819
+ pipeline.command("list-runs").description("List pipeline runs").option("--definition <id>", "Filter by definition ID").option("--branch <ref>", "Filter by branch name").option("--status <filter>", "Filter by status (inProgress|completed|cancelling|...)").option("--top <n>", "Maximum results", "50").action(async (opts) => {
4820
+ const start = Date.now();
4821
+ try {
4822
+ const { collection, project, buildClient } = getAdoContext(ado);
4823
+ const data = await buildClient.listBuilds(collection, project, {
4824
+ definitionIds: opts.definition ? [parseInt(opts.definition, 10)] : void 0,
4825
+ branchName: opts.branch,
4826
+ statusFilter: opts.status,
4827
+ top: parseInt(opts.top, 10)
4828
+ });
4829
+ success(data, "ado", "pipeline-list-runs", start);
4830
+ } catch (err) {
4831
+ fail(err, "ado", "pipeline-list-runs", start);
4832
+ }
4833
+ });
4834
+ pipeline.command("get-run").description("Get a pipeline run by build ID").requiredOption("--id <n>", "Build ID").action(async (opts) => {
4835
+ const start = Date.now();
4836
+ try {
4837
+ const { collection, project, buildClient } = getAdoContext(ado);
4838
+ const data = await buildClient.getBuild(collection, project, parseInt(opts.id, 10));
4839
+ success(data, "ado", "pipeline-get-run", start);
4840
+ } catch (err) {
4841
+ fail(err, "ado", "pipeline-get-run", start);
4842
+ }
4843
+ });
4844
+ pipeline.command("cancel-run").description("Cancel a running pipeline build").requiredOption("--id <n>", "Build ID").action(async (opts) => {
4845
+ const start = Date.now();
4846
+ try {
4847
+ const { collection, project, buildClient } = getAdoContext(ado);
4848
+ const data = await buildClient.cancelBuild(collection, project, parseInt(opts.id, 10));
4849
+ success(data, "ado", "pipeline-cancel-run", start);
4850
+ } catch (err) {
4851
+ fail(err, "ado", "pipeline-cancel-run", start);
4852
+ }
4853
+ });
4854
+ pipeline.command("logs").description("List or retrieve build logs").requiredOption("--id <n>", "Build ID").option("--log-id <n>", "Specific log ID (omit to list all logs)").action(async (opts) => {
4855
+ const start = Date.now();
4856
+ try {
4857
+ const { collection, project, buildClient } = getAdoContext(ado);
4858
+ const buildId = parseInt(opts.id, 10);
4859
+ if (opts.logId) {
4860
+ const data = await buildClient.getLog(collection, project, buildId, parseInt(opts.logId, 10));
4861
+ success({ log: data }, "ado", "pipeline-logs", start);
4862
+ } else {
4863
+ const data = await buildClient.listLogs(collection, project, buildId);
4864
+ success(data, "ado", "pipeline-logs", start);
4865
+ }
4866
+ } catch (err) {
4867
+ fail(err, "ado", "pipeline-logs", start);
4868
+ }
4869
+ });
4870
+ }
4871
+
4872
+ // src/services/ado/commands/index.ts
4873
+ function registerAdoCommands(program2) {
4874
+ const ado = program2.command("ado").description("Azure DevOps Server operations").option("--collection <name>", "Azure DevOps collection (organization)").option("--organization <name>", "Alias for --collection").option("--project <name>", "Azure DevOps team project").option("--repo <name>", "Azure DevOps git repository name");
4875
+ ado.command("whoami").description("Show current authenticated user").action(async () => {
4876
+ const start = Date.now();
4877
+ try {
4878
+ const { collection, coreClient } = getAdoContext(ado);
4879
+ const data = await coreClient.getConnectionData(collection);
4880
+ success(data, "ado", "whoami", start);
4881
+ } catch (err) {
4882
+ fail(err, "ado", "whoami", start);
4883
+ }
4884
+ });
4885
+ ado.command("connection-data").description("Show raw connection data from the ADO server").action(async () => {
4886
+ const start = Date.now();
4887
+ try {
4888
+ const { collection, coreClient } = getAdoContext(ado);
4889
+ const data = await coreClient.getConnectionData(collection);
4890
+ success(data, "ado", "connection-data", start);
4891
+ } catch (err) {
4892
+ fail(err, "ado", "connection-data", start);
4893
+ }
4894
+ });
4895
+ registerAdoProjectCommands(ado);
4896
+ registerAdoWorkCommands(ado);
4897
+ registerAdoRepoCommands(ado);
4898
+ registerAdoPipelineCommands(ado);
4899
+ }
4900
+
4183
4901
  // src/cli.ts
4184
4902
  if (!process.env.PNCLI_VERIFY_TLS) {
4185
4903
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -4209,6 +4927,7 @@ registerSonarCommands(program);
4209
4927
  registerSdeCommands(program);
4210
4928
  registerDepsCommands(program);
4211
4929
  registerConfigCommands(program);
4930
+ registerAdoCommands(program);
4212
4931
  program.addHelpText("after", `
4213
4932
  Services:
4214
4933
  git Local git operations (status, diff, log, branch)
@@ -4218,6 +4937,7 @@ Services:
4218
4937
  confluence Confluence
4219
4938
  sonar SonarQube Server (quality gates, issues, metrics, hotspots)
4220
4939
  sde SDElements (threat modeling, countermeasures, compliance)
4940
+ ado Azure DevOps Server (work items, repos, PRs, pipelines)
4221
4941
  config Manage pncli configuration
4222
4942
  `);
4223
4943
  program.parseAsync(process.argv).catch((err) => {