azdo-cli 0.12.0 → 0.14.0-032-fix-code-generics.576
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 +7 -1
- package/dist/index.js +334 -17
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
17
17
|
- Check branch pull request status, open PRs to `develop`, list PR comment threads for any PR (`--pr-number`), and resolve/reopen threads from the CLI (`pr`)
|
|
18
18
|
- Persist org/project/default fields in local config (`config`)
|
|
19
19
|
- List all fields of a work item (`list-fields`)
|
|
20
|
-
- Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. See [docs/authentication.md](docs/authentication.md).
|
|
20
|
+
- Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. See [docs/authentication.md](docs/authentication.md).
|
|
21
|
+
- Trace all HTTP requests to a local file with `--trace <filepath>` (sensitive headers and tokens are automatically redacted).
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -62,6 +63,11 @@ azdo pr status # PR checks (status + branch policies +
|
|
|
62
63
|
azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
|
|
63
64
|
azdo pr comment-reopen 17 --pr-number 64
|
|
64
65
|
|
|
66
|
+
# Reply to a PR comment thread
|
|
67
|
+
azdo pr comments reply 148 "Great suggestion, I'll address it." # human-readable output
|
|
68
|
+
azdo pr comments reply 148 "Done." --pr-number 64 --json # JSON: { pullRequestId, threadId, commentId, content }
|
|
69
|
+
azdo pr comment-reply 148 "Done." --pr-number 64 # flat alias, identical behaviour
|
|
70
|
+
|
|
65
71
|
# Pipelines — list, inspect runs, wait (exit code = result), start
|
|
66
72
|
azdo pipeline list --filter ci
|
|
67
73
|
azdo pipeline get-runs 12 --branch develop --limit 1
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,78 @@ var version = pkg.version;
|
|
|
51
51
|
// src/commands/get-item.ts
|
|
52
52
|
import { Command } from "commander";
|
|
53
53
|
|
|
54
|
+
// src/services/trace-writer.ts
|
|
55
|
+
import { openSync, writeSync, closeSync } from "fs";
|
|
56
|
+
import { platform } from "os";
|
|
57
|
+
var REDACTED = "[REDACTED]";
|
|
58
|
+
var SENSITIVE_HEADER = /^(authorization|x-.*token)$/i;
|
|
59
|
+
var SENSITIVE_QUERY_PARAM = /^(token|pat)$/i;
|
|
60
|
+
var SENSITIVE_BODY_FIELD = /^(token|accessToken|pat)$/;
|
|
61
|
+
function redactHeaders(headers) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
64
|
+
out[key] = SENSITIVE_HEADER.test(key) ? REDACTED : value;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function redactUrl(url) {
|
|
69
|
+
try {
|
|
70
|
+
const u = new URL(url);
|
|
71
|
+
for (const [key] of u.searchParams.entries()) {
|
|
72
|
+
if (SENSITIVE_QUERY_PARAM.test(key)) {
|
|
73
|
+
u.searchParams.set(key, REDACTED);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return u.toString();
|
|
77
|
+
} catch {
|
|
78
|
+
return url;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function redactBody(body) {
|
|
82
|
+
if (body === null) return null;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(body);
|
|
85
|
+
let changed = false;
|
|
86
|
+
const redacted = { ...parsed };
|
|
87
|
+
for (const key of Object.keys(parsed)) {
|
|
88
|
+
if (SENSITIVE_BODY_FIELD.test(key)) {
|
|
89
|
+
redacted[key] = REDACTED;
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return changed ? JSON.stringify(redacted) : body;
|
|
94
|
+
} catch {
|
|
95
|
+
return body;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var TraceWriter = class {
|
|
99
|
+
fd;
|
|
100
|
+
constructor(filepath) {
|
|
101
|
+
const mode = platform() === "win32" ? void 0 : 384;
|
|
102
|
+
this.fd = openSync(filepath, "a", mode);
|
|
103
|
+
}
|
|
104
|
+
append(entry) {
|
|
105
|
+
const line = JSON.stringify(entry) + "\n\n";
|
|
106
|
+
writeSync(this.fd, line);
|
|
107
|
+
}
|
|
108
|
+
close() {
|
|
109
|
+
closeSync(this.fd);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var activeWriter = null;
|
|
113
|
+
function initTraceWriter(filepath) {
|
|
114
|
+
try {
|
|
115
|
+
activeWriter = new TraceWriter(filepath);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
process.stderr.write(`Warning: could not open trace file "${filepath}": ${msg}
|
|
119
|
+
`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function getActiveTraceWriter() {
|
|
123
|
+
return activeWriter;
|
|
124
|
+
}
|
|
125
|
+
|
|
54
126
|
// src/services/azdo-client.ts
|
|
55
127
|
var DEFAULT_FIELDS = [
|
|
56
128
|
"System.Title",
|
|
@@ -74,12 +146,48 @@ function authHeaders(credentialOrPat) {
|
|
|
74
146
|
const token = Buffer.from(`:${credentialOrPat.pat}`).toString("base64");
|
|
75
147
|
return { Authorization: `Basic ${token}` };
|
|
76
148
|
}
|
|
149
|
+
async function fetchRaw(url, init) {
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await fetch(url, init);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw new Error(`NETWORK_ERROR: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
155
|
+
}
|
|
156
|
+
const body = await response.text();
|
|
157
|
+
return { status: response.status, body };
|
|
158
|
+
}
|
|
77
159
|
async function fetchWithErrors(url, init) {
|
|
160
|
+
const writer = getActiveTraceWriter();
|
|
78
161
|
let response;
|
|
79
162
|
try {
|
|
80
163
|
response = await fetch(url, init);
|
|
81
|
-
} catch {
|
|
82
|
-
throw new Error("NETWORK_ERROR");
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new Error("NETWORK_ERROR", { cause: err });
|
|
166
|
+
}
|
|
167
|
+
if (writer) {
|
|
168
|
+
const reqHeaders = redactHeaders(init.headers ?? {});
|
|
169
|
+
const reqBody = typeof init.body === "string" ? redactBody(init.body) : null;
|
|
170
|
+
let responseBody = "";
|
|
171
|
+
const clone = response.clone();
|
|
172
|
+
try {
|
|
173
|
+
responseBody = await clone.text();
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
const respHeaders = {};
|
|
177
|
+
response.headers.forEach((v, k) => {
|
|
178
|
+
respHeaders[k] = v;
|
|
179
|
+
});
|
|
180
|
+
const entry = {
|
|
181
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
182
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
183
|
+
url: redactUrl(url),
|
|
184
|
+
requestHeaders: reqHeaders,
|
|
185
|
+
requestBody: reqBody ?? null,
|
|
186
|
+
responseStatus: response.status,
|
|
187
|
+
responseHeaders: respHeaders,
|
|
188
|
+
responseBody
|
|
189
|
+
};
|
|
190
|
+
writer.append(entry);
|
|
83
191
|
}
|
|
84
192
|
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
85
193
|
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
@@ -624,7 +732,7 @@ function findDotEnvPat(startDir = process.cwd()) {
|
|
|
624
732
|
if (existsSync(envFile)) {
|
|
625
733
|
const contents = readFileSync2(envFile, "utf8");
|
|
626
734
|
for (const line of contents.split("\n")) {
|
|
627
|
-
const match = line.match(/^AZDO_PAT\s
|
|
735
|
+
const match = line.match(/^AZDO_PAT\s*=([^\n\r]+)$/);
|
|
628
736
|
if (match) {
|
|
629
737
|
const value = match[1].trim().replace(/^["']|["']$/g, "");
|
|
630
738
|
if (value.length > 0) return value;
|
|
@@ -867,11 +975,18 @@ function parseSingleRemoteLine(line) {
|
|
|
867
975
|
const url = urlEnd === -1 ? afterTab : afterTab.slice(0, urlEnd);
|
|
868
976
|
return { remoteName, url };
|
|
869
977
|
}
|
|
978
|
+
function decodePctSegment(segment) {
|
|
979
|
+
try {
|
|
980
|
+
return decodeURIComponent(segment);
|
|
981
|
+
} catch {
|
|
982
|
+
return segment;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
870
985
|
function matchAzdoRemote(remoteName, url) {
|
|
871
986
|
for (const pattern of patterns) {
|
|
872
987
|
const match = pattern.exec(url);
|
|
873
988
|
if (!match) continue;
|
|
874
|
-
const project = match[2];
|
|
989
|
+
const project = decodePctSegment(match[2]);
|
|
875
990
|
if (/^DefaultCollection$/i.test(project)) return null;
|
|
876
991
|
return { remoteName, org: match[1], project, hasEmbeddedSecret: httpsEmbeddedSecret.test(url) };
|
|
877
992
|
}
|
|
@@ -1115,14 +1230,34 @@ function isHtml(content) {
|
|
|
1115
1230
|
}
|
|
1116
1231
|
|
|
1117
1232
|
// src/services/md-convert.ts
|
|
1233
|
+
var PLT = "@@PLT@@";
|
|
1234
|
+
var PGT = "@@PGT@@";
|
|
1235
|
+
function escapeAnglesInCodeElements(html) {
|
|
1236
|
+
return html.replace(/<code([^>]*)>([\s\S]*?)<\/code>/gi, (_, attrs, content) => {
|
|
1237
|
+
const safe = content.split("<").join(PLT).split(">").join(PGT).replaceAll("<", "<").replaceAll(">", ">").split(PLT).join("<").split(PGT).join(">");
|
|
1238
|
+
return `<code${attrs}>${safe}</code>`;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function escapeAnglesInMarkdownCodeSpans(markdown) {
|
|
1242
|
+
return markdown.replace(/(?<!`)`([^`\n]+)`(?!`)/g, (_, inner) => {
|
|
1243
|
+
const safe = inner.split("<").join(PLT).split(">").join(PGT).replaceAll("<", "<").replaceAll(">", ">").split(PLT).join("<").split(PGT).join(">");
|
|
1244
|
+
return "`" + safe + "`";
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
function decodeEntitiesInMarkdownCodeSpans(text) {
|
|
1248
|
+
return text.replace(/(?<!`)`([^`\n]+)`(?!`)/g, (_, inner) => {
|
|
1249
|
+
const decoded = inner.replaceAll("<", "<").replaceAll(">", ">");
|
|
1250
|
+
return "`" + decoded + "`";
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1118
1253
|
function htmlToMarkdown(html) {
|
|
1119
|
-
return NodeHtmlMarkdown.translate(html);
|
|
1254
|
+
return NodeHtmlMarkdown.translate(escapeAnglesInCodeElements(html));
|
|
1120
1255
|
}
|
|
1121
1256
|
function toMarkdown(content) {
|
|
1122
1257
|
if (isHtml(content)) {
|
|
1123
1258
|
return htmlToMarkdown(content);
|
|
1124
1259
|
}
|
|
1125
|
-
return content;
|
|
1260
|
+
return decodeEntitiesInMarkdownCodeSpans(content);
|
|
1126
1261
|
}
|
|
1127
1262
|
|
|
1128
1263
|
// src/services/command-helpers.ts
|
|
@@ -1561,6 +1696,79 @@ function createClearPatCommand() {
|
|
|
1561
1696
|
|
|
1562
1697
|
// src/commands/auth.ts
|
|
1563
1698
|
import { Command as Command3 } from "commander";
|
|
1699
|
+
|
|
1700
|
+
// src/services/auth-diagnostics.ts
|
|
1701
|
+
async function runConnectivityTest(org, cred) {
|
|
1702
|
+
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/projects?api-version=7.1&$top=1`;
|
|
1703
|
+
let result;
|
|
1704
|
+
try {
|
|
1705
|
+
result = await fetchRaw(url, { headers: authHeaders(cred) });
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
1708
|
+
}
|
|
1709
|
+
if (result.status >= 200 && result.status < 300) {
|
|
1710
|
+
return { status: "ok", error: null };
|
|
1711
|
+
}
|
|
1712
|
+
let error = `HTTP ${result.status}`;
|
|
1713
|
+
try {
|
|
1714
|
+
const parsed = JSON.parse(result.body);
|
|
1715
|
+
if (typeof parsed.message === "string" && parsed.message.trim() !== "") {
|
|
1716
|
+
error = parsed.message.trim();
|
|
1717
|
+
}
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
return { status: "failed", error };
|
|
1721
|
+
}
|
|
1722
|
+
async function diagnoseAuth(org, project, resolveCredential) {
|
|
1723
|
+
const cred = await resolveCredential(org);
|
|
1724
|
+
if (cred === null) {
|
|
1725
|
+
return {
|
|
1726
|
+
authType: "none",
|
|
1727
|
+
credentialSource: null,
|
|
1728
|
+
org,
|
|
1729
|
+
project,
|
|
1730
|
+
connectivityStatus: "no-credentials",
|
|
1731
|
+
connectivityError: null
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
const connectivity = await runConnectivityTest(org, cred);
|
|
1735
|
+
const envVarName = process.env.AZDO_PAT ? "AZDO_PAT" : "dotenv";
|
|
1736
|
+
const sourceLabel = cred.source === "env" ? `env:${envVarName}` : "credential-store";
|
|
1737
|
+
return {
|
|
1738
|
+
authType: cred.kind ?? "pat",
|
|
1739
|
+
credentialSource: sourceLabel,
|
|
1740
|
+
org,
|
|
1741
|
+
project,
|
|
1742
|
+
connectivityStatus: connectivity.status,
|
|
1743
|
+
connectivityError: connectivity.error
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function formatDiagnosticReport(report, json) {
|
|
1747
|
+
if (json) {
|
|
1748
|
+
return JSON.stringify(report, null, 2);
|
|
1749
|
+
}
|
|
1750
|
+
let connectivityLine;
|
|
1751
|
+
if (report.connectivityStatus === "ok") {
|
|
1752
|
+
connectivityLine = "OK";
|
|
1753
|
+
} else if (report.connectivityStatus === "no-credentials") {
|
|
1754
|
+
connectivityLine = "no credentials found";
|
|
1755
|
+
} else {
|
|
1756
|
+
connectivityLine = "FAILED";
|
|
1757
|
+
}
|
|
1758
|
+
const lines = [
|
|
1759
|
+
`Auth type: ${report.authType}`,
|
|
1760
|
+
`Source: ${report.credentialSource ?? "(none)"}`,
|
|
1761
|
+
`Org: ${report.org}`,
|
|
1762
|
+
`Project: ${report.project ?? "(not set)"}`,
|
|
1763
|
+
`Connectivity: ${connectivityLine}`
|
|
1764
|
+
];
|
|
1765
|
+
if (report.connectivityStatus === "failed" && report.connectivityError !== null) {
|
|
1766
|
+
lines.push(`Error: ${report.connectivityError}`);
|
|
1767
|
+
}
|
|
1768
|
+
return lines.join("\n");
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// src/commands/auth.ts
|
|
1564
1772
|
async function readStdinToString() {
|
|
1565
1773
|
const chunks = [];
|
|
1566
1774
|
for await (const chunk of process.stdin) {
|
|
@@ -1982,6 +2190,25 @@ Note: \`azdo auth\` (no subcommand) preserves the legacy PAT-prompt entry point;
|
|
|
1982
2190
|
const globals = logoutCmd.optsWithGlobals();
|
|
1983
2191
|
await handleLogout(options, globals.org);
|
|
1984
2192
|
});
|
|
2193
|
+
const diagnoseCmd = command.command("diagnose").description("Show auth type, credential source, org, and live connectivity test result").option("--org <name>", "Azure DevOps organization (overrides context resolution)").option("--project <name>", "Azure DevOps project (optional context)").option("--json", "emit JSON instead of human-readable text", false);
|
|
2194
|
+
diagnoseCmd.action(async (options) => {
|
|
2195
|
+
const globals = diagnoseCmd.optsWithGlobals();
|
|
2196
|
+
const orgName = options.org ?? globals.org;
|
|
2197
|
+
const resolved = resolveOrg({ org: orgName });
|
|
2198
|
+
if (!resolved) {
|
|
2199
|
+
process.stderr.write(`${formatResolutionError()}
|
|
2200
|
+
`);
|
|
2201
|
+
process.exitCode = 1;
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const report = await diagnoseAuth(resolved.org, options.project ?? null, resolveAuthCredential);
|
|
2205
|
+
const output = formatDiagnosticReport(report, options.json ?? false);
|
|
2206
|
+
process.stdout.write(`${output}
|
|
2207
|
+
`);
|
|
2208
|
+
if (report.connectivityStatus === "failed") {
|
|
2209
|
+
process.exitCode = 1;
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
1985
2212
|
return command;
|
|
1986
2213
|
}
|
|
1987
2214
|
|
|
@@ -2468,8 +2695,9 @@ function createSetMdFieldCommand() {
|
|
|
2468
2695
|
try {
|
|
2469
2696
|
context = resolveContext(options);
|
|
2470
2697
|
const credential = await requireAuthCredential(context.org);
|
|
2698
|
+
const safeContent = escapeAnglesInMarkdownCodeSpans(content);
|
|
2471
2699
|
const operations = [
|
|
2472
|
-
{ op: "add", path: `/fields/${field}`, value:
|
|
2700
|
+
{ op: "add", path: `/fields/${field}`, value: safeContent },
|
|
2473
2701
|
{ op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
|
|
2474
2702
|
];
|
|
2475
2703
|
const result = await updateWorkItem(context, id, credential, field, operations);
|
|
@@ -3188,6 +3416,30 @@ async function getPullRequestThreads(context, repo, cred, prId) {
|
|
|
3188
3416
|
const data = await readJsonResponse(response);
|
|
3189
3417
|
return data.value.map(mapThread).filter((thread) => thread !== null);
|
|
3190
3418
|
}
|
|
3419
|
+
function buildThreadCommentUrl(context, repo, prId, threadId) {
|
|
3420
|
+
const url = new URL(
|
|
3421
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads/${threadId}/comments`
|
|
3422
|
+
);
|
|
3423
|
+
url.searchParams.set("api-version", "7.1");
|
|
3424
|
+
return url;
|
|
3425
|
+
}
|
|
3426
|
+
async function postThreadComment(context, repo, cred, prId, threadId, content) {
|
|
3427
|
+
const response = await fetchWithErrors(buildThreadCommentUrl(context, repo, prId, threadId).toString(), {
|
|
3428
|
+
method: "POST",
|
|
3429
|
+
headers: {
|
|
3430
|
+
...authHeaders(cred),
|
|
3431
|
+
"Content-Type": "application/json"
|
|
3432
|
+
},
|
|
3433
|
+
body: JSON.stringify({ content, parentCommentId: 0, commentType: 1 })
|
|
3434
|
+
});
|
|
3435
|
+
const data = await readJsonResponse(response);
|
|
3436
|
+
return {
|
|
3437
|
+
id: data.id,
|
|
3438
|
+
author: data.author?.displayName ?? null,
|
|
3439
|
+
content: data.content ?? content,
|
|
3440
|
+
publishedAt: data.publishedDate ?? null
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3191
3443
|
|
|
3192
3444
|
// src/commands/pr.ts
|
|
3193
3445
|
function parsePositivePrNumber(raw) {
|
|
@@ -3205,7 +3457,8 @@ function autoDetectZeroMatch(branch) {
|
|
|
3205
3457
|
return `No open pull request matches branch ${branch}. Pass --pr-number to target a specific PR, or push the branch and open a pull request.`;
|
|
3206
3458
|
}
|
|
3207
3459
|
function autoDetectMultiMatch(branch, ids) {
|
|
3208
|
-
|
|
3460
|
+
const idList = ids.map((id) => `#${id}`).join(", ");
|
|
3461
|
+
return `Multiple open pull requests match branch ${branch}: ${idList}. Re-run with --pr-number to choose.`;
|
|
3209
3462
|
}
|
|
3210
3463
|
function writeContractError(line) {
|
|
3211
3464
|
process.stderr.write(`${line}
|
|
@@ -3538,6 +3791,7 @@ function createPrCommentsCommand() {
|
|
|
3538
3791
|
handlePrCommandError(err, context, "read");
|
|
3539
3792
|
}
|
|
3540
3793
|
});
|
|
3794
|
+
command.addCommand(createPrCommentsReplyCommand());
|
|
3541
3795
|
return command;
|
|
3542
3796
|
}
|
|
3543
3797
|
async function resolveThreadTarget(threadIdRaw, options) {
|
|
@@ -3656,6 +3910,64 @@ function createPrCommentReopenCommand() {
|
|
|
3656
3910
|
});
|
|
3657
3911
|
return command;
|
|
3658
3912
|
}
|
|
3913
|
+
async function runCommentReply(threadIdRaw, text, options) {
|
|
3914
|
+
let context;
|
|
3915
|
+
try {
|
|
3916
|
+
const trimmedText = text.trim();
|
|
3917
|
+
if (!trimmedText) {
|
|
3918
|
+
writeError("Reply text must not be empty.");
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
const target = await resolveThreadTarget(threadIdRaw, options);
|
|
3922
|
+
if (target === null) {
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
context = target.context;
|
|
3926
|
+
const threads = await getPullRequestThreads(target.context, target.repo, target.pat, target.pullRequest.id);
|
|
3927
|
+
const thread = threads.find((t) => t.id === target.threadId);
|
|
3928
|
+
if (!thread) {
|
|
3929
|
+
writeError(`Thread #${target.threadId} not found on pull request #${target.pullRequest.id}.`);
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
const posted = await postThreadComment(
|
|
3933
|
+
target.context,
|
|
3934
|
+
target.repo,
|
|
3935
|
+
target.pat,
|
|
3936
|
+
target.pullRequest.id,
|
|
3937
|
+
target.threadId,
|
|
3938
|
+
trimmedText
|
|
3939
|
+
);
|
|
3940
|
+
const result = {
|
|
3941
|
+
pullRequestId: target.pullRequest.id,
|
|
3942
|
+
threadId: target.threadId,
|
|
3943
|
+
commentId: posted.id,
|
|
3944
|
+
content: posted.content
|
|
3945
|
+
};
|
|
3946
|
+
if (options.json) {
|
|
3947
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3948
|
+
`);
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
process.stdout.write(`Reply posted to thread #${target.threadId} on pull request #${target.pullRequest.id}.
|
|
3952
|
+
`);
|
|
3953
|
+
} catch (err) {
|
|
3954
|
+
handlePrCommandError(err, context, "write");
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
function createPrCommentsReplyCommand() {
|
|
3958
|
+
const command = new Command12("reply");
|
|
3959
|
+
configureUnwrappedHelp(command).description("Post a reply to a pull request comment thread").argument("<threadId>", "numeric id of the thread to reply to").argument("<text>", "text of the reply").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, options) => {
|
|
3960
|
+
await runCommentReply(threadIdRaw, text, options);
|
|
3961
|
+
});
|
|
3962
|
+
return command;
|
|
3963
|
+
}
|
|
3964
|
+
function createPrCommentReplyCommand() {
|
|
3965
|
+
const command = new Command12("comment-reply");
|
|
3966
|
+
configureUnwrappedHelp(command).description('Post a reply to a pull request comment thread (alias of "azdo pr comments reply")').argument("<threadId>", "numeric id of the thread to reply to").argument("<text>", "text of the reply").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (threadIdRaw, text, options) => {
|
|
3967
|
+
await runCommentReply(threadIdRaw, text, options);
|
|
3968
|
+
});
|
|
3969
|
+
return command;
|
|
3970
|
+
}
|
|
3659
3971
|
function createPrCommand() {
|
|
3660
3972
|
const command = new Command12("pr");
|
|
3661
3973
|
command.description("Manage Azure DevOps pull requests");
|
|
@@ -3664,6 +3976,7 @@ function createPrCommand() {
|
|
|
3664
3976
|
command.addCommand(createPrCommentsCommand());
|
|
3665
3977
|
command.addCommand(createPrCommentResolveCommand());
|
|
3666
3978
|
command.addCommand(createPrCommentReopenCommand());
|
|
3979
|
+
command.addCommand(createPrCommentReplyCommand());
|
|
3667
3980
|
return command;
|
|
3668
3981
|
}
|
|
3669
3982
|
|
|
@@ -4782,10 +5095,10 @@ async function addWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
4782
5095
|
const relType = await resolveRelationType(context, cred, type);
|
|
4783
5096
|
const workItem = await getWorkItemWithRelations(context, cred, id1);
|
|
4784
5097
|
const targetUrl = `https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/wit/workItems/${id2}`;
|
|
4785
|
-
const
|
|
4786
|
-
(r) => r.rel === relType.referenceName && r.url
|
|
5098
|
+
const exists = (workItem.relations ?? []).some(
|
|
5099
|
+
(r) => r.rel === relType.referenceName && parseTargetId(r.url) === id2
|
|
4787
5100
|
);
|
|
4788
|
-
if (
|
|
5101
|
+
if (exists) {
|
|
4789
5102
|
return { status: "already_exists", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
4790
5103
|
}
|
|
4791
5104
|
const patchUrl = buildWorkItemUrl2(context, id1);
|
|
@@ -5118,6 +5431,7 @@ process.stderr.on("error", exitOnEpipe);
|
|
|
5118
5431
|
var program = new Command17();
|
|
5119
5432
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
5120
5433
|
program.option("--no-update-check", "Skip the check for a newer published version");
|
|
5434
|
+
program.option("--trace <filepath>", "Append redacted HTTP request/response trace to a file (owner-read-only permissions)");
|
|
5121
5435
|
program.addCommand(createGetItemCommand());
|
|
5122
5436
|
program.addCommand(createAuthCommand());
|
|
5123
5437
|
program.addCommand(createClearPatCommand());
|
|
@@ -5135,16 +5449,19 @@ program.addCommand(createCommentsCommand());
|
|
|
5135
5449
|
program.addCommand(createDownloadAttachmentCommand());
|
|
5136
5450
|
program.addCommand(createRelationsCommand());
|
|
5137
5451
|
program.showHelpAfterError();
|
|
5452
|
+
program.hook("preAction", () => {
|
|
5453
|
+
const { trace } = program.opts();
|
|
5454
|
+
if (trace) {
|
|
5455
|
+
initTraceWriter(trace);
|
|
5456
|
+
}
|
|
5457
|
+
});
|
|
5138
5458
|
program.hook("postAction", async () => {
|
|
5139
5459
|
const notice = await getUpdateNotice({ enabled: program.opts().updateCheck });
|
|
5140
5460
|
if (notice) {
|
|
5141
5461
|
process.stderr.write(notice + "\n");
|
|
5142
5462
|
}
|
|
5143
5463
|
});
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
program.help();
|
|
5148
|
-
}
|
|
5464
|
+
await program.parseAsync();
|
|
5465
|
+
if (process.argv.length <= 2) {
|
|
5466
|
+
program.help();
|
|
5149
5467
|
}
|
|
5150
|
-
void main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0-032-fix-code-generics.576",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"lint": "eslint src/",
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"format": "prettier --check src/",
|
|
17
|
-
"test": "npm run build && vitest run tests/unit tests/integration",
|
|
18
|
-
"test:unit": "npm run build && vitest run tests/unit",
|
|
17
|
+
"test": "npm run typecheck && npm run lint && npm run build && vitest run tests/unit tests/integration",
|
|
18
|
+
"test:unit": "npm run typecheck && npm run lint && npm run build && vitest run tests/unit",
|
|
19
19
|
"test:integration": "npm run build && vitest run tests/integration",
|
|
20
20
|
"test:integration:full": "bash scripts/setup-keyring.sh && npm run test:integration"
|
|
21
21
|
},
|