@kolatts/pncli 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,276 +1,37 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getGlobalConfigPath,
4
+ loadConfig,
5
+ maskConfig,
6
+ setConfigValue,
7
+ setRepoConfigValue,
8
+ writeGlobalConfig,
9
+ writeRepoConfig
10
+ } from "./chunk-VEHCNGUP.js";
11
+ import {
12
+ ExitCode,
13
+ PncliError,
14
+ createHttpClient,
15
+ fail,
16
+ log,
17
+ setGlobalOptions,
18
+ setGlobalUser,
19
+ success,
20
+ warn
21
+ } from "./chunk-HWXXNMGE.js";
22
+ import {
23
+ AdoCoreClient
24
+ } from "./chunk-KT3XINLJ.js";
2
25
 
3
26
  // src/cli.ts
4
27
  import { Command } from "commander";
5
28
  import { createRequire } from "module";
6
29
 
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
30
  // src/services/git/client.ts
270
- import { execSync as execSync2, execFileSync } from "child_process";
31
+ import { execSync, execFileSync } from "child_process";
271
32
  var DIFF_LINE_LIMIT = 5e3;
272
33
  function exec(cmd, cwd) {
273
- return execSync2(cmd, { encoding: "utf8", cwd }).trim();
34
+ return execSync(cmd, { encoding: "utf8", cwd }).trim();
274
35
  }
275
36
  function getStatus(root) {
276
37
  const output = exec("git status --porcelain", root);
@@ -402,17 +163,17 @@ function getBranches(root) {
402
163
  }
403
164
 
404
165
  // src/lib/git-context.ts
405
- import { execSync as execSync3 } from "child_process";
406
- function getRepoRoot2() {
166
+ import { execSync as execSync2 } from "child_process";
167
+ function getRepoRoot() {
407
168
  try {
408
- return execSync3("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
169
+ return execSync2("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
409
170
  } catch {
410
171
  return null;
411
172
  }
412
173
  }
413
174
  function getCurrentBranch(repoRoot) {
414
175
  try {
415
- return execSync3("git rev-parse --abbrev-ref HEAD", {
176
+ return execSync2("git rev-parse --abbrev-ref HEAD", {
416
177
  encoding: "utf8",
417
178
  cwd: repoRoot
418
179
  }).trim();
@@ -439,16 +200,59 @@ function parseRemote(remoteUrl, bitbucketBaseUrl) {
439
200
  }
440
201
  return null;
441
202
  }
203
+ function parseAdoRemote(remoteUrl, adoBaseUrl) {
204
+ if (!adoBaseUrl) return null;
205
+ let baseHostname;
206
+ try {
207
+ const normalizedBase = /^https?:\/\//.test(adoBaseUrl) ? adoBaseUrl : `https://${adoBaseUrl}`;
208
+ baseHostname = new URL(normalizedBase).hostname.toLowerCase();
209
+ } catch {
210
+ baseHostname = adoBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "").split(/[:/]/)[0].toLowerCase();
211
+ }
212
+ let path6 = remoteUrl;
213
+ let remoteHostname = "";
214
+ const sshProtoMatch = path6.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
215
+ if (sshProtoMatch) {
216
+ remoteHostname = sshProtoMatch[1].toLowerCase();
217
+ path6 = sshProtoMatch[2];
218
+ } else {
219
+ const gitAtMatch = path6.match(/^git@([^:]+)(?::\d+)?:(.*)/);
220
+ if (gitAtMatch) {
221
+ remoteHostname = gitAtMatch[1].toLowerCase();
222
+ path6 = "/" + gitAtMatch[2];
223
+ } else {
224
+ const httpsMatch = path6.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
225
+ if (httpsMatch) {
226
+ remoteHostname = httpsMatch[1].toLowerCase();
227
+ path6 = httpsMatch[2];
228
+ }
229
+ }
230
+ }
231
+ if (!remoteHostname || remoteHostname !== baseHostname) return null;
232
+ path6 = path6.replace(/\.git$/, "");
233
+ const gitIdx = path6.indexOf("/_git/");
234
+ const sshIdx = path6.indexOf("/_ssh/");
235
+ const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
236
+ if (markerIdx === -1) return null;
237
+ const repo = path6.slice(markerIdx + 6);
238
+ if (!repo) return null;
239
+ const before = path6.slice(1, markerIdx);
240
+ const parts = before.split("/").filter(Boolean);
241
+ if (parts.length < 2) return null;
242
+ const project = parts[parts.length - 1];
243
+ const collection = parts[parts.length - 2];
244
+ return { collection, project, repo };
245
+ }
442
246
  function getRemoteUrls(repoRoot) {
443
247
  try {
444
- const output = execSync3("git remote -v", { encoding: "utf8", cwd: repoRoot });
248
+ const output = execSync2("git remote -v", { encoding: "utf8", cwd: repoRoot });
445
249
  return output.split("\n").filter((line) => line.includes("(fetch)")).map((line) => line.split(" ")[1]?.split(" ")[0] ?? "").filter(Boolean);
446
250
  } catch {
447
251
  return [];
448
252
  }
449
253
  }
450
254
  function getGitContext(config) {
451
- const root = getRepoRoot2();
255
+ const root = getRepoRoot();
452
256
  if (!root) return null;
453
257
  const branch = getCurrentBranch(root) ?? "unknown";
454
258
  const remoteUrls = getRemoteUrls(root);
@@ -462,320 +266,15 @@ function getGitContext(config) {
462
266
  break;
463
267
  }
464
268
  }
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++;
269
+ let adoContext = null;
270
+ for (const url of remoteUrls) {
271
+ const parsed = parseAdoRemote(url, config.ado.baseUrl);
272
+ if (parsed) {
273
+ adoContext = parsed;
274
+ break;
692
275
  }
693
- return results;
694
276
  }
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
- }
776
- };
777
- function createHttpClient(config, dryRun = false) {
778
- return new HttpClient(config, dryRun);
277
+ return { root, branch, project, repo, ado: adoContext };
779
278
  }
780
279
 
781
280
  // src/services/bitbucket/client.ts
@@ -860,14 +359,29 @@ var BitbucketClient = class {
860
359
  { method: "POST", params: { version } }
861
360
  );
862
361
  }
863
- async listComments(project, repo, prId) {
362
+ async listComments(project, repo, prId, opts = {}) {
363
+ const { withReplies = true, inlineOnly = false, generalOnly = false } = opts;
864
364
  const activities = await this.http.paginate(
865
365
  (start, limit) => this.http.bitbucket(
866
366
  `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/activities`,
867
367
  { params: { start, limit } }
868
368
  )
869
369
  );
870
- return activities.filter((a) => a.action === "COMMENTED" && a.comment).map((a) => a.comment);
370
+ const topLevel = activities.filter((a) => a.action === "COMMENTED" && a.comment).map((a) => a.comment);
371
+ if (!withReplies) {
372
+ return topLevel.filter((c) => !inlineOnly || !!c.anchor).filter((c) => !generalOnly || !c.anchor);
373
+ }
374
+ function flatten(comment, parentId) {
375
+ const node = {
376
+ ...comment,
377
+ comments: void 0,
378
+ ...parentId !== void 0 ? { parentId } : {}
379
+ };
380
+ const replies = (comment.comments ?? []).flatMap((c) => flatten(c, comment.id));
381
+ return [node, ...replies];
382
+ }
383
+ const flat = topLevel.flatMap((c) => flatten(c));
384
+ return flat.filter((c) => !inlineOnly || !!c.anchor).filter((c) => !generalOnly || !c.anchor).sort((a, b) => a.createdDate - b.createdDate);
871
385
  }
872
386
  async addComment(project, repo, prId, text) {
873
387
  return this.http.bitbucket(
@@ -968,7 +482,7 @@ var BitbucketClient = class {
968
482
 
969
483
  // src/services/git/commands.ts
970
484
  function requireRepoRoot() {
971
- const root = getRepoRoot2();
485
+ const root = getRepoRoot();
972
486
  if (!root) throw new PncliError("Not a git repository", 1);
973
487
  return root;
974
488
  }
@@ -1489,11 +1003,15 @@ function registerBitbucketCommands(program2) {
1489
1003
  fail(err, "bitbucket", "decline-pr", start);
1490
1004
  }
1491
1005
  });
1492
- bb.command("list-comments").description("List comments on a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
1006
+ 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
1007
  const start = Date.now();
1494
1008
  try {
1495
1009
  const { client, project, repo } = getClient2(program2);
1496
- const data = await client.listComments(project, repo, parseInt(opts.pr, 10));
1010
+ const data = await client.listComments(project, repo, parseInt(opts.pr, 10), {
1011
+ withReplies: opts.withReplies,
1012
+ inlineOnly: opts.inlineOnly,
1013
+ generalOnly: opts.generalOnly
1014
+ });
1497
1015
  success(data, "bitbucket", "list-comments", start);
1498
1016
  } catch (err) {
1499
1017
  fail(err, "bitbucket", "list-comments", start);
@@ -2522,24 +2040,24 @@ function registerSdeCommands(program2) {
2522
2040
  }
2523
2041
 
2524
2042
  // 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";
2043
+ import fs from "fs";
2044
+ import path4 from "path";
2045
+ import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
2528
2046
 
2529
2047
  // src/services/deps/parsers/npm.ts
2530
- import path2 from "path";
2048
+ import path from "path";
2531
2049
  function findNpmManifests(files) {
2532
2050
  const lockFiles = new Set(
2533
2051
  files.filter((f) => {
2534
- const base = path2.basename(f);
2052
+ const base = path.basename(f);
2535
2053
  return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
2536
2054
  })
2537
2055
  );
2538
2056
  const manifests = [];
2539
2057
  for (const file of files) {
2540
- if (path2.basename(file) !== "package.json") continue;
2058
+ if (path.basename(file) !== "package.json") continue;
2541
2059
  if (file.split("/").includes("node_modules")) continue;
2542
- const dir = path2.dirname(file);
2060
+ const dir = path.dirname(file);
2543
2061
  let lockFile;
2544
2062
  for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
2545
2063
  const candidate = dir === "." ? lf : `${dir}/${lf}`;
@@ -2554,7 +2072,7 @@ function findNpmManifests(files) {
2554
2072
  }
2555
2073
  function parseNpmPackages(content, manifest, opts, lockContent) {
2556
2074
  if (lockContent && manifest.lockFile) {
2557
- const base = path2.basename(manifest.lockFile);
2075
+ const base = path.basename(manifest.lockFile);
2558
2076
  if (base === "package-lock.json") {
2559
2077
  const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
2560
2078
  if (pkgs.length > 0) return pkgs;
@@ -2702,26 +2220,26 @@ function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
2702
2220
  }
2703
2221
 
2704
2222
  // src/services/deps/parsers/nuget.ts
2705
- import path3 from "path";
2223
+ import path2 from "path";
2706
2224
  var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
2707
2225
  function findNugetManifests(files) {
2708
2226
  const lockFiles = /* @__PURE__ */ new Map();
2709
2227
  for (const f of files) {
2710
- if (path3.basename(f) === "packages.lock.json") {
2711
- lockFiles.set(path3.dirname(f), f);
2228
+ if (path2.basename(f) === "packages.lock.json") {
2229
+ lockFiles.set(path2.dirname(f), f);
2712
2230
  }
2713
2231
  }
2714
2232
  const manifests = [];
2715
2233
  for (const file of files) {
2716
- const base = path3.basename(file);
2717
- const ext = path3.extname(file);
2234
+ const base = path2.basename(file);
2235
+ const ext = path2.extname(file);
2718
2236
  if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
2719
- const dir = path3.dirname(file);
2237
+ const dir = path2.dirname(file);
2720
2238
  const lockFile = lockFiles.get(dir);
2721
2239
  manifests.push({ file, ecosystem: "nuget", lockFile });
2722
2240
  }
2723
2241
  if (base === "Directory.Packages.props") {
2724
- const dir = path3.dirname(file);
2242
+ const dir = path2.dirname(file);
2725
2243
  manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
2726
2244
  }
2727
2245
  }
@@ -2740,7 +2258,7 @@ function findNugetManifests(files) {
2740
2258
  return deduped;
2741
2259
  }
2742
2260
  function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
2743
- const base = path3.basename(manifest.file);
2261
+ const base = path2.basename(manifest.file);
2744
2262
  if (lockContent) {
2745
2263
  return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
2746
2264
  }
@@ -2861,14 +2379,14 @@ function parsePackagesLock(content, filePath, opts) {
2861
2379
  }
2862
2380
 
2863
2381
  // src/services/deps/parsers/maven.ts
2864
- import path4 from "path";
2382
+ import path3 from "path";
2865
2383
  function findMavenManifests(files) {
2866
- const lockFiles = new Set(files.filter((f) => path4.basename(f) === "gradle.lockfile"));
2384
+ const lockFiles = new Set(files.filter((f) => path3.basename(f) === "gradle.lockfile"));
2867
2385
  const manifests = [];
2868
2386
  for (const file of files) {
2869
- const base = path4.basename(file);
2387
+ const base = path3.basename(file);
2870
2388
  if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
2871
- const dir = path4.dirname(file);
2389
+ const dir = path3.dirname(file);
2872
2390
  const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
2873
2391
  const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
2874
2392
  manifests.push({ file, ecosystem: "maven", lockFile });
@@ -2877,7 +2395,7 @@ function findMavenManifests(files) {
2877
2395
  return manifests;
2878
2396
  }
2879
2397
  function parseMavenPackages(content, manifest, opts, lockContent) {
2880
- const base = path4.basename(manifest.file);
2398
+ const base = path3.basename(manifest.file);
2881
2399
  if (lockContent && manifest.lockFile) {
2882
2400
  return parseGradleLock(lockContent, manifest.lockFile, opts);
2883
2401
  }
@@ -3026,7 +2544,7 @@ function parseGradleLock(content, filePath, opts) {
3026
2544
  // src/services/deps/parsers/index.ts
3027
2545
  function getRepoFiles(repoRoot) {
3028
2546
  try {
3029
- const out = execSync4("git ls-files --cached --others --exclude-standard", {
2547
+ const out = execSync3("git ls-files --cached --others --exclude-standard", {
3030
2548
  encoding: "utf8",
3031
2549
  cwd: repoRoot,
3032
2550
  maxBuffer: 10 * 1024 * 1024
@@ -3038,7 +2556,7 @@ function getRepoFiles(repoRoot) {
3038
2556
  }
3039
2557
  function readFile(repoRoot, relPath) {
3040
2558
  try {
3041
- return fs4.readFileSync(path5.join(repoRoot, relPath), "utf8");
2559
+ return fs.readFileSync(path4.join(repoRoot, relPath), "utf8");
3042
2560
  } catch {
3043
2561
  return null;
3044
2562
  }
@@ -3056,13 +2574,13 @@ function readFileAtRef(repoRoot, ref, relPath) {
3056
2574
  }
3057
2575
  }
3058
2576
  function findPropsContent(manifestFile, readFn) {
3059
- let dir = path5.dirname(manifestFile);
2577
+ let dir = path4.dirname(manifestFile);
3060
2578
  for (let depth = 0; depth < 10; depth++) {
3061
2579
  const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
3062
2580
  const content = readFn(candidate);
3063
2581
  if (content) return content;
3064
2582
  if (dir === ".") break;
3065
- dir = path5.dirname(dir);
2583
+ dir = path4.dirname(dir);
3066
2584
  }
3067
2585
  return null;
3068
2586
  }
@@ -3149,7 +2667,7 @@ function buildScanData(manifests, packages) {
3149
2667
  // src/services/deps/scan.ts
3150
2668
  function runScan(config, opts) {
3151
2669
  void config;
3152
- const repoRoot = getRepoRoot2();
2670
+ const repoRoot = getRepoRoot();
3153
2671
  if (!repoRoot) {
3154
2672
  throw new PncliError("Not inside a git repository. deps commands require a git repo root.", 1);
3155
2673
  }
@@ -3196,7 +2714,7 @@ function isDowngrade(from, to) {
3196
2714
  // src/services/deps/diff.ts
3197
2715
  function runDiff(config, from, to, opts) {
3198
2716
  void config;
3199
- const repoRoot = getRepoRoot2();
2717
+ const repoRoot = getRepoRoot();
3200
2718
  if (!repoRoot) {
3201
2719
  throw new PncliError("Not inside a git repository.", 1);
3202
2720
  }
@@ -3252,7 +2770,7 @@ var OSV_ECOSYSTEM = {
3252
2770
  nuget: "NuGet",
3253
2771
  maven: "Maven"
3254
2772
  };
3255
- async function fetchWithTimeout2(url, init) {
2773
+ async function fetchWithTimeout(url, init) {
3256
2774
  const controller = new AbortController();
3257
2775
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
3258
2776
  try {
@@ -3263,7 +2781,7 @@ async function fetchWithTimeout2(url, init) {
3263
2781
  }
3264
2782
  async function checkOsvConnectivity() {
3265
2783
  try {
3266
- const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
2784
+ const res = await fetchWithTimeout(`${OSV_URL}/v1/querybatch`, {
3267
2785
  method: "POST",
3268
2786
  headers: { "Content-Type": "application/json" },
3269
2787
  body: JSON.stringify({ queries: [] })
@@ -3335,7 +2853,7 @@ function mapVuln(vuln, pkg2) {
3335
2853
  };
3336
2854
  }
3337
2855
  async function queryBatch(queries, packages) {
3338
- const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
2856
+ const res = await fetchWithTimeout(`${OSV_URL}/v1/querybatch`, {
3339
2857
  method: "POST",
3340
2858
  headers: { "Content-Type": "application/json" },
3341
2859
  body: JSON.stringify({ queries })
@@ -3377,7 +2895,7 @@ async function checkPackagesForVulns(packages) {
3377
2895
 
3378
2896
  // src/services/deps/clients/artifactory.ts
3379
2897
  var TIMEOUT_MS2 = 15e3;
3380
- async function fetchWithTimeout3(url, token) {
2898
+ async function fetchWithTimeout2(url, token) {
3381
2899
  const controller = new AbortController();
3382
2900
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
3383
2901
  try {
@@ -3415,7 +2933,7 @@ async function checkArtifactoryConnectivity(config) {
3415
2933
  };
3416
2934
  }
3417
2935
  try {
3418
- const res = await fetchWithTimeout3(`${config.baseUrl}/api/system/ping`, config.token);
2936
+ const res = await fetchWithTimeout2(`${config.baseUrl}/api/system/ping`, config.token);
3419
2937
  if (res.status === 401 || res.status === 403) {
3420
2938
  return {
3421
2939
  reachable: true,
@@ -3465,7 +2983,7 @@ function repoForEcosystem(config, eco) {
3465
2983
  async function getLatestNpm(baseUrl, token, repoName, packageName) {
3466
2984
  try {
3467
2985
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
3468
- const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
2986
+ const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3469
2987
  if (!res.ok) return null;
3470
2988
  const data = await res.json();
3471
2989
  return data["dist-tags"]?.latest ?? null;
@@ -3476,7 +2994,7 @@ async function getLatestNpm(baseUrl, token, repoName, packageName) {
3476
2994
  async function getLatestNuget(baseUrl, token, repoName, packageName) {
3477
2995
  try {
3478
2996
  const lower = packageName.toLowerCase();
3479
- const res = await fetchWithTimeout3(
2997
+ const res = await fetchWithTimeout2(
3480
2998
  `${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
3481
2999
  token
3482
3000
  );
@@ -3492,7 +3010,7 @@ async function getLatestMaven(baseUrl, token, repoName, packageName) {
3492
3010
  const [groupId, artifactId] = packageName.split(":");
3493
3011
  if (!groupId || !artifactId) return null;
3494
3012
  try {
3495
- const res = await fetchWithTimeout3(
3013
+ const res = await fetchWithTimeout2(
3496
3014
  `${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
3497
3015
  token
3498
3016
  );
@@ -3542,7 +3060,7 @@ async function getOutdatedPackages(packages, config, filterType) {
3542
3060
  async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3543
3061
  try {
3544
3062
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
3545
- const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3063
+ const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3546
3064
  if (!res.ok) return null;
3547
3065
  const data = await res.json();
3548
3066
  if (typeof data.license === "string") return data.license;
@@ -3555,7 +3073,7 @@ async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3555
3073
  async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
3556
3074
  try {
3557
3075
  const lower = packageName.toLowerCase();
3558
- const res = await fetchWithTimeout3(
3076
+ const res = await fetchWithTimeout2(
3559
3077
  `${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
3560
3078
  token
3561
3079
  );
@@ -3651,7 +3169,7 @@ async function buildConnectivityData(config) {
3651
3169
 
3652
3170
  // src/services/deps/frisk.ts
3653
3171
  async function runFrisk(config, opts) {
3654
- const repoRoot = getRepoRoot2();
3172
+ const repoRoot = getRepoRoot();
3655
3173
  if (!repoRoot) {
3656
3174
  throw new PncliError("Not inside a git repository.", 1);
3657
3175
  }
@@ -3681,7 +3199,7 @@ async function runFrisk(config, opts) {
3681
3199
 
3682
3200
  // src/services/deps/outdated.ts
3683
3201
  async function runOutdated(config, opts, filterType) {
3684
- const repoRoot = getRepoRoot2();
3202
+ const repoRoot = getRepoRoot();
3685
3203
  if (!repoRoot) {
3686
3204
  throw new PncliError("Not inside a git repository.", 1);
3687
3205
  }
@@ -3713,7 +3231,7 @@ async function runOutdated(config, opts, filterType) {
3713
3231
 
3714
3232
  // src/services/deps/license-check.ts
3715
3233
  async function runLicenseCheck(config, opts) {
3716
- const repoRoot = getRepoRoot2();
3234
+ const repoRoot = getRepoRoot();
3717
3235
  if (!repoRoot) {
3718
3236
  throw new PncliError("Not inside a git repository.", 1);
3719
3237
  }
@@ -3843,7 +3361,145 @@ function registerDepsCommands(program2) {
3843
3361
 
3844
3362
  // src/services/config/commands.ts
3845
3363
  import { input, password, confirm } from "@inquirer/prompts";
3846
- import fs5 from "fs";
3364
+
3365
+ // src/services/ado/client/work.ts
3366
+ var API6 = "7.1";
3367
+ var API_PREVIEW = "7.1-preview.4";
3368
+ var AdoWorkClient = class {
3369
+ constructor(http) {
3370
+ this.http = http;
3371
+ }
3372
+ http;
3373
+ async getWorkItem(collection, id) {
3374
+ return this.http.ado(
3375
+ `/${encodeURIComponent(collection)}/_apis/wit/workitems/${id}?api-version=${API6}&$expand=all`
3376
+ );
3377
+ }
3378
+ async createWorkItem(collection, project, type, patch) {
3379
+ return this.http.ado(
3380
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitems/${encodeURIComponent("$" + type)}?api-version=${API6}`,
3381
+ { method: "POST", body: patch, headers: { "Content-Type": "application/json-patch+json" } }
3382
+ );
3383
+ }
3384
+ async updateWorkItem(collection, id, patch) {
3385
+ return this.http.ado(
3386
+ `/${encodeURIComponent(collection)}/_apis/wit/workitems/${id}?api-version=${API6}`,
3387
+ { method: "PATCH", body: patch, headers: { "Content-Type": "application/json-patch+json" } }
3388
+ );
3389
+ }
3390
+ async queryWiql(collection, project, wiql) {
3391
+ return this.http.ado(
3392
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/wiql?api-version=${API6}`,
3393
+ { method: "POST", body: { query: wiql } }
3394
+ );
3395
+ }
3396
+ async listComments(collection, project, workItemId) {
3397
+ const result = await this.http.ado(
3398
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?api-version=${API_PREVIEW}`
3399
+ );
3400
+ return result.value ?? [];
3401
+ }
3402
+ async addComment(collection, project, workItemId, text) {
3403
+ return this.http.ado(
3404
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?api-version=${API_PREVIEW}`,
3405
+ { method: "POST", body: { text } }
3406
+ );
3407
+ }
3408
+ async listWorkItemTypes(collection, project) {
3409
+ const result = await this.http.ado(
3410
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes?api-version=${API6}`
3411
+ );
3412
+ return result.value ?? [];
3413
+ }
3414
+ async listTypeStates(collection, project, type) {
3415
+ const result = await this.http.ado(
3416
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes/${encodeURIComponent(type)}/states?api-version=${API6}`
3417
+ );
3418
+ return result.value ?? [];
3419
+ }
3420
+ async listFields(collection, project) {
3421
+ const scope = project ? `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}` : `/${encodeURIComponent(collection)}`;
3422
+ const result = await this.http.ado(
3423
+ `${scope}/_apis/wit/fields?api-version=${API6}`
3424
+ );
3425
+ return result.value ?? [];
3426
+ }
3427
+ async listTypeFields(collection, project, type) {
3428
+ const result = await this.http.ado(
3429
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes/${encodeURIComponent(type)}/fields?api-version=${API6}`
3430
+ );
3431
+ return result.value ?? [];
3432
+ }
3433
+ };
3434
+
3435
+ // src/services/ado/discovery.ts
3436
+ async function discoverFields(client, collection, project) {
3437
+ const fields = await client.listFields(collection, project);
3438
+ return fields.map(toFieldMeta);
3439
+ }
3440
+ async function discoverTypeFields(client, collection, project, type) {
3441
+ const fields = await client.listTypeFields(collection, project, type);
3442
+ return fields.map(toFieldMeta);
3443
+ }
3444
+ function toFieldMeta(f) {
3445
+ return {
3446
+ referenceName: f.referenceName,
3447
+ name: f.name,
3448
+ type: f.type,
3449
+ readOnly: f.readOnly,
3450
+ ...f.picklistId ? { picklistId: f.picklistId } : {}
3451
+ };
3452
+ }
3453
+ async function discoverTypes(client, collection, project) {
3454
+ const types = await client.listWorkItemTypes(collection, project);
3455
+ const result = [];
3456
+ for (const t of types) {
3457
+ const states = await client.listTypeStates(collection, project, t.name);
3458
+ const requiredFields = (t.fields ?? []).filter((f) => f.alwaysRequired).map((f) => f.referenceName);
3459
+ result.push({
3460
+ name: t.name,
3461
+ states: states.map((s) => s.name),
3462
+ requiredFields
3463
+ });
3464
+ }
3465
+ return result;
3466
+ }
3467
+ function buildDefaultAliases(fields) {
3468
+ const refNames = new Set(fields.map((f) => f.referenceName));
3469
+ const candidates = [
3470
+ ["title", "System.Title"],
3471
+ ["state", "System.State"],
3472
+ ["assignedto", "System.AssignedTo"],
3473
+ ["assigned-to", "System.AssignedTo"],
3474
+ ["description", "System.Description"],
3475
+ ["areapath", "System.AreaPath"],
3476
+ ["area-path", "System.AreaPath"],
3477
+ ["iterationpath", "System.IterationPath"],
3478
+ ["iteration", "System.IterationPath"],
3479
+ ["tags", "System.Tags"],
3480
+ ["priority", "Microsoft.VSTS.Common.Priority"],
3481
+ ["severity", "Microsoft.VSTS.Common.Severity"],
3482
+ ["effort", "Microsoft.VSTS.Scheduling.Effort"],
3483
+ ["storypoints", "Microsoft.VSTS.Scheduling.StoryPoints"],
3484
+ ["story-points", "Microsoft.VSTS.Scheduling.StoryPoints"],
3485
+ ["remainingwork", "Microsoft.VSTS.Scheduling.RemainingWork"],
3486
+ ["remaining", "Microsoft.VSTS.Scheduling.RemainingWork"],
3487
+ ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
3488
+ ["reprosteps", "Microsoft.VSTS.TCM.ReproSteps"],
3489
+ ["foundinsource", "Microsoft.VSTS.Build.FoundIn"]
3490
+ ];
3491
+ const aliases = {};
3492
+ for (const [alias, ref] of candidates) {
3493
+ if (refNames.has(ref)) {
3494
+ aliases[alias] = ref;
3495
+ }
3496
+ }
3497
+ return aliases;
3498
+ }
3499
+
3500
+ // src/services/config/commands.ts
3501
+ import chalk from "chalk";
3502
+ import fs2 from "fs";
3847
3503
  function registerConfigCommands(program2) {
3848
3504
  const config = program2.command("config").description("Manage pncli configuration");
3849
3505
  config.command("init").description("Interactive setup wizard").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
@@ -3872,12 +3528,17 @@ function registerConfigCommands(program2) {
3872
3528
  fail(err, "config", "show", start);
3873
3529
  }
3874
3530
  });
3875
- config.command("set").description("Set a config value by dot-notation key (e.g. jira.baseUrl https://...)").argument("<key>", "Config key in dot notation").argument("<value>", "Value to set").action((key, value) => {
3531
+ config.command("set").description("Set a config value by dot-notation key (e.g. jira.baseUrl https://...)").argument("<key>", "Config key in dot notation").argument("<value>", "Value to set").option("--repo", "Write to repo config (.pncli.json) instead of global config").action((key, value, cmdOpts) => {
3876
3532
  const start = Date.now();
3877
3533
  try {
3878
- const opts = program2.optsWithGlobals();
3879
- setConfigValue(key, value, opts.config);
3880
- success({ key, value }, "config", "set", start);
3534
+ if (cmdOpts.repo) {
3535
+ setRepoConfigValue(key, value);
3536
+ success({ key, value, target: ".pncli.json" }, "config", "set", start);
3537
+ } else {
3538
+ const opts = program2.optsWithGlobals();
3539
+ setConfigValue(key, value, opts.config);
3540
+ success({ key, value }, "config", "set", start);
3541
+ }
3881
3542
  } catch (err) {
3882
3543
  fail(err, "config", "set", start);
3883
3544
  }
@@ -3939,32 +3600,186 @@ function registerConfigCommands(program2) {
3939
3600
  } else {
3940
3601
  results.sde = { ok: null, message: "not configured" };
3941
3602
  }
3603
+ if (cfg.ado.baseUrl) {
3604
+ try {
3605
+ const collection = cfg.defaults.ado?.collection;
3606
+ const path6 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1` : "/_apis/projectCollections?api-version=7.1";
3607
+ await http.ado(path6);
3608
+ results.ado = { ok: true, message: "connected" };
3609
+ } catch (err) {
3610
+ results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
3611
+ }
3612
+ } else {
3613
+ results.ado = { ok: null, message: "not configured" };
3614
+ }
3942
3615
  success(results, "config", "test", start);
3943
3616
  } catch (err) {
3944
3617
  fail(err, "config", "test", start);
3945
3618
  }
3946
3619
  });
3947
- }
3948
- async function initGlobalConfig(start) {
3949
- process.stderr.write("pncli config init \u2014 Global configuration\n\n");
3950
- process.stderr.write("\u2500\u2500 Identity \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");
3951
- const userEmail = await input({
3952
- message: "Your email address (used across Jira, Bitbucket, etc.):",
3953
- default: ""
3954
- });
3955
- const userId = await input({
3956
- message: "Your username / user ID:",
3957
- default: ""
3958
- });
3959
- process.stderr.write("\n\u2500\u2500 Jira \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\u2500\u2500\u2500\u2500\n");
3960
- const jiraBaseUrl = await input({
3961
- message: "Jira base URL (e.g. https://jira.your-company.com):",
3962
- default: ""
3963
- });
3964
- const jiraApiToken = await password({
3965
- message: "Jira personal access token:"
3966
- });
3967
- process.stderr.write("\n\u2500\u2500 Bitbucket \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");
3620
+ config.command("check").description("Check PAT status for every service: Blank, Valid, or Invalid").option("--output <format>", "Output format: json or table", "json").action(async (cmdOpts) => {
3621
+ const start = Date.now();
3622
+ try {
3623
+ let categorize2 = function(err) {
3624
+ const status = err.status ?? -1;
3625
+ if (status === 401 || status === 403) {
3626
+ return { status: "invalid", message: `auth rejected (${status})` };
3627
+ }
3628
+ if (status === 0) {
3629
+ return { status: "error", message: err instanceof Error ? err.message : String(err) };
3630
+ }
3631
+ return { status: "error", message: err instanceof Error ? err.message : String(err) };
3632
+ };
3633
+ var categorize = categorize2;
3634
+ const opts = program2.optsWithGlobals();
3635
+ const cfg = loadConfig({ configPath: opts.config });
3636
+ const http = createHttpClient(cfg, false);
3637
+ const results = {};
3638
+ if (!cfg.jira.apiToken) {
3639
+ results.jira = { status: "blank", message: "not configured" };
3640
+ } else if (!cfg.jira.baseUrl) {
3641
+ results.jira = { status: "error", message: "baseUrl not configured" };
3642
+ } else {
3643
+ try {
3644
+ await http.jira("/rest/api/2/myself", { timeoutMs: 1e4 });
3645
+ results.jira = { status: "valid", message: "ok" };
3646
+ } catch (err) {
3647
+ results.jira = categorize2(err);
3648
+ }
3649
+ }
3650
+ if (!cfg.bitbucket.pat) {
3651
+ results.bitbucket = { status: "blank", message: "not configured" };
3652
+ } else if (!cfg.bitbucket.baseUrl) {
3653
+ results.bitbucket = { status: "error", message: "baseUrl not configured" };
3654
+ } else {
3655
+ try {
3656
+ await http.bitbucket("/rest/api/1.0/application-properties", { timeoutMs: 1e4 });
3657
+ results.bitbucket = { status: "valid", message: "ok" };
3658
+ } catch (err) {
3659
+ results.bitbucket = categorize2(err);
3660
+ }
3661
+ }
3662
+ if (!cfg.confluence.apiTokenExplicit && !cfg.jira.apiToken) {
3663
+ results.confluence = { status: "blank", message: "not configured" };
3664
+ } else if (!cfg.confluence.apiToken) {
3665
+ results.confluence = { status: "blank", message: "not configured" };
3666
+ } else if (!cfg.confluence.baseUrl) {
3667
+ results.confluence = { status: "error", message: "baseUrl not configured" };
3668
+ } else {
3669
+ try {
3670
+ await http.confluence("/rest/api/space", { params: { limit: 1 }, timeoutMs: 1e4 });
3671
+ const msg = cfg.confluence.apiTokenExplicit ? "ok" : "ok (inherited from Jira token)";
3672
+ results.confluence = { status: "valid", message: msg };
3673
+ } catch (err) {
3674
+ results.confluence = categorize2(err);
3675
+ }
3676
+ }
3677
+ if (!cfg.sonar.token) {
3678
+ results.sonar = { status: "blank", message: "not configured" };
3679
+ } else if (!cfg.sonar.baseUrl) {
3680
+ results.sonar = { status: "error", message: "baseUrl not configured" };
3681
+ } else {
3682
+ try {
3683
+ await http.sonar("/api/system/status", { timeoutMs: 1e4 });
3684
+ results.sonar = { status: "valid", message: "ok" };
3685
+ } catch (err) {
3686
+ results.sonar = categorize2(err);
3687
+ }
3688
+ }
3689
+ if (!cfg.sde.token) {
3690
+ results.sde = { status: "blank", message: "not configured" };
3691
+ } else if (!cfg.sde.baseUrl) {
3692
+ results.sde = { status: "error", message: "baseUrl not configured" };
3693
+ } else {
3694
+ try {
3695
+ await http.sde("/api/v2/users/me/", { timeoutMs: 1e4 });
3696
+ results.sde = { status: "valid", message: "ok" };
3697
+ } catch (err) {
3698
+ results.sde = categorize2(err);
3699
+ }
3700
+ }
3701
+ if (!cfg.ado.pat) {
3702
+ results.ado = { status: "blank", message: "not configured" };
3703
+ } else if (!cfg.ado.baseUrl) {
3704
+ results.ado = { status: "error", message: "baseUrl not configured" };
3705
+ } else {
3706
+ try {
3707
+ const collection = cfg.defaults.ado?.collection;
3708
+ const path6 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1` : "/_apis/projectCollections?api-version=7.1";
3709
+ await http.ado(path6, { timeoutMs: 1e4 });
3710
+ results.ado = { status: "valid", message: "ok" };
3711
+ } catch (err) {
3712
+ results.ado = categorize2(err);
3713
+ }
3714
+ }
3715
+ const artResult = await checkArtifactoryConnectivity(cfg.artifactory);
3716
+ if (!artResult.configured) {
3717
+ results.artifactory = { status: "blank", message: artResult.error ?? "not configured" };
3718
+ } else if (!artResult.authenticated) {
3719
+ results.artifactory = { status: "invalid", message: artResult.error ?? "auth rejected" };
3720
+ } else if (!artResult.reachable) {
3721
+ results.artifactory = { status: "error", message: artResult.error ?? "unreachable" };
3722
+ } else {
3723
+ results.artifactory = { status: "valid", message: "ok" };
3724
+ }
3725
+ const statuses = Object.values(results).map((r) => r.status);
3726
+ if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
3727
+ else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
3728
+ if (cmdOpts.output === "table") {
3729
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "artifactory"];
3730
+ const labelWidth = 14;
3731
+ const statusWidth = 9;
3732
+ for (const svc of services) {
3733
+ const r = results[svc];
3734
+ if (!r) continue;
3735
+ const label = svc.padEnd(labelWidth);
3736
+ const coloredStatus = r.status === "valid" ? chalk.green("valid".padEnd(statusWidth)) : r.status === "blank" ? chalk.gray("blank".padEnd(statusWidth)) : r.status === "invalid" ? chalk.red("invalid".padEnd(statusWidth)) : chalk.yellow("error".padEnd(statusWidth));
3737
+ process.stdout.write(` ${label}${coloredStatus}${r.message}
3738
+ `);
3739
+ }
3740
+ process.stdout.write("\n");
3741
+ } else {
3742
+ if (opts.pretty) {
3743
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "artifactory"];
3744
+ const labelWidth = 14;
3745
+ const statusWidth = 9;
3746
+ for (const svc of services) {
3747
+ const r = results[svc];
3748
+ if (!r) continue;
3749
+ const label = svc.padEnd(labelWidth);
3750
+ const coloredStatus = r.status === "valid" ? chalk.green("valid".padEnd(statusWidth)) : r.status === "blank" ? chalk.gray("blank".padEnd(statusWidth)) : r.status === "invalid" ? chalk.red("invalid".padEnd(statusWidth)) : chalk.yellow("error".padEnd(statusWidth));
3751
+ process.stderr.write(` ${label}${coloredStatus}${r.message}
3752
+ `);
3753
+ }
3754
+ process.stderr.write("\n");
3755
+ }
3756
+ success(results, "config", "check", start);
3757
+ }
3758
+ } catch (err) {
3759
+ fail(err, "config", "check", start);
3760
+ }
3761
+ });
3762
+ }
3763
+ async function initGlobalConfig(start) {
3764
+ process.stderr.write("pncli config init \u2014 Global configuration\n\n");
3765
+ process.stderr.write("\u2500\u2500 Identity \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");
3766
+ const userEmail = await input({
3767
+ message: "Your email address (used across Jira, Bitbucket, etc.):",
3768
+ default: ""
3769
+ });
3770
+ const userId = await input({
3771
+ message: "Your username / user ID:",
3772
+ default: ""
3773
+ });
3774
+ process.stderr.write("\n\u2500\u2500 Jira \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\u2500\u2500\u2500\u2500\n");
3775
+ const jiraBaseUrl = await input({
3776
+ message: "Jira base URL (e.g. https://jira.your-company.com):",
3777
+ default: ""
3778
+ });
3779
+ const jiraApiToken = await password({
3780
+ message: "Jira personal access token:"
3781
+ });
3782
+ process.stderr.write("\n\u2500\u2500 Bitbucket \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");
3968
3783
  const bitbucketBaseUrl = await input({
3969
3784
  message: "Bitbucket Server base URL (e.g. https://bitbucket.your-company.com):",
3970
3785
  default: ""
@@ -4036,13 +3851,105 @@ async function initGlobalConfig(start) {
4036
3851
  let sdeBaseUrl = "";
4037
3852
  let sdeToken = "";
4038
3853
  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: ",
3854
+ process.stderr.write(" Connection string format: <api-token>@<base-url>\n");
3855
+ const useSdeConnectionString = await confirm({
3856
+ message: "Do you have a connection string to paste (token@url)?",
3857
+ default: false
3858
+ });
3859
+ if (useSdeConnectionString) {
3860
+ const sdeConnection = await password({
3861
+ message: "SDElements connection string (api-token@hostname, e.g. mytoken@myorg.sdelements.com):"
3862
+ });
3863
+ const atIdx = sdeConnection.indexOf("@");
3864
+ if (atIdx > 0) {
3865
+ sdeToken = sdeConnection.slice(0, atIdx);
3866
+ sdeBaseUrl = sdeConnection.slice(atIdx + 1);
3867
+ }
3868
+ } else {
3869
+ sdeBaseUrl = await input({
3870
+ message: "SDElements hostname\n Cloud-hosted: your-org.sdelements.com\n On-premise: sde.your-company.com\n Host: ",
3871
+ default: ""
3872
+ });
3873
+ sdeToken = await password({
3874
+ message: "SDElements API token:"
3875
+ });
3876
+ }
3877
+ }
3878
+ 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");
3879
+ const useAdo = await confirm({
3880
+ message: "Configure Azure DevOps Server for work items, repos, and pipelines?",
3881
+ default: false
3882
+ });
3883
+ let adoBaseUrl = "";
3884
+ let adoPat = "";
3885
+ let adoCollection = "";
3886
+ let adoProject = "";
3887
+ let adoFieldAliases = {};
3888
+ let adoDiscoveredFields = [];
3889
+ let adoDiscoveredTypes = [];
3890
+ if (useAdo) {
3891
+ adoBaseUrl = await input({
3892
+ message: "Azure DevOps Server base URL (e.g. https://tfs.company.com or https://devops.company.com/tfs):",
3893
+ default: ""
3894
+ });
3895
+ adoPat = await password({
3896
+ message: "Azure DevOps personal access token:"
3897
+ });
3898
+ adoCollection = await input({
3899
+ message: "Default collection name (e.g. DefaultCollection):",
4041
3900
  default: ""
4042
3901
  });
4043
- sdeToken = await password({
4044
- message: "SDElements API token:"
3902
+ adoProject = await input({
3903
+ message: "Default team project name:",
3904
+ default: ""
4045
3905
  });
3906
+ if (adoBaseUrl && adoCollection) {
3907
+ process.stderr.write("\n Verifying connection...\n");
3908
+ try {
3909
+ const tempConfig = {
3910
+ ...loadConfig(),
3911
+ ado: {
3912
+ baseUrl: adoBaseUrl,
3913
+ pat: adoPat || void 0,
3914
+ fieldAliases: {},
3915
+ discoveredFields: [],
3916
+ discoveredTypes: []
3917
+ }
3918
+ };
3919
+ const tempHttp = createHttpClient(tempConfig);
3920
+ const tempCore = new AdoCoreClient(tempHttp);
3921
+ await tempCore.getConnectionData(adoCollection);
3922
+ process.stderr.write(" Connected.\n");
3923
+ if (adoProject) {
3924
+ const doDiscover = await confirm({
3925
+ message: "Discover work item types and fields from this collection now? (recommended)",
3926
+ default: true
3927
+ });
3928
+ if (doDiscover) {
3929
+ process.stderr.write(" Fetching work item types and fields...\n");
3930
+ const tempWork = new AdoWorkClient(tempHttp);
3931
+ adoDiscoveredFields = await discoverFields(tempWork, adoCollection);
3932
+ process.stderr.write(` Found ${adoDiscoveredFields.length} fields.
3933
+ `);
3934
+ adoDiscoveredTypes = await discoverTypes(tempWork, adoCollection, adoProject);
3935
+ process.stderr.write(` Found ${adoDiscoveredTypes.length} work item types.
3936
+ `);
3937
+ const doAliases = await confirm({
3938
+ message: 'Save friendly aliases for common fields? (e.g. "priority" \u2192 reference name)',
3939
+ default: true
3940
+ });
3941
+ if (doAliases) {
3942
+ adoFieldAliases = buildDefaultAliases(adoDiscoveredFields);
3943
+ process.stderr.write(` Generated ${Object.keys(adoFieldAliases).length} aliases.
3944
+ `);
3945
+ }
3946
+ }
3947
+ }
3948
+ } catch (err) {
3949
+ warn(`Could not connect to Azure DevOps: ${err instanceof Error ? err.message : String(err)}`);
3950
+ warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
3951
+ }
3952
+ }
4046
3953
  }
4047
3954
  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
3955
  const jiraProject = await input({
@@ -4101,10 +4008,18 @@ async function initGlobalConfig(start) {
4101
4008
  token: sonarToken || void 0
4102
4009
  }
4103
4010
  } : {},
4104
- ...useSde ? {
4011
+ ...useSde && sdeToken && sdeBaseUrl ? {
4105
4012
  sde: {
4106
- baseUrl: sdeBaseUrl || void 0,
4107
- token: sdeToken || void 0
4013
+ connection: `${sdeToken}@${sdeBaseUrl}`
4014
+ }
4015
+ } : {},
4016
+ ...useAdo && adoBaseUrl ? {
4017
+ ado: {
4018
+ baseUrl: adoBaseUrl,
4019
+ ...adoPat ? { pat: adoPat } : {},
4020
+ ...Object.keys(adoFieldAliases).length ? { fieldAliases: adoFieldAliases } : {},
4021
+ ...adoDiscoveredFields.length ? { discoveredFields: adoDiscoveredFields } : {},
4022
+ ...adoDiscoveredTypes.length ? { discoveredTypes: adoDiscoveredTypes } : {}
4108
4023
  }
4109
4024
  } : {},
4110
4025
  defaults: {
@@ -4120,6 +4035,12 @@ async function initGlobalConfig(start) {
4120
4035
  sde: {
4121
4036
  project: sdeProject || void 0
4122
4037
  }
4038
+ } : {},
4039
+ ...useAdo && (adoCollection || adoProject) ? {
4040
+ ado: {
4041
+ collection: adoCollection || void 0,
4042
+ project: adoProject || void 0
4043
+ }
4123
4044
  } : {}
4124
4045
  }
4125
4046
  });
@@ -4145,6 +4066,18 @@ async function initRepoConfig(start) {
4145
4066
  message: "Default target branch for PRs:",
4146
4067
  default: "main"
4147
4068
  });
4069
+ const adoRepoCollection = await input({
4070
+ message: "Default Azure DevOps collection for this repo (leave blank to use global):",
4071
+ default: ""
4072
+ });
4073
+ const adoRepoProject = await input({
4074
+ message: "Default Azure DevOps project for this repo (leave blank to use global):",
4075
+ default: ""
4076
+ });
4077
+ const adoRepoRepo = await input({
4078
+ message: "Default Azure DevOps repo name for this repo (leave blank to auto-detect):",
4079
+ default: ""
4080
+ });
4148
4081
  const confirmed = await confirm({
4149
4082
  message: "Write config to .pncli.json in repo root?",
4150
4083
  default: true
@@ -4154,7 +4087,7 @@ async function initRepoConfig(start) {
4154
4087
  process.exitCode = ExitCode.SUCCESS;
4155
4088
  return;
4156
4089
  }
4157
- if (fs5.existsSync(".pncli.json")) {
4090
+ if (fs2.existsSync(".pncli.json")) {
4158
4091
  const overwrite = await confirm({
4159
4092
  message: ".pncli.json already exists. Overwrite?",
4160
4093
  default: false
@@ -4174,12 +4107,1111 @@ async function initRepoConfig(start) {
4174
4107
  },
4175
4108
  bitbucket: {
4176
4109
  targetBranch: targetBranch || void 0
4177
- }
4110
+ },
4111
+ ...adoRepoCollection || adoRepoProject || adoRepoRepo ? {
4112
+ ado: {
4113
+ collection: adoRepoCollection || void 0,
4114
+ project: adoRepoProject || void 0,
4115
+ repo: adoRepoRepo || void 0
4116
+ }
4117
+ } : {}
4178
4118
  }
4179
4119
  });
4180
4120
  success({ written: ".pncli.json" }, "config", "init", start);
4181
4121
  }
4182
4122
 
4123
+ // src/services/ado/client/git.ts
4124
+ var API7 = "7.1";
4125
+ var AdoGitClient = class {
4126
+ constructor(http) {
4127
+ this.http = http;
4128
+ }
4129
+ http;
4130
+ // ── Repositories ──────────────────────────────────────────────────
4131
+ async listRepos(collection, project) {
4132
+ const result = await this.http.ado(
4133
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories?api-version=${API7}`
4134
+ );
4135
+ return result.value ?? [];
4136
+ }
4137
+ async getRepo(collection, project, repoId) {
4138
+ return this.http.ado(
4139
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repoId)}?api-version=${API7}`
4140
+ );
4141
+ }
4142
+ // ── Pull Requests ─────────────────────────────────────────────────
4143
+ async listPRs(collection, project, repo, opts = {}) {
4144
+ return this.http.adoPaginate(async (token) => {
4145
+ const params = {
4146
+ "api-version": API7,
4147
+ "searchCriteria.status": opts.status ?? "active",
4148
+ ...opts.creatorAlias ? { "searchCriteria.creatorId": opts.creatorAlias } : {},
4149
+ ...opts.reviewerAlias ? { "searchCriteria.reviewerId": opts.reviewerAlias } : {},
4150
+ ...token ? { continuationToken: token } : {}
4151
+ };
4152
+ return this.http.adoRaw(
4153
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`,
4154
+ { params }
4155
+ );
4156
+ });
4157
+ }
4158
+ async getPR(collection, project, repo, prId) {
4159
+ return this.http.ado(
4160
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`
4161
+ );
4162
+ }
4163
+ async createPR(collection, project, repo, body) {
4164
+ return this.http.ado(
4165
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests?api-version=${API7}`,
4166
+ { method: "POST", body }
4167
+ );
4168
+ }
4169
+ async updatePR(collection, project, repo, prId, body) {
4170
+ return this.http.ado(
4171
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`,
4172
+ { method: "PATCH", body }
4173
+ );
4174
+ }
4175
+ async completePR(collection, project, repo, prId, lastMergeSourceCommit, completionOptions) {
4176
+ return this.http.ado(
4177
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`,
4178
+ {
4179
+ method: "PATCH",
4180
+ body: {
4181
+ status: "completed",
4182
+ lastMergeSourceCommit: { commitId: lastMergeSourceCommit },
4183
+ completionOptions
4184
+ }
4185
+ }
4186
+ );
4187
+ }
4188
+ async abandonPR(collection, project, repo, prId) {
4189
+ return this.http.ado(
4190
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}?api-version=${API7}`,
4191
+ { method: "PATCH", body: { status: "abandoned" } }
4192
+ );
4193
+ }
4194
+ // ── Reviewers / Votes ─────────────────────────────────────────────
4195
+ async setPRVote(collection, project, repo, prId, reviewerId, vote) {
4196
+ return this.http.ado(
4197
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/reviewers/${encodeURIComponent(reviewerId)}?api-version=${API7}`,
4198
+ { method: "PUT", body: { vote } }
4199
+ );
4200
+ }
4201
+ async listReviewers(collection, project, repo, prId) {
4202
+ const pr = await this.getPR(collection, project, repo, prId);
4203
+ return pr.reviewers;
4204
+ }
4205
+ // ── Threads / Comments ────────────────────────────────────────────
4206
+ async listThreads(collection, project, repo, prId) {
4207
+ const result = await this.http.ado(
4208
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads?api-version=${API7}`
4209
+ );
4210
+ return result.value ?? [];
4211
+ }
4212
+ async createThread(collection, project, repo, prId, body) {
4213
+ return this.http.ado(
4214
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads?api-version=${API7}`,
4215
+ { method: "POST", body }
4216
+ );
4217
+ }
4218
+ async addCommentToThread(collection, project, repo, prId, threadId, content) {
4219
+ return this.http.ado(
4220
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads/${threadId}/comments?api-version=${API7}`,
4221
+ { method: "POST", body: { content, commentType: "text" } }
4222
+ );
4223
+ }
4224
+ async updateThread(collection, project, repo, prId, threadId, status) {
4225
+ return this.http.ado(
4226
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads/${threadId}?api-version=${API7}`,
4227
+ { method: "PATCH", body: { status } }
4228
+ );
4229
+ }
4230
+ async deleteComment(collection, project, repo, prId, threadId, commentId) {
4231
+ await this.http.ado(
4232
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/threads/${threadId}/comments/${commentId}?api-version=${API7}`,
4233
+ { method: "DELETE" }
4234
+ );
4235
+ }
4236
+ // ── Diffs / Files ─────────────────────────────────────────────────
4237
+ async getPRDiff(collection, project, repo, prId) {
4238
+ const pr = await this.getPR(collection, project, repo, prId);
4239
+ const sourceCommit = pr.lastMergeSourceCommit?.commitId;
4240
+ const targetCommit = pr.lastMergeTargetCommit?.commitId;
4241
+ if (!sourceCommit || !targetCommit) return { changes: [], aheadCount: 0, behindCount: 0 };
4242
+ return this.http.ado(
4243
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/diffs/commits?baseVersion=${encodeURIComponent(targetCommit)}&baseVersionType=commit&targetVersion=${encodeURIComponent(sourceCommit)}&targetVersionType=commit&api-version=${API7}`
4244
+ );
4245
+ }
4246
+ async getCommitStatuses(collection, project, repo, commitId) {
4247
+ const result = await this.http.ado(
4248
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/commits/${encodeURIComponent(commitId)}/statuses?api-version=${API7}`
4249
+ );
4250
+ return result.value ?? [];
4251
+ }
4252
+ async listPRChanges(collection, project, repo, prId) {
4253
+ const iterations = await this.http.ado(
4254
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/iterations?api-version=${API7}`
4255
+ );
4256
+ const latestId = (iterations.value ?? []).at(-1)?.id;
4257
+ if (!latestId) return [];
4258
+ const changes = await this.http.ado(
4259
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}/iterations/${latestId}/changes?api-version=${API7}`
4260
+ );
4261
+ return changes.changeEntries ?? [];
4262
+ }
4263
+ };
4264
+
4265
+ // src/services/ado/client/build.ts
4266
+ var API8 = "7.1";
4267
+ var AdoBuildClient = class {
4268
+ constructor(http) {
4269
+ this.http = http;
4270
+ }
4271
+ http;
4272
+ // ── Definitions ───────────────────────────────────────────────────
4273
+ async listDefinitions(collection, project) {
4274
+ return this.http.adoPaginate(async (token) => {
4275
+ const params = {
4276
+ "api-version": API8,
4277
+ ...token ? { continuationToken: token } : {}
4278
+ };
4279
+ return this.http.adoRaw(
4280
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/definitions`,
4281
+ { params }
4282
+ );
4283
+ });
4284
+ }
4285
+ async getDefinition(collection, project, id) {
4286
+ return this.http.ado(
4287
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/definitions/${id}?api-version=${API8}`
4288
+ );
4289
+ }
4290
+ // ── Builds (runs) ─────────────────────────────────────────────────
4291
+ async queueBuild(collection, project, definitionId, opts = {}) {
4292
+ const body = {
4293
+ definition: { id: definitionId },
4294
+ ...opts.sourceBranch ? { sourceBranch: opts.sourceBranch } : {},
4295
+ ...opts.parameters ? { parameters: opts.parameters } : {}
4296
+ };
4297
+ return this.http.ado(
4298
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds?api-version=${API8}`,
4299
+ { method: "POST", body }
4300
+ );
4301
+ }
4302
+ async listBuilds(collection, project, opts = {}) {
4303
+ return this.http.adoPaginate(async (token) => {
4304
+ const params = {
4305
+ "api-version": API8,
4306
+ ...opts.definitionIds?.length ? { definitionIds: opts.definitionIds.join(",") } : {},
4307
+ ...opts.branchName ? { branchName: opts.branchName } : {},
4308
+ ...opts.statusFilter ? { statusFilter: opts.statusFilter } : {},
4309
+ ...opts.top ? { "$top": opts.top } : {},
4310
+ ...token ? { continuationToken: token } : {}
4311
+ };
4312
+ return this.http.adoRaw(
4313
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds`,
4314
+ { params }
4315
+ );
4316
+ });
4317
+ }
4318
+ async getBuild(collection, project, buildId) {
4319
+ return this.http.ado(
4320
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}?api-version=${API8}`
4321
+ );
4322
+ }
4323
+ async cancelBuild(collection, project, buildId) {
4324
+ return this.http.ado(
4325
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}?api-version=${API8}`,
4326
+ { method: "PATCH", body: { status: "cancelling" } }
4327
+ );
4328
+ }
4329
+ // ── Logs ──────────────────────────────────────────────────────────
4330
+ async listLogs(collection, project, buildId) {
4331
+ const result = await this.http.ado(
4332
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/logs?api-version=${API8}`
4333
+ );
4334
+ return result.value ?? [];
4335
+ }
4336
+ async getLog(collection, project, buildId, logId) {
4337
+ return this.http.adoText(
4338
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds/${buildId}/logs/${logId}?api-version=${API8}`
4339
+ );
4340
+ }
4341
+ };
4342
+
4343
+ // src/services/ado/helpers.ts
4344
+ function getAdoContext(program2, requireRepo = false) {
4345
+ const opts = program2.optsWithGlobals();
4346
+ const config = loadConfig({ configPath: opts.config });
4347
+ const http = createHttpClient(config, Boolean(opts.dryRun));
4348
+ const ctx = getGitContext(config);
4349
+ const collection = opts.collection ?? opts.organization ?? ctx?.ado?.collection ?? config.defaults.ado?.collection ?? "";
4350
+ const project = opts.project ?? ctx?.ado?.project ?? config.defaults.ado?.project ?? "";
4351
+ const repo = opts.repo ?? ctx?.ado?.repo ?? config.defaults.ado?.repo ?? "";
4352
+ if (!collection) {
4353
+ throw new PncliError(
4354
+ "Could not determine Azure DevOps collection. Pass --collection, or run pncli config init.",
4355
+ 1
4356
+ );
4357
+ }
4358
+ if (!project) {
4359
+ throw new PncliError(
4360
+ "Could not determine Azure DevOps project. Pass --project, or run pncli config init.",
4361
+ 1
4362
+ );
4363
+ }
4364
+ if (requireRepo && !repo) {
4365
+ throw new PncliError(
4366
+ "Could not determine Azure DevOps repo. Pass --repo, or run from inside a checked-out ADO repo.",
4367
+ 1
4368
+ );
4369
+ }
4370
+ return {
4371
+ collection,
4372
+ project,
4373
+ repo,
4374
+ coreClient: new AdoCoreClient(http),
4375
+ workClient: new AdoWorkClient(http),
4376
+ gitClient: new AdoGitClient(http),
4377
+ buildClient: new AdoBuildClient(http)
4378
+ };
4379
+ }
4380
+ function parseFieldArgs2(fields) {
4381
+ const result = {};
4382
+ for (const f of fields) {
4383
+ const eq = f.indexOf("=");
4384
+ if (eq <= 0) throw new PncliError(`Invalid --field value "${f}". Expected format: name=value`, 1);
4385
+ result[f.slice(0, eq)] = f.slice(eq + 1);
4386
+ }
4387
+ return result;
4388
+ }
4389
+ function buildFieldPatch(fields, fieldAliases) {
4390
+ return Object.entries(fields).map(([key, value]) => {
4391
+ const refName = fieldAliases[key.toLowerCase()] ?? fieldAliases[key] ?? key;
4392
+ return { op: "add", path: `/fields/${refName}`, value };
4393
+ });
4394
+ }
4395
+ function fieldArgsToPatch(fieldArgs, fieldAliases) {
4396
+ return buildFieldPatch(parseFieldArgs2(fieldArgs), fieldAliases);
4397
+ }
4398
+ var PR_VOTE = {
4399
+ APPROVE: 10,
4400
+ APPROVE_WITH_SUGGESTIONS: 5,
4401
+ NO_VOTE: 0,
4402
+ WAIT_FOR_AUTHOR: -5,
4403
+ REJECT: -10
4404
+ };
4405
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed"]);
4406
+ function isBuildTerminal(build) {
4407
+ return TERMINAL_STATUSES.has(build.status);
4408
+ }
4409
+ async function pollUntilTerminal(fetchStatus, isTerminal, opts) {
4410
+ const deadline = Date.now() + opts.timeoutSec * 1e3;
4411
+ let status = await fetchStatus();
4412
+ while (!isTerminal(status)) {
4413
+ if (Date.now() >= deadline) {
4414
+ throw new PncliError(
4415
+ `Timed out waiting for completion after ${opts.timeoutSec}s`,
4416
+ 1
4417
+ );
4418
+ }
4419
+ log(`Waiting ${opts.pollSec}s...`);
4420
+ await new Promise((resolve) => setTimeout(resolve, opts.pollSec * 1e3));
4421
+ status = await fetchStatus();
4422
+ }
4423
+ return status;
4424
+ }
4425
+
4426
+ // src/services/ado/commands/project.ts
4427
+ function registerAdoProjectCommands(ado) {
4428
+ const project = ado.command("project").description("Azure DevOps project and collection operations");
4429
+ project.command("list-collections").description("List all project collections on the server").action(async () => {
4430
+ const start = Date.now();
4431
+ try {
4432
+ const { coreClient } = getAdoContext(ado);
4433
+ const data = await coreClient.listCollections();
4434
+ success(data, "ado", "project-list-collections", start);
4435
+ } catch (err) {
4436
+ fail(err, "ado", "project-list-collections", start);
4437
+ }
4438
+ });
4439
+ project.command("list").description("List team projects in a collection").action(async () => {
4440
+ const start = Date.now();
4441
+ try {
4442
+ const { collection, coreClient } = getAdoContext(ado);
4443
+ const data = await coreClient.listProjects(collection);
4444
+ success(data, "ado", "project-list", start);
4445
+ } catch (err) {
4446
+ fail(err, "ado", "project-list", start);
4447
+ }
4448
+ });
4449
+ project.command("get").description("Get a team project by name").requiredOption("--name <project>", "Project name").action(async (opts) => {
4450
+ const start = Date.now();
4451
+ try {
4452
+ const { collection, coreClient } = getAdoContext(ado);
4453
+ const data = await coreClient.getProject(collection, opts.name);
4454
+ success(data, "ado", "project-get", start);
4455
+ } catch (err) {
4456
+ fail(err, "ado", "project-get", start);
4457
+ }
4458
+ });
4459
+ }
4460
+
4461
+ // src/services/ado/commands/work.ts
4462
+ function registerAdoWorkCommands(ado) {
4463
+ const work = ado.command("work").description("Azure DevOps work item operations");
4464
+ work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
4465
+ const start = Date.now();
4466
+ try {
4467
+ const { collection, workClient } = getAdoContext(ado);
4468
+ const data = await workClient.getWorkItem(collection, parseInt(opts.id, 10));
4469
+ success(data, "ado", "work-get", start);
4470
+ } catch (err) {
4471
+ fail(err, "ado", "work-get", start);
4472
+ }
4473
+ });
4474
+ 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) => {
4475
+ acc.push(v);
4476
+ return acc;
4477
+ }, []).action(async (opts) => {
4478
+ const start = Date.now();
4479
+ try {
4480
+ const globalOpts = ado.optsWithGlobals();
4481
+ const config = loadConfig({ configPath: globalOpts.config });
4482
+ const { collection, project, workClient } = getAdoContext(ado);
4483
+ const builtIn = {
4484
+ "System.Title": opts.title,
4485
+ ...opts.description ? { "System.Description": opts.description } : {},
4486
+ ...opts.assignee ? { "System.AssignedTo": opts.assignee } : {},
4487
+ ...opts.priority ? { "Microsoft.VSTS.Common.Priority": opts.priority } : {}
4488
+ };
4489
+ const extra = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
4490
+ const patch = [
4491
+ ...Object.entries(builtIn).map(([k, v]) => ({ op: "add", path: `/fields/${k}`, value: v })),
4492
+ ...extra
4493
+ ];
4494
+ const data = await workClient.createWorkItem(collection, project, opts.type, patch);
4495
+ success(data, "ado", "work-create", start);
4496
+ } catch (err) {
4497
+ fail(err, "ado", "work-create", start);
4498
+ }
4499
+ });
4500
+ 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) => {
4501
+ acc.push(v);
4502
+ return acc;
4503
+ }, []).action(async (opts) => {
4504
+ const start = Date.now();
4505
+ try {
4506
+ const globalOpts = ado.optsWithGlobals();
4507
+ const config = loadConfig({ configPath: globalOpts.config });
4508
+ const { collection, workClient } = getAdoContext(ado);
4509
+ const patch = fieldArgsToPatch(opts.field, config.ado.fieldAliases);
4510
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4511
+ success(data, "ado", "work-update", start);
4512
+ } catch (err) {
4513
+ fail(err, "ado", "work-update", start);
4514
+ }
4515
+ });
4516
+ 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) => {
4517
+ const start = Date.now();
4518
+ try {
4519
+ const { collection, workClient } = getAdoContext(ado);
4520
+ const patch = [{ op: "add", path: "/fields/System.State", value: opts.state }];
4521
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4522
+ success(data, "ado", "work-transition", start);
4523
+ } catch (err) {
4524
+ fail(err, "ado", "work-transition", start);
4525
+ }
4526
+ });
4527
+ 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) => {
4528
+ const start = Date.now();
4529
+ try {
4530
+ const { collection, workClient } = getAdoContext(ado);
4531
+ const patch = [{ op: "add", path: "/fields/System.AssignedTo", value: opts.to }];
4532
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4533
+ success(data, "ado", "work-assign", start);
4534
+ } catch (err) {
4535
+ fail(err, "ado", "work-assign", start);
4536
+ }
4537
+ });
4538
+ 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) => {
4539
+ const start = Date.now();
4540
+ try {
4541
+ const globalOpts = ado.optsWithGlobals();
4542
+ const config = loadConfig({ configPath: globalOpts.config });
4543
+ const { collection, workClient } = getAdoContext(ado);
4544
+ const relMap = {
4545
+ related: "System.LinkTypes.Related",
4546
+ parent: "System.LinkTypes.Hierarchy-Reverse",
4547
+ child: "System.LinkTypes.Hierarchy-Forward",
4548
+ duplicate: "System.LinkTypes.Duplicate-Forward",
4549
+ "duplicate-of": "System.LinkTypes.Duplicate-Reverse"
4550
+ };
4551
+ const rel = relMap[opts.type] ?? opts.type;
4552
+ const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
4553
+ const targetUrl = `${baseUrl}/${encodeURIComponent(collection)}/_apis/wit/workitems/${opts.to}`;
4554
+ const patch = [{ op: "add", path: "/relations/-", value: { rel, url: targetUrl } }];
4555
+ const data = await workClient.updateWorkItem(collection, parseInt(opts.id, 10), patch);
4556
+ success(data, "ado", "work-link", start);
4557
+ } catch (err) {
4558
+ fail(err, "ado", "work-link", start);
4559
+ }
4560
+ });
4561
+ work.command("search").description("Run a WIQL query").requiredOption("--wiql <query>", "WIQL query string").action(async (opts) => {
4562
+ const start = Date.now();
4563
+ try {
4564
+ const { collection, project, workClient } = getAdoContext(ado);
4565
+ const data = await workClient.queryWiql(collection, project, opts.wiql);
4566
+ success(data, "ado", "work-search", start);
4567
+ } catch (err) {
4568
+ fail(err, "ado", "work-search", start);
4569
+ }
4570
+ });
4571
+ work.command("list-comments").description("List comments on a work item").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
4572
+ const start = Date.now();
4573
+ try {
4574
+ const { collection, project, workClient } = getAdoContext(ado);
4575
+ const data = await workClient.listComments(collection, project, parseInt(opts.id, 10));
4576
+ success(data, "ado", "work-list-comments", start);
4577
+ } catch (err) {
4578
+ fail(err, "ado", "work-list-comments", start);
4579
+ }
4580
+ });
4581
+ 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) => {
4582
+ const start = Date.now();
4583
+ try {
4584
+ const { collection, project, workClient } = getAdoContext(ado);
4585
+ const data = await workClient.addComment(collection, project, parseInt(opts.id, 10), opts.body);
4586
+ success(data, "ado", "work-add-comment", start);
4587
+ } catch (err) {
4588
+ fail(err, "ado", "work-add-comment", start);
4589
+ }
4590
+ });
4591
+ 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) => {
4592
+ const start = Date.now();
4593
+ try {
4594
+ const { collection, project, workClient } = getAdoContext(ado);
4595
+ const globalOpts = ado.optsWithGlobals();
4596
+ const types = await discoverTypes(workClient, collection, project);
4597
+ if (opts.save) {
4598
+ const globalConfigPath = getGlobalConfigPath(globalOpts.config);
4599
+ const fs4 = await import("fs");
4600
+ const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
4601
+ existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
4602
+ fs4.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4603
+ warn(`Saved ${types.length} types to ${globalConfigPath}`);
4604
+ }
4605
+ success(types, "ado", "work-types", start);
4606
+ } catch (err) {
4607
+ fail(err, "ado", "work-types", start);
4608
+ }
4609
+ });
4610
+ 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) => {
4611
+ const start = Date.now();
4612
+ try {
4613
+ const { collection, project, workClient } = getAdoContext(ado);
4614
+ const data = await workClient.listTypeStates(collection, project, opts.type);
4615
+ success(data, "ado", "work-list-states", start);
4616
+ } catch (err) {
4617
+ fail(err, "ado", "work-list-states", start);
4618
+ }
4619
+ });
4620
+ 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) => {
4621
+ const start = Date.now();
4622
+ try {
4623
+ const { collection, project, workClient } = getAdoContext(ado);
4624
+ const globalOpts = ado.optsWithGlobals();
4625
+ const allFields = opts.type ? await discoverTypeFields(workClient, collection, project, opts.type) : await discoverFields(workClient, collection);
4626
+ const fields = opts.customOnly ? allFields.filter(
4627
+ (f) => !f.referenceName.startsWith("System.") && !f.referenceName.startsWith("Microsoft.VSTS.")
4628
+ ) : allFields;
4629
+ if (opts.save) {
4630
+ const globalConfigPath = getGlobalConfigPath(globalOpts.config);
4631
+ const fs4 = await import("fs");
4632
+ const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
4633
+ const aliases = buildDefaultAliases(allFields);
4634
+ existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
4635
+ fs4.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4636
+ warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
4637
+ }
4638
+ success(fields, "ado", "work-fields", start);
4639
+ } catch (err) {
4640
+ fail(err, "ado", "work-fields", start);
4641
+ }
4642
+ });
4643
+ }
4644
+
4645
+ // src/services/ado/commands/repo.ts
4646
+ function flattenThreadComments(threads, filterInlineOnly, filterGeneralOnly) {
4647
+ const flat = [];
4648
+ for (const thread of threads) {
4649
+ if (thread.isDeleted) continue;
4650
+ if (filterInlineOnly && !thread.threadContext) continue;
4651
+ if (filterGeneralOnly && thread.threadContext) continue;
4652
+ for (const comment of thread.comments) {
4653
+ if (comment.isDeleted) continue;
4654
+ flat.push({ ...comment, threadId: thread.id, threadContext: thread.threadContext ?? null });
4655
+ }
4656
+ }
4657
+ return flat.sort((a, b) => a.publishedDate.localeCompare(b.publishedDate));
4658
+ }
4659
+ function registerAdoRepoCommands(ado) {
4660
+ const repo = ado.command("repo").description("Azure DevOps repository and pull request operations");
4661
+ repo.command("list").description("List git repositories in the project").action(async () => {
4662
+ const start = Date.now();
4663
+ try {
4664
+ const { collection, project, gitClient } = getAdoContext(ado);
4665
+ const data = await gitClient.listRepos(collection, project);
4666
+ success(data, "ado", "repo-list", start);
4667
+ } catch (err) {
4668
+ fail(err, "ado", "repo-list", start);
4669
+ }
4670
+ });
4671
+ repo.command("get").description("Get a specific repository").action(async () => {
4672
+ const start = Date.now();
4673
+ try {
4674
+ const { collection, project, repo: repoName, gitClient } = getAdoContext(ado, true);
4675
+ const data = await gitClient.getRepo(collection, project, repoName);
4676
+ success(data, "ado", "repo-get", start);
4677
+ } catch (err) {
4678
+ fail(err, "ado", "repo-get", start);
4679
+ }
4680
+ });
4681
+ 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) => {
4682
+ const start = Date.now();
4683
+ try {
4684
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4685
+ const data = await gitClient.listPRs(collection, project, repo2, {
4686
+ status: opts.state,
4687
+ creatorAlias: opts.creator,
4688
+ reviewerAlias: opts.reviewer
4689
+ });
4690
+ success(data, "ado", "repo-list-prs", start);
4691
+ } catch (err) {
4692
+ fail(err, "ado", "repo-list-prs", start);
4693
+ }
4694
+ });
4695
+ repo.command("get-pr").description("Get a pull request by ID").requiredOption("--id <n>", "Pull request ID").action(async (opts) => {
4696
+ const start = Date.now();
4697
+ try {
4698
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4699
+ const data = await gitClient.getPR(collection, project, repo2, parseInt(opts.id, 10));
4700
+ success(data, "ado", "repo-get-pr", start);
4701
+ } catch (err) {
4702
+ fail(err, "ado", "repo-get-pr", start);
4703
+ }
4704
+ });
4705
+ 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) => {
4706
+ const start = Date.now();
4707
+ try {
4708
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4709
+ const reviewers = opts.reviewers ? opts.reviewers.split(",").map((id) => ({ id: id.trim() })) : [];
4710
+ const data = await gitClient.createPR(collection, project, repo2, {
4711
+ title: opts.title,
4712
+ description: opts.description,
4713
+ sourceRefName: `refs/heads/${opts.source}`,
4714
+ targetRefName: `refs/heads/${opts.target}`,
4715
+ reviewers
4716
+ });
4717
+ success(data, "ado", "repo-create-pr", start);
4718
+ } catch (err) {
4719
+ fail(err, "ado", "repo-create-pr", start);
4720
+ }
4721
+ });
4722
+ 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) => {
4723
+ const start = Date.now();
4724
+ try {
4725
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4726
+ const reviewers = opts.reviewers ? opts.reviewers.split(",").map((id) => ({ id: id.trim() })) : void 0;
4727
+ const body = {};
4728
+ if (opts.title) body.title = opts.title;
4729
+ if (opts.description !== void 0) body.description = opts.description;
4730
+ if (reviewers) body.reviewers = reviewers;
4731
+ const data = await gitClient.updatePR(collection, project, repo2, parseInt(opts.id, 10), body);
4732
+ success(data, "ado", "repo-update-pr", start);
4733
+ } catch (err) {
4734
+ fail(err, "ado", "repo-update-pr", start);
4735
+ }
4736
+ });
4737
+ 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) => {
4738
+ const start = Date.now();
4739
+ try {
4740
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4741
+ const prId = parseInt(opts.id, 10);
4742
+ const pr = await gitClient.getPR(collection, project, repo2, prId);
4743
+ const data = await gitClient.completePR(
4744
+ collection,
4745
+ project,
4746
+ repo2,
4747
+ prId,
4748
+ pr.lastMergeSourceCommit?.commitId ?? "",
4749
+ {
4750
+ mergeStrategy: opts.strategy,
4751
+ deleteSourceBranch: opts.deleteSource ?? false
4752
+ }
4753
+ );
4754
+ success(data, "ado", "repo-merge-pr", start);
4755
+ } catch (err) {
4756
+ fail(err, "ado", "repo-merge-pr", start);
4757
+ }
4758
+ });
4759
+ repo.command("abandon-pr").description("Abandon a pull request").requiredOption("--id <n>", "Pull request ID").action(async (opts) => {
4760
+ const start = Date.now();
4761
+ try {
4762
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4763
+ const data = await gitClient.abandonPR(collection, project, repo2, parseInt(opts.id, 10));
4764
+ success(data, "ado", "repo-abandon-pr", start);
4765
+ } catch (err) {
4766
+ fail(err, "ado", "repo-abandon-pr", start);
4767
+ }
4768
+ });
4769
+ 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) => {
4770
+ const start = Date.now();
4771
+ try {
4772
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4773
+ const threads = await gitClient.listThreads(collection, project, repo2, parseInt(opts.pr, 10));
4774
+ const flat = flattenThreadComments(threads, opts.inlineOnly ?? false, opts.generalOnly ?? false);
4775
+ success(flat, "ado", "repo-list-comments", start);
4776
+ } catch (err) {
4777
+ fail(err, "ado", "repo-list-comments", start);
4778
+ }
4779
+ });
4780
+ 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) => {
4781
+ const start = Date.now();
4782
+ try {
4783
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4784
+ const data = await gitClient.createThread(collection, project, repo2, parseInt(opts.pr, 10), {
4785
+ comments: [{ content: opts.body, commentType: "text" }],
4786
+ status: "active"
4787
+ });
4788
+ success(data, "ado", "repo-add-comment", start);
4789
+ } catch (err) {
4790
+ fail(err, "ado", "repo-add-comment", start);
4791
+ }
4792
+ });
4793
+ 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) => {
4794
+ const start = Date.now();
4795
+ try {
4796
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4797
+ const lineNum = parseInt(opts.line, 10);
4798
+ const isRight = opts.lineType !== "left";
4799
+ const lineRange = isRight ? { rightFileStart: { line: lineNum, offset: 1 }, rightFileEnd: { line: lineNum, offset: 1 } } : { leftFileStart: { line: lineNum, offset: 1 }, leftFileEnd: { line: lineNum, offset: 1 } };
4800
+ const data = await gitClient.createThread(collection, project, repo2, parseInt(opts.pr, 10), {
4801
+ comments: [{ content: opts.body, commentType: "text" }],
4802
+ status: "active",
4803
+ threadContext: { filePath: opts.file, ...lineRange }
4804
+ });
4805
+ success(data, "ado", "repo-add-inline-comment", start);
4806
+ } catch (err) {
4807
+ fail(err, "ado", "repo-add-inline-comment", start);
4808
+ }
4809
+ });
4810
+ 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) => {
4811
+ const start = Date.now();
4812
+ try {
4813
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4814
+ const data = await gitClient.addCommentToThread(
4815
+ collection,
4816
+ project,
4817
+ repo2,
4818
+ parseInt(opts.pr, 10),
4819
+ parseInt(opts.threadId, 10),
4820
+ opts.body
4821
+ );
4822
+ success(data, "ado", "repo-reply-comment", start);
4823
+ } catch (err) {
4824
+ fail(err, "ado", "repo-reply-comment", start);
4825
+ }
4826
+ });
4827
+ 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) => {
4828
+ const start = Date.now();
4829
+ try {
4830
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4831
+ const data = await gitClient.updateThread(
4832
+ collection,
4833
+ project,
4834
+ repo2,
4835
+ parseInt(opts.pr, 10),
4836
+ parseInt(opts.threadId, 10),
4837
+ "fixed"
4838
+ );
4839
+ success(data, "ado", "repo-resolve-comment", start);
4840
+ } catch (err) {
4841
+ fail(err, "ado", "repo-resolve-comment", start);
4842
+ }
4843
+ });
4844
+ 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) => {
4845
+ const start = Date.now();
4846
+ try {
4847
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4848
+ await gitClient.deleteComment(
4849
+ collection,
4850
+ project,
4851
+ repo2,
4852
+ parseInt(opts.pr, 10),
4853
+ parseInt(opts.threadId, 10),
4854
+ parseInt(opts.commentId, 10)
4855
+ );
4856
+ success({ deleted: true }, "ado", "repo-delete-comment", start);
4857
+ } catch (err) {
4858
+ fail(err, "ado", "repo-delete-comment", start);
4859
+ }
4860
+ });
4861
+ repo.command("list-files").description("List files changed in a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4862
+ const start = Date.now();
4863
+ try {
4864
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4865
+ const data = await gitClient.listPRChanges(collection, project, repo2, parseInt(opts.pr, 10));
4866
+ success(data, "ado", "repo-list-files", start);
4867
+ } catch (err) {
4868
+ fail(err, "ado", "repo-list-files", start);
4869
+ }
4870
+ });
4871
+ repo.command("diff").description("Show files changed in a pull request with change types and commit metadata").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4872
+ const start = Date.now();
4873
+ try {
4874
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4875
+ const data = await gitClient.getPRDiff(collection, project, repo2, parseInt(opts.pr, 10));
4876
+ success(data, "ado", "repo-diff", start);
4877
+ } catch (err) {
4878
+ fail(err, "ado", "repo-diff", start);
4879
+ }
4880
+ });
4881
+ repo.command("get-build-status").description("Get CI/build statuses posted to a commit").requiredOption("--commit <sha>", "Commit SHA").action(async (opts) => {
4882
+ const start = Date.now();
4883
+ try {
4884
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4885
+ const data = await gitClient.getCommitStatuses(collection, project, repo2, opts.commit);
4886
+ success(data, "ado", "repo-get-build-status", start);
4887
+ } catch (err) {
4888
+ fail(err, "ado", "repo-get-build-status", start);
4889
+ }
4890
+ });
4891
+ repo.command("list-reviewers").description("List reviewers on a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4892
+ const start = Date.now();
4893
+ try {
4894
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4895
+ const data = await gitClient.listReviewers(collection, project, repo2, parseInt(opts.pr, 10));
4896
+ success(data, "ado", "repo-list-reviewers", start);
4897
+ } catch (err) {
4898
+ fail(err, "ado", "repo-list-reviewers", start);
4899
+ }
4900
+ });
4901
+ const voteCmd = (name, description, vote, action) => {
4902
+ repo.command(name).description(description).requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4903
+ const start = Date.now();
4904
+ try {
4905
+ const globalOpts = ado.optsWithGlobals();
4906
+ const { loadConfig: lc } = await import("./config-IJ57IIQA.js");
4907
+ const config = lc({ configPath: globalOpts.config });
4908
+ const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
4909
+ const { AdoCoreClient: AdoCoreClient2 } = await import("./core-AJYPJ2Z7.js");
4910
+ const { createHttpClient: createHttpClient2 } = await import("./http-FC24JSK7.js");
4911
+ const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
4912
+ const coreClient = new AdoCoreClient2(http);
4913
+ const me = await coreClient.getConnectionData(collection);
4914
+ const myId = me.authenticatedUser.id;
4915
+ const data = await gitClient.setPRVote(collection, project, repo2, parseInt(opts.pr, 10), myId, vote);
4916
+ success(data, "ado", action, start);
4917
+ } catch (err) {
4918
+ fail(err, "ado", action, start);
4919
+ }
4920
+ });
4921
+ };
4922
+ voteCmd("approve", "Approve a pull request", PR_VOTE.APPROVE, "repo-approve");
4923
+ voteCmd("unapprove", "Remove approval from a pull request", PR_VOTE.NO_VOTE, "repo-unapprove");
4924
+ voteCmd("wait-for-author", "Mark a pull request as waiting for author", PR_VOTE.WAIT_FOR_AUTHOR, "repo-wait-for-author");
4925
+ repo.command("list-builds").description("List builds associated with a pull request").requiredOption("--pr <n>", "Pull request ID").action(async (opts) => {
4926
+ const start = Date.now();
4927
+ try {
4928
+ const { collection, project, repo: repo2, gitClient, buildClient } = getAdoContext(ado, true);
4929
+ const pr = await gitClient.getPR(collection, project, repo2, parseInt(opts.pr, 10));
4930
+ const data = await buildClient.listBuilds(collection, project, {
4931
+ branchName: pr.sourceRefName
4932
+ });
4933
+ success(data, "ado", "repo-list-builds", start);
4934
+ } catch (err) {
4935
+ fail(err, "ado", "repo-list-builds", start);
4936
+ }
4937
+ });
4938
+ }
4939
+
4940
+ // src/services/ado/commands/pipeline.ts
4941
+ function registerAdoPipelineCommands(ado) {
4942
+ const pipeline = ado.command("pipeline").description("Azure DevOps pipeline (build) operations");
4943
+ pipeline.command("list").description("List pipeline definitions").action(async () => {
4944
+ const start = Date.now();
4945
+ try {
4946
+ const { collection, project, buildClient } = getAdoContext(ado);
4947
+ const data = await buildClient.listDefinitions(collection, project);
4948
+ success(data, "ado", "pipeline-list", start);
4949
+ } catch (err) {
4950
+ fail(err, "ado", "pipeline-list", start);
4951
+ }
4952
+ });
4953
+ pipeline.command("get").description("Get a pipeline definition by ID").requiredOption("--id <n>", "Pipeline definition ID").action(async (opts) => {
4954
+ const start = Date.now();
4955
+ try {
4956
+ const { collection, project, buildClient } = getAdoContext(ado);
4957
+ const data = await buildClient.getDefinition(collection, project, parseInt(opts.id, 10));
4958
+ success(data, "ado", "pipeline-get", start);
4959
+ } catch (err) {
4960
+ fail(err, "ado", "pipeline-get", start);
4961
+ }
4962
+ });
4963
+ 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) => {
4964
+ acc.push(v);
4965
+ return acc;
4966
+ }, []).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) => {
4967
+ const start = Date.now();
4968
+ try {
4969
+ const { collection, project, buildClient } = getAdoContext(ado);
4970
+ const sourceBranch = opts.branch ? opts.branch.startsWith("refs/") ? opts.branch : `refs/heads/${opts.branch}` : void 0;
4971
+ for (const p of opts.parameter) {
4972
+ if (!p.includes("=")) throw new PncliError(`Invalid --parameter "${p}". Expected format: key=value`, 1);
4973
+ }
4974
+ const parameters = opts.parameter.length > 0 ? JSON.stringify(Object.fromEntries(opts.parameter.map((p) => {
4975
+ const eq = p.indexOf("=");
4976
+ return [p.slice(0, eq), p.slice(eq + 1)];
4977
+ }))) : void 0;
4978
+ let build = await buildClient.queueBuild(collection, project, parseInt(opts.id, 10), {
4979
+ sourceBranch,
4980
+ parameters
4981
+ });
4982
+ log(`Queued build #${build.id} (${build.buildNumber}) \u2014 status: ${build.status}`);
4983
+ if (opts.wait) {
4984
+ const timeoutSec = parseInt(opts.timeout, 10);
4985
+ const pollSec = parseInt(opts.poll, 10);
4986
+ if (isNaN(timeoutSec) || timeoutSec <= 0) throw new PncliError(`Invalid --timeout "${opts.timeout}". Expected a positive number of seconds.`, 1);
4987
+ if (isNaN(pollSec) || pollSec <= 0) throw new PncliError(`Invalid --poll "${opts.poll}". Expected a positive number of seconds.`, 1);
4988
+ log(`Waiting for build #${build.id} to complete (timeout ${timeoutSec}s, poll ${pollSec}s)...`);
4989
+ build = await pollUntilTerminal(
4990
+ () => buildClient.getBuild(collection, project, build.id),
4991
+ isBuildTerminal,
4992
+ { timeoutSec, pollSec }
4993
+ );
4994
+ if (build.result && build.result !== "succeeded") {
4995
+ process.exitCode = ExitCode.GENERAL_ERROR;
4996
+ }
4997
+ }
4998
+ success(build, "ado", "pipeline-run", start);
4999
+ } catch (err) {
5000
+ fail(err, "ado", "pipeline-run", start);
5001
+ }
5002
+ });
5003
+ 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) => {
5004
+ const start = Date.now();
5005
+ try {
5006
+ const { collection, project, buildClient } = getAdoContext(ado);
5007
+ const data = await buildClient.listBuilds(collection, project, {
5008
+ definitionIds: opts.definition ? [parseInt(opts.definition, 10)] : void 0,
5009
+ branchName: opts.branch,
5010
+ statusFilter: opts.status,
5011
+ top: parseInt(opts.top, 10)
5012
+ });
5013
+ success(data, "ado", "pipeline-list-runs", start);
5014
+ } catch (err) {
5015
+ fail(err, "ado", "pipeline-list-runs", start);
5016
+ }
5017
+ });
5018
+ pipeline.command("get-run").description("Get a pipeline run by build ID").requiredOption("--id <n>", "Build ID").action(async (opts) => {
5019
+ const start = Date.now();
5020
+ try {
5021
+ const { collection, project, buildClient } = getAdoContext(ado);
5022
+ const data = await buildClient.getBuild(collection, project, parseInt(opts.id, 10));
5023
+ success(data, "ado", "pipeline-get-run", start);
5024
+ } catch (err) {
5025
+ fail(err, "ado", "pipeline-get-run", start);
5026
+ }
5027
+ });
5028
+ pipeline.command("cancel-run").description("Cancel a running pipeline build").requiredOption("--id <n>", "Build ID").action(async (opts) => {
5029
+ const start = Date.now();
5030
+ try {
5031
+ const { collection, project, buildClient } = getAdoContext(ado);
5032
+ const data = await buildClient.cancelBuild(collection, project, parseInt(opts.id, 10));
5033
+ success(data, "ado", "pipeline-cancel-run", start);
5034
+ } catch (err) {
5035
+ fail(err, "ado", "pipeline-cancel-run", start);
5036
+ }
5037
+ });
5038
+ 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) => {
5039
+ const start = Date.now();
5040
+ try {
5041
+ const { collection, project, buildClient } = getAdoContext(ado);
5042
+ const buildId = parseInt(opts.id, 10);
5043
+ if (opts.logId) {
5044
+ const data = await buildClient.getLog(collection, project, buildId, parseInt(opts.logId, 10));
5045
+ success({ log: data }, "ado", "pipeline-logs", start);
5046
+ } else {
5047
+ const data = await buildClient.listLogs(collection, project, buildId);
5048
+ success(data, "ado", "pipeline-logs", start);
5049
+ }
5050
+ } catch (err) {
5051
+ fail(err, "ado", "pipeline-logs", start);
5052
+ }
5053
+ });
5054
+ }
5055
+
5056
+ // src/services/ado/commands/index.ts
5057
+ function registerAdoCommands(program2) {
5058
+ 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");
5059
+ ado.command("whoami").description("Show current authenticated user").action(async () => {
5060
+ const start = Date.now();
5061
+ try {
5062
+ const { collection, coreClient } = getAdoContext(ado);
5063
+ const data = await coreClient.getConnectionData(collection);
5064
+ success(data, "ado", "whoami", start);
5065
+ } catch (err) {
5066
+ fail(err, "ado", "whoami", start);
5067
+ }
5068
+ });
5069
+ ado.command("connection-data").description("Show raw connection data from the ADO server").action(async () => {
5070
+ const start = Date.now();
5071
+ try {
5072
+ const { collection, coreClient } = getAdoContext(ado);
5073
+ const data = await coreClient.getConnectionData(collection);
5074
+ success(data, "ado", "connection-data", start);
5075
+ } catch (err) {
5076
+ fail(err, "ado", "connection-data", start);
5077
+ }
5078
+ });
5079
+ registerAdoProjectCommands(ado);
5080
+ registerAdoWorkCommands(ado);
5081
+ registerAdoRepoCommands(ado);
5082
+ registerAdoPipelineCommands(ado);
5083
+ }
5084
+
5085
+ // src/services/skills/commands.ts
5086
+ import fs3 from "fs";
5087
+ import path5 from "path";
5088
+ var GITHUB_API = "https://api.github.com/repos/kolatts/pncli/contents/.claude/skills";
5089
+ var RAW_BASE = "https://raw.githubusercontent.com/kolatts/pncli/main/.claude/skills";
5090
+ var RAW_COPILOT_INSTRUCTIONS = "https://raw.githubusercontent.com/kolatts/pncli/main/copilot-instructions.md";
5091
+ function registerSkillsCommands(program2) {
5092
+ const skills = program2.command("skills").description("Manage pncli Claude Code skills");
5093
+ skills.command("install").description("Download latest pncli skills and copilot-instructions.md into the current repo").option("--target <dir>", "Target directory for skills", ".claude/skills").option("--skip-instructions", "Skip downloading copilot-instructions.md (always written to repo root)").action(async (opts) => {
5094
+ const start = Date.now();
5095
+ try {
5096
+ const targetDir = path5.resolve(opts.target);
5097
+ warn("Fetching skill list from GitHub...");
5098
+ const listRes = await fetch(GITHUB_API, {
5099
+ headers: { "Accept": "application/vnd.github.v3+json", "User-Agent": "pncli" }
5100
+ });
5101
+ if (!listRes.ok) {
5102
+ throw new Error(`GitHub API returned ${listRes.status}: ${await listRes.text()}`);
5103
+ }
5104
+ const entries = await listRes.json();
5105
+ const skillDirs = entries.filter((e) => e.type === "dir").map((e) => e.name);
5106
+ if (skillDirs.length === 0) {
5107
+ throw new Error("No skills found in the pncli repository");
5108
+ }
5109
+ const resolvedTarget = path5.resolve(targetDir);
5110
+ for (const skillName of skillDirs) {
5111
+ const existingDir = path5.resolve(targetDir, skillName);
5112
+ if (!existingDir.startsWith(resolvedTarget + path5.sep)) continue;
5113
+ if (fs3.existsSync(existingDir)) {
5114
+ fs3.rmSync(existingDir, { recursive: true, force: true });
5115
+ }
5116
+ }
5117
+ warn(`Found ${skillDirs.length} skills. Downloading...`);
5118
+ const installed = [];
5119
+ const failed = [];
5120
+ for (const skillName of skillDirs) {
5121
+ const skillDir = path5.resolve(targetDir, skillName);
5122
+ if (!skillDir.startsWith(resolvedTarget + path5.sep)) {
5123
+ failed.push(skillName);
5124
+ continue;
5125
+ }
5126
+ const skillUrl = `${RAW_BASE}/${skillName}/SKILL.md`;
5127
+ try {
5128
+ const res = await fetch(skillUrl, {
5129
+ headers: { "User-Agent": "pncli" }
5130
+ });
5131
+ if (!res.ok) {
5132
+ failed.push(skillName);
5133
+ continue;
5134
+ }
5135
+ const content = await res.text();
5136
+ fs3.mkdirSync(skillDir, { recursive: true });
5137
+ fs3.writeFileSync(path5.join(skillDir, "SKILL.md"), content, "utf8");
5138
+ installed.push(skillName);
5139
+ } catch {
5140
+ failed.push(skillName);
5141
+ }
5142
+ }
5143
+ warn(`Installed ${installed.length} skill(s) to ${targetDir}`);
5144
+ if (failed.length > 0) {
5145
+ warn(`Failed to download: ${failed.join(", ")}`);
5146
+ }
5147
+ let instructionsWritten = false;
5148
+ if (!opts.skipInstructions) {
5149
+ warn("Downloading copilot-instructions.md...");
5150
+ try {
5151
+ const res = await fetch(RAW_COPILOT_INSTRUCTIONS, {
5152
+ headers: { "User-Agent": "pncli" }
5153
+ });
5154
+ if (res.ok) {
5155
+ const content = await res.text();
5156
+ fs3.writeFileSync("copilot-instructions.md", content, "utf8");
5157
+ instructionsWritten = true;
5158
+ warn("Wrote copilot-instructions.md");
5159
+ } else {
5160
+ warn(`Failed to download copilot-instructions.md (${res.status})`);
5161
+ }
5162
+ } catch {
5163
+ warn("Failed to download copilot-instructions.md");
5164
+ }
5165
+ }
5166
+ success({
5167
+ installed,
5168
+ failed,
5169
+ target: targetDir,
5170
+ total: installed.length,
5171
+ copilotInstructions: instructionsWritten
5172
+ }, "skills", "install", start);
5173
+ } catch (err) {
5174
+ fail(err, "skills", "install", start);
5175
+ }
5176
+ });
5177
+ skills.command("list").description("List locally installed skills").option("--target <dir>", "Skills directory to scan", ".claude/skills").action((opts) => {
5178
+ const start = Date.now();
5179
+ try {
5180
+ const targetDir = path5.resolve(opts.target);
5181
+ if (!fs3.existsSync(targetDir)) {
5182
+ success({ skills: [], message: "No skills directory found. Run: pncli skills install" }, "skills", "list", start);
5183
+ return;
5184
+ }
5185
+ const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
5186
+ const skillPath = path5.join(targetDir, name, "SKILL.md");
5187
+ return fs3.existsSync(skillPath);
5188
+ });
5189
+ const skills2 = skillDirs.map((name) => {
5190
+ const content = fs3.readFileSync(path5.join(targetDir, name, "SKILL.md"), "utf8");
5191
+ const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
5192
+ const data = {};
5193
+ if (frontmatter) {
5194
+ for (const line of frontmatter[1].split("\n")) {
5195
+ const colonIdx = line.indexOf(":");
5196
+ if (colonIdx === -1) continue;
5197
+ data[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
5198
+ }
5199
+ }
5200
+ return {
5201
+ name: data.name || name,
5202
+ slug: name,
5203
+ category: data.category || "other",
5204
+ services: data.services || "",
5205
+ providers: data.providers || "none"
5206
+ };
5207
+ });
5208
+ success({ skills: skills2, total: skills2.length }, "skills", "list", start);
5209
+ } catch (err) {
5210
+ fail(err, "skills", "list", start);
5211
+ }
5212
+ });
5213
+ }
5214
+
4183
5215
  // src/cli.ts
4184
5216
  if (!process.env.PNCLI_VERIFY_TLS) {
4185
5217
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -4209,6 +5241,8 @@ registerSonarCommands(program);
4209
5241
  registerSdeCommands(program);
4210
5242
  registerDepsCommands(program);
4211
5243
  registerConfigCommands(program);
5244
+ registerAdoCommands(program);
5245
+ registerSkillsCommands(program);
4212
5246
  program.addHelpText("after", `
4213
5247
  Services:
4214
5248
  git Local git operations (status, diff, log, branch)
@@ -4218,7 +5252,9 @@ Services:
4218
5252
  confluence Confluence
4219
5253
  sonar SonarQube Server (quality gates, issues, metrics, hotspots)
4220
5254
  sde SDElements (threat modeling, countermeasures, compliance)
5255
+ ado Azure DevOps Server (work items, repos, PRs, pipelines)
4221
5256
  config Manage pncli configuration
5257
+ skills Download and manage Claude Code skills
4222
5258
  `);
4223
5259
  program.parseAsync(process.argv).catch((err) => {
4224
5260
  if (process.exitCode !== void 0) return;