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