@kolatts/pncli 1.0.1
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/LICENSE +201 -0
- package/NOTICE +81 -0
- package/README.md +93 -0
- package/copilot-instructions.md +159 -0
- package/dist/cli.js +1536 -0
- package/dist/cli.js.map +1 -0
- package/package.json +49 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
|
|
7
|
+
// src/lib/output.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/lib/errors.ts
|
|
11
|
+
var PncliError = class extends Error {
|
|
12
|
+
status;
|
|
13
|
+
url;
|
|
14
|
+
constructor(message, status = 1, url) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "PncliError";
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.url = url;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/lib/output.ts
|
|
23
|
+
var globalOptions = { pretty: false, verbose: false };
|
|
24
|
+
function setGlobalOptions(opts) {
|
|
25
|
+
globalOptions = opts;
|
|
26
|
+
}
|
|
27
|
+
function buildMeta(service, action, startTime) {
|
|
28
|
+
return {
|
|
29
|
+
service,
|
|
30
|
+
action,
|
|
31
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
duration_ms: Date.now() - startTime
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function success(data, service, action, startTime) {
|
|
36
|
+
const envelope = {
|
|
37
|
+
ok: true,
|
|
38
|
+
data,
|
|
39
|
+
meta: buildMeta(service, action, startTime)
|
|
40
|
+
};
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
(globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function fail(err, service, action, startTime) {
|
|
46
|
+
const errorDetail = {
|
|
47
|
+
status: err instanceof PncliError ? err.status : 1,
|
|
48
|
+
message: err instanceof Error ? err.message : String(err),
|
|
49
|
+
url: err instanceof PncliError ? err.url ?? null : null
|
|
50
|
+
};
|
|
51
|
+
const envelope = {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: errorDetail,
|
|
54
|
+
meta: buildMeta(service, action, startTime)
|
|
55
|
+
};
|
|
56
|
+
const msg = globalOptions.pretty ? chalk.red("\u2717 Error: ") + errorDetail.message : null;
|
|
57
|
+
if (msg) process.stderr.write(msg + "\n");
|
|
58
|
+
process.stdout.write(
|
|
59
|
+
(globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n"
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
function log(message) {
|
|
64
|
+
if (globalOptions.verbose) {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
(globalOptions.pretty ? chalk.gray("\u25B8 ") : "") + message + "\n"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function warn(message) {
|
|
71
|
+
process.stderr.write(
|
|
72
|
+
(globalOptions.pretty ? chalk.yellow("\u26A0 ") : "") + message + "\n"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/services/git/client.ts
|
|
77
|
+
import { execSync, execFileSync } from "child_process";
|
|
78
|
+
var DIFF_LINE_LIMIT = 5e3;
|
|
79
|
+
function exec(cmd, cwd) {
|
|
80
|
+
return execSync(cmd, { encoding: "utf8", cwd }).trim();
|
|
81
|
+
}
|
|
82
|
+
function getStatus(root) {
|
|
83
|
+
const output = exec("git status --porcelain", root);
|
|
84
|
+
const staged = [];
|
|
85
|
+
const unstaged = [];
|
|
86
|
+
const untracked = [];
|
|
87
|
+
if (!output) return { staged, unstaged, untracked };
|
|
88
|
+
for (const line of output.split("\n")) {
|
|
89
|
+
if (!line) continue;
|
|
90
|
+
const x = line[0];
|
|
91
|
+
const y = line[1];
|
|
92
|
+
const file = line.slice(3);
|
|
93
|
+
if (x === "?" && y === "?") {
|
|
94
|
+
untracked.push(file);
|
|
95
|
+
} else {
|
|
96
|
+
if (x !== " " && x !== "?") staged.push(file);
|
|
97
|
+
if (y !== " " && y !== "?") unstaged.push(file);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { staged, unstaged, untracked };
|
|
101
|
+
}
|
|
102
|
+
function parseHunkHeader(header) {
|
|
103
|
+
const match = header.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
104
|
+
if (!match) return { oldStart: 0, oldCount: 0, newStart: 0, newCount: 0 };
|
|
105
|
+
return {
|
|
106
|
+
oldStart: parseInt(match[1], 10),
|
|
107
|
+
oldCount: parseInt(match[2] ?? "1", 10),
|
|
108
|
+
newStart: parseInt(match[3], 10),
|
|
109
|
+
newCount: parseInt(match[4] ?? "1", 10)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function parseDiff(rawDiff) {
|
|
113
|
+
const lines = rawDiff.split("\n");
|
|
114
|
+
const files = [];
|
|
115
|
+
let globalTruncated = false;
|
|
116
|
+
let lineCount = 0;
|
|
117
|
+
let currentFile = null;
|
|
118
|
+
let currentHunk = null;
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
lineCount++;
|
|
121
|
+
if (lineCount > DIFF_LINE_LIMIT) {
|
|
122
|
+
globalTruncated = true;
|
|
123
|
+
if (currentFile) currentFile.truncated = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (line.startsWith("diff --git ")) {
|
|
127
|
+
if (currentHunk && currentFile) currentFile.hunks.push(currentHunk);
|
|
128
|
+
currentHunk = null;
|
|
129
|
+
if (currentFile) files.push(currentFile);
|
|
130
|
+
const pathMatch = line.match(/diff --git a\/(.+) b\/.+$/);
|
|
131
|
+
currentFile = {
|
|
132
|
+
path: pathMatch ? pathMatch[1] : line,
|
|
133
|
+
binary: false,
|
|
134
|
+
truncated: false,
|
|
135
|
+
hunks: []
|
|
136
|
+
};
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!currentFile) continue;
|
|
140
|
+
if (line.startsWith("Binary files")) {
|
|
141
|
+
currentFile.binary = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (line.startsWith("@@ ")) {
|
|
145
|
+
if (currentHunk) currentFile.hunks.push(currentHunk);
|
|
146
|
+
const { oldStart, oldCount, newStart, newCount } = parseHunkHeader(line);
|
|
147
|
+
currentHunk = { oldStart, oldCount, newStart, newCount, lines: [] };
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (currentHunk && (line.startsWith("+") || line.startsWith("-") || line.startsWith(" "))) {
|
|
151
|
+
currentHunk.lines.push(line);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (currentHunk && currentFile) currentFile.hunks.push(currentHunk);
|
|
155
|
+
if (currentFile) files.push(currentFile);
|
|
156
|
+
return { files, truncated: globalTruncated };
|
|
157
|
+
}
|
|
158
|
+
function getDiff(root, opts) {
|
|
159
|
+
const args = ["git", "diff"];
|
|
160
|
+
if (opts.staged) args.push("--staged");
|
|
161
|
+
if (opts.file) args.push("--", opts.file);
|
|
162
|
+
try {
|
|
163
|
+
const raw = exec(args.join(" "), root);
|
|
164
|
+
if (!raw) return { files: [], truncated: false };
|
|
165
|
+
return parseDiff(raw);
|
|
166
|
+
} catch {
|
|
167
|
+
return { files: [], truncated: false };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function getLog(root, opts) {
|
|
171
|
+
const sep = "";
|
|
172
|
+
const fmt = `%H${sep}%an${sep}%aI${sep}%s`;
|
|
173
|
+
const args = [`log`, `--format=${fmt}`];
|
|
174
|
+
if (opts.count) args.push(`-n`, String(opts.count));
|
|
175
|
+
if (opts.since) args.push(`--since=${opts.since}`);
|
|
176
|
+
try {
|
|
177
|
+
const output = execFileSync("git", args, { encoding: "utf8", cwd: root }).trim();
|
|
178
|
+
if (!output) return [];
|
|
179
|
+
return output.split("\n").filter(Boolean).map((line) => {
|
|
180
|
+
const parts = line.split(sep);
|
|
181
|
+
return {
|
|
182
|
+
hash: parts[0] ?? "",
|
|
183
|
+
author: parts[1] ?? "",
|
|
184
|
+
date: parts[2] ?? "",
|
|
185
|
+
message: parts[3] ?? ""
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function getBranches(root) {
|
|
193
|
+
const output = exec("git branch -a", root);
|
|
194
|
+
const local = [];
|
|
195
|
+
const remote = [];
|
|
196
|
+
let current = "";
|
|
197
|
+
for (const line of output.split("\n")) {
|
|
198
|
+
if (!line.trim()) continue;
|
|
199
|
+
const isCurrent = line.startsWith("*");
|
|
200
|
+
const name = line.replace(/^\*?\s+/, "").trim();
|
|
201
|
+
if (name.startsWith("remotes/")) {
|
|
202
|
+
remote.push(name.replace("remotes/", ""));
|
|
203
|
+
} else {
|
|
204
|
+
if (isCurrent) current = name;
|
|
205
|
+
local.push(name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { current, local, remote };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/lib/git-context.ts
|
|
212
|
+
import { execSync as execSync2 } from "child_process";
|
|
213
|
+
function getRepoRoot() {
|
|
214
|
+
try {
|
|
215
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function getCurrentBranch(repoRoot) {
|
|
221
|
+
try {
|
|
222
|
+
return execSync2("git rev-parse --abbrev-ref HEAD", {
|
|
223
|
+
encoding: "utf8",
|
|
224
|
+
cwd: repoRoot
|
|
225
|
+
}).trim();
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function parseRemote(remoteUrl, bitbucketBaseUrl) {
|
|
231
|
+
if (!bitbucketBaseUrl) return null;
|
|
232
|
+
const base = bitbucketBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
233
|
+
const sshMatch = remoteUrl.match(/^git@([^:]+)(?::\d+)?[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
234
|
+
if (sshMatch) {
|
|
235
|
+
const [, host, project, repo] = sshMatch;
|
|
236
|
+
if (host && base.includes(host)) {
|
|
237
|
+
return { project, repo };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const httpsMatch = remoteUrl.match(/^https?:\/\/([^/]+)\/scm\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
241
|
+
if (httpsMatch) {
|
|
242
|
+
const [, host, project, repo] = httpsMatch;
|
|
243
|
+
if (host && base.includes(host)) {
|
|
244
|
+
return { project, repo };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
function getRemoteUrls(repoRoot) {
|
|
250
|
+
try {
|
|
251
|
+
const output = execSync2("git remote -v", { encoding: "utf8", cwd: repoRoot });
|
|
252
|
+
return output.split("\n").filter((line) => line.includes("(fetch)")).map((line) => line.split(" ")[1]?.split(" ")[0] ?? "").filter(Boolean);
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function getGitContext(config) {
|
|
258
|
+
const root = getRepoRoot();
|
|
259
|
+
if (!root) return null;
|
|
260
|
+
const branch = getCurrentBranch(root) ?? "unknown";
|
|
261
|
+
const remoteUrls = getRemoteUrls(root);
|
|
262
|
+
let project = null;
|
|
263
|
+
let repo = null;
|
|
264
|
+
for (const url of remoteUrls) {
|
|
265
|
+
const parsed = parseRemote(url, config.bitbucket.baseUrl);
|
|
266
|
+
if (parsed) {
|
|
267
|
+
project = parsed.project;
|
|
268
|
+
repo = parsed.repo;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { root, branch, project, repo };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/lib/config.ts
|
|
276
|
+
import fs from "fs";
|
|
277
|
+
import os from "os";
|
|
278
|
+
import path from "path";
|
|
279
|
+
import { execSync as execSync3 } from "child_process";
|
|
280
|
+
var ENV_KEYS = {
|
|
281
|
+
JIRA_BASE_URL: "PNCLI_JIRA_BASE_URL",
|
|
282
|
+
JIRA_EMAIL: "PNCLI_JIRA_EMAIL",
|
|
283
|
+
JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
|
|
284
|
+
BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
|
|
285
|
+
BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
|
|
286
|
+
CONFIG_PATH: "PNCLI_CONFIG_PATH"
|
|
287
|
+
};
|
|
288
|
+
function getGlobalConfigPath(overridePath) {
|
|
289
|
+
if (overridePath) return overridePath;
|
|
290
|
+
const envPath = process.env[ENV_KEYS.CONFIG_PATH];
|
|
291
|
+
if (envPath) return envPath;
|
|
292
|
+
return path.join(os.homedir(), ".pncli", "config.json");
|
|
293
|
+
}
|
|
294
|
+
function loadJsonFile(filePath) {
|
|
295
|
+
try {
|
|
296
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
297
|
+
return JSON.parse(content);
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function getRepoRoot2() {
|
|
303
|
+
try {
|
|
304
|
+
return execSync3("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function mergeDefaults(global, repo) {
|
|
310
|
+
return {
|
|
311
|
+
jira: { ...global?.jira, ...repo?.jira },
|
|
312
|
+
bitbucket: { ...global?.bitbucket, ...repo?.bitbucket }
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function loadConfig(opts = {}) {
|
|
316
|
+
const globalConfigPath = getGlobalConfigPath(opts.configPath);
|
|
317
|
+
const globalConfig = loadJsonFile(globalConfigPath) ?? {};
|
|
318
|
+
const repoRoot = getRepoRoot2();
|
|
319
|
+
let repoConfig = {};
|
|
320
|
+
if (repoRoot) {
|
|
321
|
+
repoConfig = loadJsonFile(path.join(repoRoot, ".pncli.json")) ?? {};
|
|
322
|
+
}
|
|
323
|
+
const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);
|
|
324
|
+
return {
|
|
325
|
+
jira: {
|
|
326
|
+
baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,
|
|
327
|
+
email: process.env[ENV_KEYS.JIRA_EMAIL] ?? globalConfig.jira?.email,
|
|
328
|
+
apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken
|
|
329
|
+
},
|
|
330
|
+
bitbucket: {
|
|
331
|
+
baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
|
|
332
|
+
pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
|
|
333
|
+
},
|
|
334
|
+
defaults: mergedDefaults
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function writeGlobalConfig(config, configPath) {
|
|
338
|
+
const filePath = getGlobalConfigPath(configPath);
|
|
339
|
+
const dir = path.dirname(filePath);
|
|
340
|
+
if (!fs.existsSync(dir)) {
|
|
341
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
342
|
+
}
|
|
343
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
344
|
+
}
|
|
345
|
+
function writeRepoConfig(config) {
|
|
346
|
+
const repoRoot = getRepoRoot2();
|
|
347
|
+
const targetDir = repoRoot ?? process.cwd();
|
|
348
|
+
fs.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
349
|
+
}
|
|
350
|
+
function setConfigValue(key, value, configPath) {
|
|
351
|
+
const filePath = getGlobalConfigPath(configPath);
|
|
352
|
+
const existing = loadJsonFile(filePath) ?? {};
|
|
353
|
+
const parts = key.split(".");
|
|
354
|
+
let current = existing;
|
|
355
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
356
|
+
const part = parts[i];
|
|
357
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
358
|
+
current[part] = {};
|
|
359
|
+
}
|
|
360
|
+
current = current[part];
|
|
361
|
+
}
|
|
362
|
+
current[parts[parts.length - 1]] = value;
|
|
363
|
+
writeGlobalConfig(existing, configPath);
|
|
364
|
+
}
|
|
365
|
+
function maskConfig(config) {
|
|
366
|
+
return {
|
|
367
|
+
...config,
|
|
368
|
+
jira: {
|
|
369
|
+
...config.jira,
|
|
370
|
+
apiToken: config.jira.apiToken ? "***" : void 0
|
|
371
|
+
},
|
|
372
|
+
bitbucket: {
|
|
373
|
+
...config.bitbucket,
|
|
374
|
+
pat: config.bitbucket.pat ? "***" : void 0
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/lib/http.ts
|
|
380
|
+
function buildUrl(base, path2, params) {
|
|
381
|
+
const url = new URL(path2, base.endsWith("/") ? base : base + "/");
|
|
382
|
+
if (params) {
|
|
383
|
+
for (const [key, val] of Object.entries(params)) {
|
|
384
|
+
if (val !== void 0) {
|
|
385
|
+
url.searchParams.set(key, String(val));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return url.toString();
|
|
390
|
+
}
|
|
391
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
392
|
+
const controller = new AbortController();
|
|
393
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
394
|
+
try {
|
|
395
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
396
|
+
} finally {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function request(url, init, timeoutMs) {
|
|
401
|
+
let lastError;
|
|
402
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
403
|
+
let response;
|
|
404
|
+
try {
|
|
405
|
+
response = await fetchWithTimeout(url, init, timeoutMs);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
throw new PncliError(
|
|
408
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
409
|
+
0,
|
|
410
|
+
url
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (response.status === 429) {
|
|
414
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
415
|
+
const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
|
|
416
|
+
log(`Rate limited. Retrying after ${waitMs}ms...`);
|
|
417
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
418
|
+
lastError = new PncliError("Rate limited", 429, url);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
let message = `HTTP ${response.status} ${response.statusText}`;
|
|
423
|
+
try {
|
|
424
|
+
const body = await response.text();
|
|
425
|
+
const parsed = JSON.parse(body);
|
|
426
|
+
if (parsed.message) message = parsed.message;
|
|
427
|
+
else if (parsed.errors?.[0]?.message) message = parsed.errors[0].message;
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
throw new PncliError(message, response.status, url);
|
|
431
|
+
}
|
|
432
|
+
if (response.status === 204) {
|
|
433
|
+
return void 0;
|
|
434
|
+
}
|
|
435
|
+
return response.json();
|
|
436
|
+
}
|
|
437
|
+
throw lastError ?? new PncliError("Request failed after retries", 1, url);
|
|
438
|
+
}
|
|
439
|
+
var HttpClient = class {
|
|
440
|
+
config;
|
|
441
|
+
dryRun;
|
|
442
|
+
constructor(config, dryRun = false) {
|
|
443
|
+
this.config = config;
|
|
444
|
+
this.dryRun = dryRun;
|
|
445
|
+
}
|
|
446
|
+
jiraHeaders() {
|
|
447
|
+
const { email, apiToken } = this.config.jira;
|
|
448
|
+
if (!email || !apiToken) throw new PncliError("Jira credentials not configured. Run: pncli config init");
|
|
449
|
+
const token = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
450
|
+
return {
|
|
451
|
+
"Authorization": `Basic ${token}`,
|
|
452
|
+
"Content-Type": "application/json",
|
|
453
|
+
"Accept": "application/json"
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
bitbucketHeaders() {
|
|
457
|
+
const { pat } = this.config.bitbucket;
|
|
458
|
+
if (!pat) throw new PncliError("Bitbucket credentials not configured. Run: pncli config init");
|
|
459
|
+
return {
|
|
460
|
+
"Authorization": `Bearer ${pat}`,
|
|
461
|
+
"Content-Type": "application/json",
|
|
462
|
+
"Accept": "application/json"
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async jira(path2, opts = {}) {
|
|
466
|
+
const baseUrl = this.config.jira.baseUrl;
|
|
467
|
+
if (!baseUrl) throw new PncliError("Jira baseUrl not configured. Run: pncli config init");
|
|
468
|
+
const url = buildUrl(baseUrl, path2, opts.params);
|
|
469
|
+
const headers = this.jiraHeaders();
|
|
470
|
+
const init = {
|
|
471
|
+
method: opts.method ?? "GET",
|
|
472
|
+
headers,
|
|
473
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
474
|
+
};
|
|
475
|
+
if (this.dryRun) {
|
|
476
|
+
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
477
|
+
process.stderr.write(`DRY RUN: ${init.method} ${url}
|
|
478
|
+
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
479
|
+
`);
|
|
480
|
+
if (opts.body) process.stderr.write(`Body: ${JSON.stringify(opts.body, null, 2)}
|
|
481
|
+
`);
|
|
482
|
+
process.exit(0);
|
|
483
|
+
}
|
|
484
|
+
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
485
|
+
}
|
|
486
|
+
async bitbucket(path2, opts = {}) {
|
|
487
|
+
const baseUrl = this.config.bitbucket.baseUrl;
|
|
488
|
+
if (!baseUrl) throw new PncliError("Bitbucket baseUrl not configured. Run: pncli config init");
|
|
489
|
+
const url = buildUrl(baseUrl, path2, opts.params);
|
|
490
|
+
const headers = this.bitbucketHeaders();
|
|
491
|
+
const init = {
|
|
492
|
+
method: opts.method ?? "GET",
|
|
493
|
+
headers,
|
|
494
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
495
|
+
};
|
|
496
|
+
if (this.dryRun) {
|
|
497
|
+
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
498
|
+
process.stderr.write(`DRY RUN: ${init.method} ${url}
|
|
499
|
+
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
500
|
+
`);
|
|
501
|
+
if (opts.body) process.stderr.write(`Body: ${JSON.stringify(opts.body, null, 2)}
|
|
502
|
+
`);
|
|
503
|
+
process.exit(0);
|
|
504
|
+
}
|
|
505
|
+
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
506
|
+
}
|
|
507
|
+
async paginate(fetchPage) {
|
|
508
|
+
const results = [];
|
|
509
|
+
let start = 0;
|
|
510
|
+
const limit = 100;
|
|
511
|
+
while (true) {
|
|
512
|
+
const page = await fetchPage(start, limit);
|
|
513
|
+
results.push(...page.values);
|
|
514
|
+
if (page.isLastPage) break;
|
|
515
|
+
start = page.nextPageStart ?? start + limit;
|
|
516
|
+
}
|
|
517
|
+
return results;
|
|
518
|
+
}
|
|
519
|
+
async jiraPaginate(fetchPage) {
|
|
520
|
+
const results = [];
|
|
521
|
+
let startAt = 0;
|
|
522
|
+
const maxResults = 100;
|
|
523
|
+
while (true) {
|
|
524
|
+
const page = await fetchPage(startAt, maxResults);
|
|
525
|
+
const items = page.issues ?? page.values ?? [];
|
|
526
|
+
results.push(...items);
|
|
527
|
+
startAt += items.length;
|
|
528
|
+
if (startAt >= page.total || items.length === 0) break;
|
|
529
|
+
}
|
|
530
|
+
return results;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
function createHttpClient(config, dryRun = false) {
|
|
534
|
+
return new HttpClient(config, dryRun);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/services/bitbucket/client.ts
|
|
538
|
+
var API = "/rest/api/1.0";
|
|
539
|
+
var BitbucketClient = class {
|
|
540
|
+
constructor(http) {
|
|
541
|
+
this.http = http;
|
|
542
|
+
}
|
|
543
|
+
http;
|
|
544
|
+
async listPRs(opts) {
|
|
545
|
+
return this.http.paginate(
|
|
546
|
+
(start, limit) => this.http.bitbucket(
|
|
547
|
+
`${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests`,
|
|
548
|
+
{
|
|
549
|
+
params: {
|
|
550
|
+
state: opts.state ?? "OPEN",
|
|
551
|
+
...opts.author ? { "author.username": opts.author } : {},
|
|
552
|
+
start,
|
|
553
|
+
limit
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
async getPR(project, repo, id) {
|
|
560
|
+
return this.http.bitbucket(
|
|
561
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${id}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
async createPR(opts) {
|
|
565
|
+
return this.http.bitbucket(
|
|
566
|
+
`${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests`,
|
|
567
|
+
{
|
|
568
|
+
method: "POST",
|
|
569
|
+
body: {
|
|
570
|
+
title: opts.title,
|
|
571
|
+
description: opts.description,
|
|
572
|
+
fromRef: {
|
|
573
|
+
id: `refs/heads/${opts.source}`,
|
|
574
|
+
repository: {
|
|
575
|
+
slug: opts.repo,
|
|
576
|
+
project: { key: opts.project }
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
toRef: {
|
|
580
|
+
id: `refs/heads/${opts.target}`,
|
|
581
|
+
repository: {
|
|
582
|
+
slug: opts.repo,
|
|
583
|
+
project: { key: opts.project }
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
reviewers: (opts.reviewers ?? []).map((slug) => ({ user: { slug } }))
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
async updatePR(opts) {
|
|
592
|
+
const body = { version: opts.version };
|
|
593
|
+
if (opts.title) body.title = opts.title;
|
|
594
|
+
if (opts.description !== void 0) body.description = opts.description;
|
|
595
|
+
if (opts.reviewers) body.reviewers = opts.reviewers.map((slug) => ({ user: { slug } }));
|
|
596
|
+
return this.http.bitbucket(
|
|
597
|
+
`${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests/${opts.id}`,
|
|
598
|
+
{ method: "PUT", body }
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
async mergePR(opts) {
|
|
602
|
+
const params = {
|
|
603
|
+
version: opts.version
|
|
604
|
+
};
|
|
605
|
+
const body = {};
|
|
606
|
+
if (opts.strategy) body.strategyId = opts.strategy;
|
|
607
|
+
if (opts.deleteBranch) body.autoSubject = true;
|
|
608
|
+
return this.http.bitbucket(
|
|
609
|
+
`${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests/${opts.id}/merge`,
|
|
610
|
+
{ method: "POST", params, body }
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
async declinePR(project, repo, id, version) {
|
|
614
|
+
return this.http.bitbucket(
|
|
615
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${id}/decline`,
|
|
616
|
+
{ method: "POST", params: { version } }
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
async listComments(project, repo, prId) {
|
|
620
|
+
const activities = await this.http.paginate(
|
|
621
|
+
(start, limit) => this.http.bitbucket(
|
|
622
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/activities`,
|
|
623
|
+
{ params: { start, limit } }
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
return activities.filter((a) => a.action === "COMMENTED" && a.comment).map((a) => a.comment);
|
|
627
|
+
}
|
|
628
|
+
async addComment(project, repo, prId, text) {
|
|
629
|
+
return this.http.bitbucket(
|
|
630
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/comments`,
|
|
631
|
+
{ method: "POST", body: { text } }
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
async addInlineComment(opts) {
|
|
635
|
+
return this.http.bitbucket(
|
|
636
|
+
`${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests/${opts.prId}/comments`,
|
|
637
|
+
{
|
|
638
|
+
method: "POST",
|
|
639
|
+
body: {
|
|
640
|
+
text: opts.text,
|
|
641
|
+
anchor: {
|
|
642
|
+
line: opts.line,
|
|
643
|
+
lineType: opts.lineType ?? "ADDED",
|
|
644
|
+
fileType: "TO",
|
|
645
|
+
path: opts.filePath
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
async replyComment(project, repo, prId, commentId, text) {
|
|
652
|
+
return this.http.bitbucket(
|
|
653
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/comments`,
|
|
654
|
+
{ method: "POST", body: { text, parent: { id: commentId } } }
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
async resolveComment(project, repo, prId, commentId, version) {
|
|
658
|
+
await this.http.bitbucket(
|
|
659
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/comments/${commentId}/resolve`,
|
|
660
|
+
{ method: "PUT", params: { version } }
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
async deleteComment(project, repo, prId, commentId, version) {
|
|
664
|
+
await this.http.bitbucket(
|
|
665
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/comments/${commentId}`,
|
|
666
|
+
{ method: "DELETE", params: { version } }
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
async getDiff(project, repo, prId, file, contextLines) {
|
|
670
|
+
const params = {};
|
|
671
|
+
if (contextLines !== void 0) params.contextLines = contextLines;
|
|
672
|
+
if (file) params.path = file;
|
|
673
|
+
const result = await this.http.bitbucket(
|
|
674
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff`,
|
|
675
|
+
{ params }
|
|
676
|
+
);
|
|
677
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
678
|
+
}
|
|
679
|
+
async listFiles(project, repo, prId) {
|
|
680
|
+
const result = await this.http.bitbucket(
|
|
681
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/changes`,
|
|
682
|
+
{ params: { limit: 1e3 } }
|
|
683
|
+
);
|
|
684
|
+
return (result.values ?? []).map((v) => typeof v.path === "string" ? v.path : JSON.stringify(v.path));
|
|
685
|
+
}
|
|
686
|
+
async approvePR(project, repo, prId) {
|
|
687
|
+
return this.http.bitbucket(
|
|
688
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/~`,
|
|
689
|
+
{ method: "PUT", body: { status: "APPROVED" } }
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
async unapprovePR(project, repo, prId) {
|
|
693
|
+
return this.http.bitbucket(
|
|
694
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/~`,
|
|
695
|
+
{ method: "DELETE" }
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
async needsWorkPR(project, repo, prId) {
|
|
699
|
+
return this.http.bitbucket(
|
|
700
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/~`,
|
|
701
|
+
{ method: "PUT", body: { status: "NEEDS_WORK" } }
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
async listReviewers(project, repo, prId) {
|
|
705
|
+
const pr = await this.getPR(project, repo, prId);
|
|
706
|
+
return pr.reviewers;
|
|
707
|
+
}
|
|
708
|
+
async listBuilds(project, repo, prId) {
|
|
709
|
+
const commits = await this.http.bitbucket(
|
|
710
|
+
`${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/commits`,
|
|
711
|
+
{ params: { limit: 1 } }
|
|
712
|
+
);
|
|
713
|
+
const sha = commits.values[0]?.id;
|
|
714
|
+
if (!sha) return [];
|
|
715
|
+
return this.getBuildStatus(sha);
|
|
716
|
+
}
|
|
717
|
+
async getBuildStatus(commit) {
|
|
718
|
+
const result = await this.http.bitbucket(
|
|
719
|
+
`/rest/build-status/1.0/commits/${commit}`
|
|
720
|
+
);
|
|
721
|
+
return result.values ?? [];
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/services/git/commands.ts
|
|
726
|
+
function requireRepoRoot() {
|
|
727
|
+
const root = getRepoRoot();
|
|
728
|
+
if (!root) throw new PncliError("Not a git repository", 1);
|
|
729
|
+
return root;
|
|
730
|
+
}
|
|
731
|
+
function registerGitCommands(program2) {
|
|
732
|
+
const git = program2.command("git").description("Local git operations");
|
|
733
|
+
git.command("status").description("Show staged, unstaged, and untracked files as JSON").action(() => {
|
|
734
|
+
const start = Date.now();
|
|
735
|
+
try {
|
|
736
|
+
const root = requireRepoRoot();
|
|
737
|
+
const data = getStatus(root);
|
|
738
|
+
success(data, "git", "status", start);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
fail(err, "git", "status", start);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
git.command("diff").description("Show diff as structured JSON").option("--staged", "Show staged changes only").option("--file <path>", "Limit diff to a specific file").action((opts) => {
|
|
744
|
+
const start = Date.now();
|
|
745
|
+
try {
|
|
746
|
+
const root = requireRepoRoot();
|
|
747
|
+
const data = getDiff(root, { staged: opts.staged, file: opts.file });
|
|
748
|
+
success(data, "git", "diff", start);
|
|
749
|
+
} catch (err) {
|
|
750
|
+
fail(err, "git", "diff", start);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
git.command("log").description("Show recent commits as JSON").option("--count <n>", "Number of commits to show", "10").option("--since <date>", 'Show commits since date (e.g. "2 weeks ago")').action((opts) => {
|
|
754
|
+
const start = Date.now();
|
|
755
|
+
try {
|
|
756
|
+
const root = requireRepoRoot();
|
|
757
|
+
const count = opts.count ? parseInt(opts.count, 10) : void 0;
|
|
758
|
+
const data = getLog(root, { count, since: opts.since });
|
|
759
|
+
success(data, "git", "log", start);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
fail(err, "git", "log", start);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
git.command("branch").description("Show current branch and all local/remote branches").action(() => {
|
|
765
|
+
const start = Date.now();
|
|
766
|
+
try {
|
|
767
|
+
const root = requireRepoRoot();
|
|
768
|
+
const data = getBranches(root);
|
|
769
|
+
success(data, "git", "branch", start);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
fail(err, "git", "branch", start);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
git.command("current-pr").description("Find the open PR for the current branch").action(async () => {
|
|
775
|
+
const start = Date.now();
|
|
776
|
+
try {
|
|
777
|
+
const opts = program2.optsWithGlobals();
|
|
778
|
+
const config = loadConfig({ configPath: opts.config });
|
|
779
|
+
if (!config.bitbucket.baseUrl || !config.bitbucket.pat) {
|
|
780
|
+
success(
|
|
781
|
+
{ message: "Requires Bitbucket config. Available after pncli config init." },
|
|
782
|
+
"git",
|
|
783
|
+
"current-pr",
|
|
784
|
+
start
|
|
785
|
+
);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const root = requireRepoRoot();
|
|
789
|
+
const branch = getCurrentBranch(root);
|
|
790
|
+
if (!branch) throw new PncliError("Could not determine current branch", 1);
|
|
791
|
+
const ctx = getGitContext(config);
|
|
792
|
+
const project = ctx?.project ?? config.defaults.bitbucket?.project ?? "";
|
|
793
|
+
const repo = ctx?.repo ?? config.defaults.bitbucket?.repo ?? "";
|
|
794
|
+
if (!project || !repo) throw new PncliError("Could not determine Bitbucket project/repo", 1);
|
|
795
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
796
|
+
const client = new BitbucketClient(http);
|
|
797
|
+
const prs = await client.listPRs({ project, repo, state: "OPEN" });
|
|
798
|
+
const match = prs.find((pr) => pr.fromRef.displayId === branch) ?? null;
|
|
799
|
+
success(match, "git", "current-pr", start);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
fail(err, "git", "current-pr", start);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/services/jira/client.ts
|
|
807
|
+
var API2 = "/rest/api/3";
|
|
808
|
+
var JiraClient = class {
|
|
809
|
+
constructor(http) {
|
|
810
|
+
this.http = http;
|
|
811
|
+
}
|
|
812
|
+
http;
|
|
813
|
+
async getIssue(key) {
|
|
814
|
+
return this.http.jira(`${API2}/issue/${key}`);
|
|
815
|
+
}
|
|
816
|
+
async createIssue(opts) {
|
|
817
|
+
const body = {
|
|
818
|
+
fields: {
|
|
819
|
+
project: { key: opts.project },
|
|
820
|
+
issuetype: { name: opts.issueType },
|
|
821
|
+
summary: opts.summary,
|
|
822
|
+
...opts.description ? {
|
|
823
|
+
description: {
|
|
824
|
+
type: "doc",
|
|
825
|
+
version: 1,
|
|
826
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: opts.description }] }]
|
|
827
|
+
}
|
|
828
|
+
} : {},
|
|
829
|
+
...opts.priority ? { priority: { name: opts.priority } } : {},
|
|
830
|
+
...opts.assignee ? { assignee: { accountId: opts.assignee } } : {},
|
|
831
|
+
...opts.labels?.length ? { labels: opts.labels } : {}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
const created = await this.http.jira(`${API2}/issue`, {
|
|
835
|
+
method: "POST",
|
|
836
|
+
body
|
|
837
|
+
});
|
|
838
|
+
return this.getIssue(created.key);
|
|
839
|
+
}
|
|
840
|
+
async updateIssue(key, opts) {
|
|
841
|
+
const fields = {};
|
|
842
|
+
if (opts.summary) fields.summary = opts.summary;
|
|
843
|
+
if (opts.description) {
|
|
844
|
+
fields.description = {
|
|
845
|
+
type: "doc",
|
|
846
|
+
version: 1,
|
|
847
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: opts.description }] }]
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
if (opts.priority) fields.priority = { name: opts.priority };
|
|
851
|
+
if (opts.assignee) fields.assignee = { accountId: opts.assignee };
|
|
852
|
+
if (opts.labels) fields.labels = opts.labels;
|
|
853
|
+
await this.http.jira(`${API2}/issue/${key}`, {
|
|
854
|
+
method: "PUT",
|
|
855
|
+
body: { fields }
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async listTransitions(key) {
|
|
859
|
+
const result = await this.http.jira(
|
|
860
|
+
`${API2}/issue/${key}/transitions`
|
|
861
|
+
);
|
|
862
|
+
return result.transitions;
|
|
863
|
+
}
|
|
864
|
+
async transitionIssue(key, transitionId) {
|
|
865
|
+
await this.http.jira(`${API2}/issue/${key}/transitions`, {
|
|
866
|
+
method: "POST",
|
|
867
|
+
body: { transition: { id: transitionId } }
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
async addComment(key, text) {
|
|
871
|
+
return this.http.jira(`${API2}/issue/${key}/comment`, {
|
|
872
|
+
method: "POST",
|
|
873
|
+
body: {
|
|
874
|
+
body: {
|
|
875
|
+
type: "doc",
|
|
876
|
+
version: 1,
|
|
877
|
+
content: [{ type: "paragraph", content: [{ type: "text", text }] }]
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async listComments(key) {
|
|
883
|
+
return this.http.jiraPaginate(async (startAt, maxResults) => {
|
|
884
|
+
const result = await this.http.jira(
|
|
885
|
+
`${API2}/issue/${key}/comment`,
|
|
886
|
+
{ params: { startAt, maxResults } }
|
|
887
|
+
);
|
|
888
|
+
return { ...result, values: result.comments };
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
async search(jql, maxResults) {
|
|
892
|
+
if (maxResults !== void 0) {
|
|
893
|
+
return this.http.jira(`${API2}/search`, {
|
|
894
|
+
method: "POST",
|
|
895
|
+
body: { jql, maxResults, fields: ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"] }
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
const allIssues = await this.http.jiraPaginate(async (startAt, max) => {
|
|
899
|
+
const result = await this.http.jira(`${API2}/search`, {
|
|
900
|
+
method: "POST",
|
|
901
|
+
body: { jql, startAt, maxResults: max, fields: ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"] }
|
|
902
|
+
});
|
|
903
|
+
return { ...result, values: result.issues };
|
|
904
|
+
});
|
|
905
|
+
return { issues: allIssues, total: allIssues.length, startAt: 0, maxResults: allIssues.length };
|
|
906
|
+
}
|
|
907
|
+
async assignIssue(key, accountId) {
|
|
908
|
+
await this.http.jira(`${API2}/issue/${key}/assignee`, {
|
|
909
|
+
method: "PUT",
|
|
910
|
+
body: { accountId }
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
async linkIssue(opts) {
|
|
914
|
+
let linkTypeId = opts.linkType;
|
|
915
|
+
if (isNaN(parseInt(opts.linkType, 10))) {
|
|
916
|
+
const types = await this.http.jira(
|
|
917
|
+
`${API2}/issueLinkType`
|
|
918
|
+
);
|
|
919
|
+
const found = types.issueLinkTypes.find(
|
|
920
|
+
(t) => t.name.toLowerCase() === opts.linkType.toLowerCase() || t.inward.toLowerCase() === opts.linkType.toLowerCase() || t.outward.toLowerCase() === opts.linkType.toLowerCase()
|
|
921
|
+
);
|
|
922
|
+
if (!found) throw new Error(`Link type not found: ${opts.linkType}`);
|
|
923
|
+
linkTypeId = found.id;
|
|
924
|
+
}
|
|
925
|
+
await this.http.jira(`${API2}/issueLink`, {
|
|
926
|
+
method: "POST",
|
|
927
|
+
body: {
|
|
928
|
+
type: { id: linkTypeId },
|
|
929
|
+
inwardIssue: { key: opts.key },
|
|
930
|
+
outwardIssue: { key: opts.target }
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
// src/services/jira/commands.ts
|
|
937
|
+
function getClient(program2) {
|
|
938
|
+
const opts = program2.optsWithGlobals();
|
|
939
|
+
const config = loadConfig({ configPath: opts.config });
|
|
940
|
+
if (!config.jira.baseUrl) throw new PncliError("Jira not configured. Run: pncli config init");
|
|
941
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
942
|
+
return new JiraClient(http);
|
|
943
|
+
}
|
|
944
|
+
function getDefaults(program2) {
|
|
945
|
+
const opts = program2.optsWithGlobals();
|
|
946
|
+
const config = loadConfig({ configPath: opts.config });
|
|
947
|
+
return {
|
|
948
|
+
project: config.defaults.jira?.project,
|
|
949
|
+
issueType: config.defaults.jira?.issueType,
|
|
950
|
+
priority: config.defaults.jira?.priority
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function registerJiraCommands(program2) {
|
|
954
|
+
const jira = program2.command("jira").description("Jira Data Cloud operations");
|
|
955
|
+
jira.command("get-issue").description("Get a Jira issue by key").requiredOption("--key <issue-key>", "Issue key (e.g. PROJ-123)").action(async (opts) => {
|
|
956
|
+
const start = Date.now();
|
|
957
|
+
try {
|
|
958
|
+
const client = getClient(program2);
|
|
959
|
+
const data = await client.getIssue(opts.key);
|
|
960
|
+
success(data, "jira", "get-issue", start);
|
|
961
|
+
} catch (err) {
|
|
962
|
+
fail(err, "jira", "get-issue", start);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").action(async (opts) => {
|
|
966
|
+
const start = Date.now();
|
|
967
|
+
try {
|
|
968
|
+
const client = getClient(program2);
|
|
969
|
+
const defaults = getDefaults(program2);
|
|
970
|
+
const project = opts.project ?? defaults.project;
|
|
971
|
+
const issueType = opts.type ?? defaults.issueType ?? "Task";
|
|
972
|
+
const priority = opts.priority ?? defaults.priority;
|
|
973
|
+
if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
|
|
974
|
+
const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
|
|
975
|
+
const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels });
|
|
976
|
+
success(data, "jira", "create-issue", start);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
fail(err, "jira", "create-issue", start);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").action(async (opts) => {
|
|
982
|
+
const start = Date.now();
|
|
983
|
+
try {
|
|
984
|
+
const client = getClient(program2);
|
|
985
|
+
const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
|
|
986
|
+
await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels });
|
|
987
|
+
success({ updated: opts.key }, "jira", "update-issue", start);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
fail(err, "jira", "update-issue", start);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
jira.command("transition-issue").description("Transition a Jira issue to a new status").requiredOption("--key <issue-key>", "Issue key").requiredOption("--transition <name-or-id>", "Transition name or ID").action(async (opts) => {
|
|
993
|
+
const start = Date.now();
|
|
994
|
+
try {
|
|
995
|
+
const client = getClient(program2);
|
|
996
|
+
let transitionId = opts.transition;
|
|
997
|
+
if (isNaN(parseInt(opts.transition, 10))) {
|
|
998
|
+
const transitions = await client.listTransitions(opts.key);
|
|
999
|
+
const found = transitions.find((t) => t.name.toLowerCase() === opts.transition.toLowerCase());
|
|
1000
|
+
if (!found) throw new PncliError(`Transition not found: ${opts.transition}`, 1);
|
|
1001
|
+
transitionId = found.id;
|
|
1002
|
+
}
|
|
1003
|
+
await client.transitionIssue(opts.key, transitionId);
|
|
1004
|
+
success({ transitioned: opts.key, transition: opts.transition }, "jira", "transition-issue", start);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
fail(err, "jira", "transition-issue", start);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
jira.command("list-transitions").description("List available transitions for an issue").requiredOption("--key <issue-key>", "Issue key").action(async (opts) => {
|
|
1010
|
+
const start = Date.now();
|
|
1011
|
+
try {
|
|
1012
|
+
const client = getClient(program2);
|
|
1013
|
+
const data = await client.listTransitions(opts.key);
|
|
1014
|
+
success(data, "jira", "list-transitions", start);
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
fail(err, "jira", "list-transitions", start);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
jira.command("add-comment").description("Add a comment to a Jira issue").requiredOption("--key <issue-key>", "Issue key").requiredOption("--body <text>", "Comment text").action(async (opts) => {
|
|
1020
|
+
const start = Date.now();
|
|
1021
|
+
try {
|
|
1022
|
+
const client = getClient(program2);
|
|
1023
|
+
const data = await client.addComment(opts.key, opts.body);
|
|
1024
|
+
success(data, "jira", "add-comment", start);
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
fail(err, "jira", "add-comment", start);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
jira.command("list-comments").description("List comments on a Jira issue").requiredOption("--key <issue-key>", "Issue key").action(async (opts) => {
|
|
1030
|
+
const start = Date.now();
|
|
1031
|
+
try {
|
|
1032
|
+
const client = getClient(program2);
|
|
1033
|
+
const data = await client.listComments(opts.key);
|
|
1034
|
+
success(data, "jira", "list-comments", start);
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
fail(err, "jira", "list-comments", start);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
jira.command("search").description("Search Jira issues with JQL").requiredOption("--jql <query>", "JQL query string").option("--max-results <n>", "Maximum number of results").action(async (opts) => {
|
|
1040
|
+
const start = Date.now();
|
|
1041
|
+
try {
|
|
1042
|
+
const client = getClient(program2);
|
|
1043
|
+
const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : void 0;
|
|
1044
|
+
const data = await client.search(opts.jql, maxResults);
|
|
1045
|
+
success(data, "jira", "search", start);
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
fail(err, "jira", "search", start);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
jira.command("assign").description("Assign a Jira issue to a user").requiredOption("--key <issue-key>", "Issue key").requiredOption("--assignee <accountId>", "Assignee account ID").action(async (opts) => {
|
|
1051
|
+
const start = Date.now();
|
|
1052
|
+
try {
|
|
1053
|
+
const client = getClient(program2);
|
|
1054
|
+
await client.assignIssue(opts.key, opts.assignee);
|
|
1055
|
+
success({ assigned: opts.key, assignee: opts.assignee }, "jira", "assign", start);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
fail(err, "jira", "assign", start);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
jira.command("link-issue").description("Link two Jira issues together").requiredOption("--key <issue-key>", "Source issue key").requiredOption("--link-type <type>", "Link type name or ID").requiredOption("--target <issue-key>", "Target issue key").action(async (opts) => {
|
|
1061
|
+
const start = Date.now();
|
|
1062
|
+
try {
|
|
1063
|
+
const client = getClient(program2);
|
|
1064
|
+
await client.linkIssue({ key: opts.key, linkType: opts.linkType, target: opts.target });
|
|
1065
|
+
success({ linked: opts.key, to: opts.target, type: opts.linkType }, "jira", "link-issue", start);
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
fail(err, "jira", "link-issue", start);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/services/bitbucket/commands.ts
|
|
1073
|
+
function getClient2(program2) {
|
|
1074
|
+
const opts = program2.optsWithGlobals();
|
|
1075
|
+
const config = loadConfig({ configPath: opts.config });
|
|
1076
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
1077
|
+
const client = new BitbucketClient(http);
|
|
1078
|
+
const ctx = getGitContext(config);
|
|
1079
|
+
const project = opts.project ?? ctx?.project ?? config.defaults.bitbucket?.project ?? "";
|
|
1080
|
+
const repo = opts.repo ?? ctx?.repo ?? config.defaults.bitbucket?.repo ?? "";
|
|
1081
|
+
if (!project || !repo) {
|
|
1082
|
+
throw new PncliError(
|
|
1083
|
+
"Could not determine Bitbucket project/repo. Pass --project and --repo, or run pncli config init.",
|
|
1084
|
+
1
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
return { client, project, repo };
|
|
1088
|
+
}
|
|
1089
|
+
function registerBitbucketCommands(program2) {
|
|
1090
|
+
const bb = program2.command("bitbucket").description("Bitbucket Server operations").option("--project <key>", "Bitbucket project key").option("--repo <slug>", "Bitbucket repository slug");
|
|
1091
|
+
bb.command("list-prs").description("List pull requests").option("--state <state>", "PR state: OPEN|MERGED|DECLINED|ALL", "OPEN").option("--author <username>", "Filter by author username").option("--reviewer <username>", "Filter by reviewer username").action(async (opts) => {
|
|
1092
|
+
const start = Date.now();
|
|
1093
|
+
try {
|
|
1094
|
+
const { client, project, repo } = getClient2(program2);
|
|
1095
|
+
const data = await client.listPRs({ project, repo, state: opts.state, author: opts.author, reviewer: opts.reviewer });
|
|
1096
|
+
success(data, "bitbucket", "list-prs", start);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
fail(err, "bitbucket", "list-prs", start);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
bb.command("get-pr").description("Get a pull request by ID").requiredOption("--id <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1102
|
+
const start = Date.now();
|
|
1103
|
+
try {
|
|
1104
|
+
const { client, project, repo } = getClient2(program2);
|
|
1105
|
+
const data = await client.getPR(project, repo, parseInt(opts.id, 10));
|
|
1106
|
+
success(data, "bitbucket", "get-pr", start);
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
fail(err, "bitbucket", "get-pr", start);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
bb.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--source <branch>", "Source branch").option("--target <branch>", "Target branch (defaults to config)").option("--description <desc>", "PR description").option("--reviewers <users>", "Comma-separated reviewer usernames").action(async (opts) => {
|
|
1112
|
+
const start = Date.now();
|
|
1113
|
+
try {
|
|
1114
|
+
const { client, project, repo } = getClient2(program2);
|
|
1115
|
+
const config = loadConfig({ configPath: program2.optsWithGlobals().config });
|
|
1116
|
+
const target = opts.target ?? config.defaults.bitbucket?.targetBranch ?? "main";
|
|
1117
|
+
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : [];
|
|
1118
|
+
const data = await client.createPR({ project, repo, title: opts.title, source: opts.source, target, description: opts.description, reviewers });
|
|
1119
|
+
success(data, "bitbucket", "create-pr", start);
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
fail(err, "bitbucket", "create-pr", start);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
bb.command("update-pr").description("Update a pull request").requiredOption("--id <pr-id>", "Pull request ID").option("--title <title>", "New title").option("--description <desc>", "New description").option("--reviewers <users>", "Comma-separated reviewer usernames").action(async (opts) => {
|
|
1125
|
+
const start = Date.now();
|
|
1126
|
+
try {
|
|
1127
|
+
const { client, project, repo } = getClient2(program2);
|
|
1128
|
+
const prId = parseInt(opts.id, 10);
|
|
1129
|
+
const pr = await client.getPR(project, repo, prId);
|
|
1130
|
+
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : void 0;
|
|
1131
|
+
const data = await client.updatePR({ project, repo, id: prId, title: opts.title, description: opts.description, reviewers, version: pr.version });
|
|
1132
|
+
success(data, "bitbucket", "update-pr", start);
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
fail(err, "bitbucket", "update-pr", start);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
bb.command("merge-pr").description("Merge a pull request").requiredOption("--id <pr-id>", "Pull request ID").option("--strategy <strategy>", "Merge strategy: merge|squash|ff").option("--delete-branch", "Delete source branch after merge").action(async (opts) => {
|
|
1138
|
+
const start = Date.now();
|
|
1139
|
+
try {
|
|
1140
|
+
const { client, project, repo } = getClient2(program2);
|
|
1141
|
+
const prId = parseInt(opts.id, 10);
|
|
1142
|
+
const pr = await client.getPR(project, repo, prId);
|
|
1143
|
+
const data = await client.mergePR({ project, repo, id: prId, version: pr.version, strategy: opts.strategy, deleteBranch: opts.deleteBranch });
|
|
1144
|
+
success(data, "bitbucket", "merge-pr", start);
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
fail(err, "bitbucket", "merge-pr", start);
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
bb.command("decline-pr").description("Decline a pull request").requiredOption("--id <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1150
|
+
const start = Date.now();
|
|
1151
|
+
try {
|
|
1152
|
+
const { client, project, repo } = getClient2(program2);
|
|
1153
|
+
const prId = parseInt(opts.id, 10);
|
|
1154
|
+
const pr = await client.getPR(project, repo, prId);
|
|
1155
|
+
const data = await client.declinePR(project, repo, prId, pr.version);
|
|
1156
|
+
success(data, "bitbucket", "decline-pr", start);
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
fail(err, "bitbucket", "decline-pr", start);
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
bb.command("list-comments").description("List comments on a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1162
|
+
const start = Date.now();
|
|
1163
|
+
try {
|
|
1164
|
+
const { client, project, repo } = getClient2(program2);
|
|
1165
|
+
const data = await client.listComments(project, repo, parseInt(opts.pr, 10));
|
|
1166
|
+
success(data, "bitbucket", "list-comments", start);
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
fail(err, "bitbucket", "list-comments", start);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
bb.command("add-comment").description("Add a comment to a pull request").requiredOption("--pr <pr-id>", "Pull request ID").requiredOption("--body <text>", "Comment text").action(async (opts) => {
|
|
1172
|
+
const start = Date.now();
|
|
1173
|
+
try {
|
|
1174
|
+
const { client, project, repo } = getClient2(program2);
|
|
1175
|
+
const data = await client.addComment(project, repo, parseInt(opts.pr, 10), opts.body);
|
|
1176
|
+
success(data, "bitbucket", "add-comment", start);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
fail(err, "bitbucket", "add-comment", start);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
bb.command("add-inline-comment").description("Add an inline comment to a file in a pull request").requiredOption("--pr <pr-id>", "Pull request ID").requiredOption("--file <path>", "File path").requiredOption("--line <n>", "Line number").requiredOption("--body <text>", "Comment text").option("--line-type <type>", "Line type: ADDED|REMOVED|CONTEXT", "ADDED").action(async (opts) => {
|
|
1182
|
+
const start = Date.now();
|
|
1183
|
+
try {
|
|
1184
|
+
const { client, project, repo } = getClient2(program2);
|
|
1185
|
+
const data = await client.addInlineComment({
|
|
1186
|
+
project,
|
|
1187
|
+
repo,
|
|
1188
|
+
prId: parseInt(opts.pr, 10),
|
|
1189
|
+
text: opts.body,
|
|
1190
|
+
filePath: opts.file,
|
|
1191
|
+
line: parseInt(opts.line, 10),
|
|
1192
|
+
lineType: opts.lineType
|
|
1193
|
+
});
|
|
1194
|
+
success(data, "bitbucket", "add-inline-comment", start);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
fail(err, "bitbucket", "add-inline-comment", start);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
bb.command("reply-comment").description("Reply to a comment on a pull request").requiredOption("--pr <pr-id>", "Pull request ID").requiredOption("--comment-id <id>", "Comment ID to reply to").requiredOption("--body <text>", "Reply text").action(async (opts) => {
|
|
1200
|
+
const start = Date.now();
|
|
1201
|
+
try {
|
|
1202
|
+
const { client, project, repo } = getClient2(program2);
|
|
1203
|
+
const data = await client.replyComment(project, repo, parseInt(opts.pr, 10), parseInt(opts.commentId, 10), opts.body);
|
|
1204
|
+
success(data, "bitbucket", "reply-comment", start);
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
fail(err, "bitbucket", "reply-comment", start);
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
bb.command("resolve-comment").description("Resolve a comment on a pull request").requiredOption("--pr <pr-id>", "Pull request ID").requiredOption("--comment-id <id>", "Comment ID").option("--version <n>", "Comment version", "0").action(async (opts) => {
|
|
1210
|
+
const start = Date.now();
|
|
1211
|
+
try {
|
|
1212
|
+
const { client, project, repo } = getClient2(program2);
|
|
1213
|
+
await client.resolveComment(project, repo, parseInt(opts.pr, 10), parseInt(opts.commentId, 10), parseInt(opts.version ?? "0", 10));
|
|
1214
|
+
success({ resolved: true }, "bitbucket", "resolve-comment", start);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
fail(err, "bitbucket", "resolve-comment", start);
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
bb.command("delete-comment").description("Delete a comment on a pull request").requiredOption("--pr <pr-id>", "Pull request ID").requiredOption("--comment-id <id>", "Comment ID").option("--version <n>", "Comment version", "0").action(async (opts) => {
|
|
1220
|
+
const start = Date.now();
|
|
1221
|
+
try {
|
|
1222
|
+
const { client, project, repo } = getClient2(program2);
|
|
1223
|
+
await client.deleteComment(project, repo, parseInt(opts.pr, 10), parseInt(opts.commentId, 10), parseInt(opts.version ?? "0", 10));
|
|
1224
|
+
success({ deleted: true }, "bitbucket", "delete-comment", start);
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
fail(err, "bitbucket", "delete-comment", start);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
bb.command("diff").description("Get unified diff for a pull request").requiredOption("--pr <pr-id>", "Pull request ID").option("--file <path>", "Limit diff to a specific file").option("--context-lines <n>", "Lines of context around changes").action(async (opts) => {
|
|
1230
|
+
const start = Date.now();
|
|
1231
|
+
try {
|
|
1232
|
+
const { client, project, repo } = getClient2(program2);
|
|
1233
|
+
const contextLines = opts.contextLines ? parseInt(opts.contextLines, 10) : void 0;
|
|
1234
|
+
const diff = await client.getDiff(project, repo, parseInt(opts.pr, 10), opts.file, contextLines);
|
|
1235
|
+
success({ diff }, "bitbucket", "diff", start);
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
fail(err, "bitbucket", "diff", start);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
bb.command("list-files").description("List files changed in a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1241
|
+
const start = Date.now();
|
|
1242
|
+
try {
|
|
1243
|
+
const { client, project, repo } = getClient2(program2);
|
|
1244
|
+
const data = await client.listFiles(project, repo, parseInt(opts.pr, 10));
|
|
1245
|
+
success(data, "bitbucket", "list-files", start);
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
fail(err, "bitbucket", "list-files", start);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
bb.command("approve").description("Approve a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1251
|
+
const start = Date.now();
|
|
1252
|
+
try {
|
|
1253
|
+
const { client, project, repo } = getClient2(program2);
|
|
1254
|
+
const data = await client.approvePR(project, repo, parseInt(opts.pr, 10));
|
|
1255
|
+
success(data, "bitbucket", "approve", start);
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
fail(err, "bitbucket", "approve", start);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
bb.command("unapprove").description("Remove approval from a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1261
|
+
const start = Date.now();
|
|
1262
|
+
try {
|
|
1263
|
+
const { client, project, repo } = getClient2(program2);
|
|
1264
|
+
const data = await client.unapprovePR(project, repo, parseInt(opts.pr, 10));
|
|
1265
|
+
success(data, "bitbucket", "unapprove", start);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
fail(err, "bitbucket", "unapprove", start);
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
bb.command("needs-work").description("Mark a pull request as needs-work").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1271
|
+
const start = Date.now();
|
|
1272
|
+
try {
|
|
1273
|
+
const { client, project, repo } = getClient2(program2);
|
|
1274
|
+
const data = await client.needsWorkPR(project, repo, parseInt(opts.pr, 10));
|
|
1275
|
+
success(data, "bitbucket", "needs-work", start);
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
fail(err, "bitbucket", "needs-work", start);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
bb.command("list-reviewers").description("List reviewers of a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1281
|
+
const start = Date.now();
|
|
1282
|
+
try {
|
|
1283
|
+
const { client, project, repo } = getClient2(program2);
|
|
1284
|
+
const data = await client.listReviewers(project, repo, parseInt(opts.pr, 10));
|
|
1285
|
+
success(data, "bitbucket", "list-reviewers", start);
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
fail(err, "bitbucket", "list-reviewers", start);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
bb.command("list-builds").description("List build statuses for a pull request").requiredOption("--pr <pr-id>", "Pull request ID").action(async (opts) => {
|
|
1291
|
+
const start = Date.now();
|
|
1292
|
+
try {
|
|
1293
|
+
const { client, project, repo } = getClient2(program2);
|
|
1294
|
+
const data = await client.listBuilds(project, repo, parseInt(opts.pr, 10));
|
|
1295
|
+
success(data, "bitbucket", "list-builds", start);
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
fail(err, "bitbucket", "list-builds", start);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
bb.command("get-build-status").description("Get build status for a commit SHA").requiredOption("--commit <sha>", "Commit SHA").action(async (opts) => {
|
|
1301
|
+
const start = Date.now();
|
|
1302
|
+
try {
|
|
1303
|
+
const { client } = getClient2(program2);
|
|
1304
|
+
const data = await client.getBuildStatus(opts.commit);
|
|
1305
|
+
success(data, "bitbucket", "get-build-status", start);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
fail(err, "bitbucket", "get-build-status", start);
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/services/confluence/commands.ts
|
|
1313
|
+
function registerConfluenceCommands(program2) {
|
|
1314
|
+
program2.command("confluence").description("Confluence operations").action(() => {
|
|
1315
|
+
success(
|
|
1316
|
+
{ message: "Coming soon \u2014 the nightmare never ends." },
|
|
1317
|
+
"confluence",
|
|
1318
|
+
"stub",
|
|
1319
|
+
Date.now()
|
|
1320
|
+
);
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/services/sonar/commands.ts
|
|
1325
|
+
function registerSonarCommands(program2) {
|
|
1326
|
+
program2.command("sonar").description("SonarQube operations").action(() => {
|
|
1327
|
+
success(
|
|
1328
|
+
{ message: "Coming soon \u2014 the nightmare never ends." },
|
|
1329
|
+
"sonar",
|
|
1330
|
+
"stub",
|
|
1331
|
+
Date.now()
|
|
1332
|
+
);
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/services/artifactory/commands.ts
|
|
1337
|
+
function registerArtifactoryCommands(program2) {
|
|
1338
|
+
program2.command("artifactory").description("Artifactory operations").action(() => {
|
|
1339
|
+
success(
|
|
1340
|
+
{ message: "Coming soon \u2014 the nightmare never ends." },
|
|
1341
|
+
"artifactory",
|
|
1342
|
+
"stub",
|
|
1343
|
+
Date.now()
|
|
1344
|
+
);
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/services/config/commands.ts
|
|
1349
|
+
import { input, password, confirm } from "@inquirer/prompts";
|
|
1350
|
+
import fs2 from "fs";
|
|
1351
|
+
function registerConfigCommands(program2) {
|
|
1352
|
+
const config = program2.command("config").description("Manage pncli configuration");
|
|
1353
|
+
config.command("init").description("Interactive setup wizard").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
|
|
1354
|
+
const start = Date.now();
|
|
1355
|
+
try {
|
|
1356
|
+
if (opts.repo) {
|
|
1357
|
+
await initRepoConfig(start);
|
|
1358
|
+
} else {
|
|
1359
|
+
await initGlobalConfig(start);
|
|
1360
|
+
}
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
if (err instanceof Error && err.message.includes("User force closed")) {
|
|
1363
|
+
process.stderr.write("\nSetup cancelled.\n");
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
fail(err, "config", "init", start);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
config.command("show").description("Print fully resolved config (PATs masked)").action(() => {
|
|
1370
|
+
const start = Date.now();
|
|
1371
|
+
try {
|
|
1372
|
+
const resolved = loadConfig();
|
|
1373
|
+
success(maskConfig(resolved), "config", "show", start);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
fail(err, "config", "show", start);
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
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) => {
|
|
1379
|
+
const start = Date.now();
|
|
1380
|
+
try {
|
|
1381
|
+
const opts = program2.optsWithGlobals();
|
|
1382
|
+
setConfigValue(key, value, opts.config);
|
|
1383
|
+
success({ key, value }, "config", "set", start);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
fail(err, "config", "set", start);
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
config.command("test").description("Test connectivity to configured services").action(() => {
|
|
1389
|
+
const start = Date.now();
|
|
1390
|
+
success(
|
|
1391
|
+
{ message: "Service connectivity test available after Phase 2." },
|
|
1392
|
+
"config",
|
|
1393
|
+
"test",
|
|
1394
|
+
start
|
|
1395
|
+
);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
async function initGlobalConfig(start) {
|
|
1399
|
+
process.stderr.write("pncli config init \u2014 Global configuration\n\n");
|
|
1400
|
+
const jiraBaseUrl = await input({
|
|
1401
|
+
message: "Jira base URL (e.g. https://your-domain.atlassian.net):",
|
|
1402
|
+
default: ""
|
|
1403
|
+
});
|
|
1404
|
+
const jiraEmail = await input({
|
|
1405
|
+
message: "Jira email address:",
|
|
1406
|
+
default: ""
|
|
1407
|
+
});
|
|
1408
|
+
const jiraApiToken = await password({
|
|
1409
|
+
message: "Jira API token:"
|
|
1410
|
+
});
|
|
1411
|
+
const bitbucketBaseUrl = await input({
|
|
1412
|
+
message: "Bitbucket Server base URL (e.g. https://bitbucket.your-company.com):",
|
|
1413
|
+
default: ""
|
|
1414
|
+
});
|
|
1415
|
+
const bitbucketPat = await password({
|
|
1416
|
+
message: "Bitbucket personal access token:"
|
|
1417
|
+
});
|
|
1418
|
+
const jiraProject = await input({
|
|
1419
|
+
message: "Default Jira project key (optional):",
|
|
1420
|
+
default: ""
|
|
1421
|
+
});
|
|
1422
|
+
const confirmed = await confirm({
|
|
1423
|
+
message: "Write config to ~/.pncli/config.json?",
|
|
1424
|
+
default: true
|
|
1425
|
+
});
|
|
1426
|
+
if (!confirmed) {
|
|
1427
|
+
process.stderr.write("Aborted.\n");
|
|
1428
|
+
process.exit(0);
|
|
1429
|
+
}
|
|
1430
|
+
writeGlobalConfig({
|
|
1431
|
+
jira: {
|
|
1432
|
+
baseUrl: jiraBaseUrl || void 0,
|
|
1433
|
+
email: jiraEmail || void 0,
|
|
1434
|
+
apiToken: jiraApiToken || void 0
|
|
1435
|
+
},
|
|
1436
|
+
bitbucket: {
|
|
1437
|
+
baseUrl: bitbucketBaseUrl || void 0,
|
|
1438
|
+
pat: bitbucketPat || void 0
|
|
1439
|
+
},
|
|
1440
|
+
defaults: {
|
|
1441
|
+
jira: {
|
|
1442
|
+
project: jiraProject || void 0
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
const configPath = getGlobalConfigPath();
|
|
1447
|
+
warn(`Config written to ${configPath}`);
|
|
1448
|
+
success({ written: configPath }, "config", "init", start);
|
|
1449
|
+
}
|
|
1450
|
+
async function initRepoConfig(start) {
|
|
1451
|
+
process.stderr.write("pncli config init --repo \u2014 Repo configuration\n\n");
|
|
1452
|
+
const jiraProject = await input({
|
|
1453
|
+
message: "Jira project key (e.g. ACME):",
|
|
1454
|
+
default: ""
|
|
1455
|
+
});
|
|
1456
|
+
const jiraIssueType = await input({
|
|
1457
|
+
message: "Default issue type:",
|
|
1458
|
+
default: "Story"
|
|
1459
|
+
});
|
|
1460
|
+
const jiraPriority = await input({
|
|
1461
|
+
message: "Default priority:",
|
|
1462
|
+
default: "Medium"
|
|
1463
|
+
});
|
|
1464
|
+
const targetBranch = await input({
|
|
1465
|
+
message: "Default target branch for PRs:",
|
|
1466
|
+
default: "main"
|
|
1467
|
+
});
|
|
1468
|
+
const confirmed = await confirm({
|
|
1469
|
+
message: "Write config to .pncli.json in repo root?",
|
|
1470
|
+
default: true
|
|
1471
|
+
});
|
|
1472
|
+
if (!confirmed) {
|
|
1473
|
+
process.stderr.write("Aborted.\n");
|
|
1474
|
+
process.exit(0);
|
|
1475
|
+
}
|
|
1476
|
+
if (fs2.existsSync(".pncli.json")) {
|
|
1477
|
+
const overwrite = await confirm({
|
|
1478
|
+
message: ".pncli.json already exists. Overwrite?",
|
|
1479
|
+
default: false
|
|
1480
|
+
});
|
|
1481
|
+
if (!overwrite) {
|
|
1482
|
+
process.stderr.write("Aborted.\n");
|
|
1483
|
+
process.exit(0);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
writeRepoConfig({
|
|
1487
|
+
defaults: {
|
|
1488
|
+
jira: {
|
|
1489
|
+
project: jiraProject || void 0,
|
|
1490
|
+
issueType: jiraIssueType || void 0,
|
|
1491
|
+
priority: jiraPriority || void 0
|
|
1492
|
+
},
|
|
1493
|
+
bitbucket: {
|
|
1494
|
+
targetBranch: targetBranch || void 0
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
success({ written: ".pncli.json" }, "config", "init", start);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/cli.ts
|
|
1502
|
+
var require2 = createRequire(import.meta.url);
|
|
1503
|
+
var pkg = require2("../package.json");
|
|
1504
|
+
var TAGLINE = "One command does what three meetings couldn't.";
|
|
1505
|
+
var program = new Command();
|
|
1506
|
+
program.name("pncli").description(`The Paperwork Nightmare CLI \u2014 ${TAGLINE}`).version(`${pkg.version} \u2014 ${TAGLINE}`, "-v, --version").option("--pretty", "Human-readable formatted output", false).option("--verbose", "Include full response metadata", false).option("--dry-run", "Print API requests without executing", false).option("--config <path>", "Override global config file location");
|
|
1507
|
+
program.hook("preAction", (thisCommand) => {
|
|
1508
|
+
const opts = thisCommand.optsWithGlobals();
|
|
1509
|
+
setGlobalOptions({
|
|
1510
|
+
pretty: Boolean(opts.pretty),
|
|
1511
|
+
verbose: Boolean(opts.verbose)
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
registerGitCommands(program);
|
|
1515
|
+
registerJiraCommands(program);
|
|
1516
|
+
registerBitbucketCommands(program);
|
|
1517
|
+
registerConfluenceCommands(program);
|
|
1518
|
+
registerSonarCommands(program);
|
|
1519
|
+
registerArtifactoryCommands(program);
|
|
1520
|
+
registerConfigCommands(program);
|
|
1521
|
+
program.addHelpText("after", `
|
|
1522
|
+
Services:
|
|
1523
|
+
git Local git operations (status, diff, log, branch)
|
|
1524
|
+
jira Jira Data Cloud (coming soon)
|
|
1525
|
+
bitbucket Bitbucket Server (coming soon)
|
|
1526
|
+
confluence Confluence (coming soon)
|
|
1527
|
+
sonar SonarQube (coming soon)
|
|
1528
|
+
artifactory Artifactory (coming soon)
|
|
1529
|
+
config Manage pncli configuration
|
|
1530
|
+
`);
|
|
1531
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1532
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
1533
|
+
`);
|
|
1534
|
+
process.exit(1);
|
|
1535
|
+
});
|
|
1536
|
+
//# sourceMappingURL=cli.js.map
|