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