azdo-cli 0.4.0 → 0.5.0-010-work-item-comments.149
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 +75 -11
- package/dist/index.js +208 -12
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
12
12
|
- Update work item state (`set-state`)
|
|
13
13
|
- Assign and unassign work items (`assign`)
|
|
14
14
|
- Set any work item field by reference name (`set-field`)
|
|
15
|
-
- Create or update
|
|
15
|
+
- Create or update work items from markdown documents (`upsert`)
|
|
16
|
+
- Read and post work item comments (`comments`)
|
|
16
17
|
- Read rich-text fields as markdown (`get-md-field`)
|
|
17
18
|
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
18
19
|
- Check branch pull request status, open PRs to `develop`, and review active comments (`pr`)
|
|
@@ -26,6 +27,20 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
26
27
|
npm install -g azdo-cli
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
## Utility Scripts
|
|
31
|
+
|
|
32
|
+
The repository also includes a helper script for syncing local `.env` entries into GitHub Actions secrets for the current repository:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
./scripts/sync-env-to-gh-secrets.zsh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It walks upward from the current directory until it finds a `.env`, then sets each valid `KEY=VALUE` entry with `gh secret set`. You can also limit the sync to selected keys:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
./scripts/sync-env-to-gh-secrets.zsh FOO BAR
|
|
42
|
+
```
|
|
43
|
+
|
|
29
44
|
## Authentication and Context Resolution
|
|
30
45
|
|
|
31
46
|
PAT resolution order:
|
|
@@ -51,8 +66,12 @@ azdo get-item 12345
|
|
|
51
66
|
# 3) Update state
|
|
52
67
|
azdo set-state 12345 "Active"
|
|
53
68
|
|
|
54
|
-
# 4) Create or update a
|
|
55
|
-
azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
69
|
+
# 4) Create or update a work item from markdown
|
|
70
|
+
azdo upsert --type "User Story" --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
71
|
+
|
|
72
|
+
# 5) Review work item discussion and post an update
|
|
73
|
+
azdo comments list 12345
|
|
74
|
+
azdo comments add 12345 "Investigating the root cause now."
|
|
56
75
|
```
|
|
57
76
|
|
|
58
77
|
## Command Cheat Sheet
|
|
@@ -63,7 +82,8 @@ azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
|
63
82
|
| `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
|
|
64
83
|
| `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
|
|
65
84
|
| `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
|
|
66
|
-
| `azdo upsert [id]` | Create or update a
|
|
85
|
+
| `azdo upsert [id]` | Create or update a work item from markdown | `--content`, `--file`, `--type`, `--json`, `--org`, `--project` |
|
|
86
|
+
| `azdo comments <subcommand>` | Read or add work item comments | `list`, `add`, `--json`, `--org`, `--project` |
|
|
67
87
|
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
68
88
|
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
69
89
|
| `azdo list-fields <id>` | List all fields of a work item | `--json`, `--org`, `--project` |
|
|
@@ -88,8 +108,6 @@ azdo get-item 12345 --fields "System.Tags,Microsoft.VSTS.Common.Priority"
|
|
|
88
108
|
# Convert rich text fields to markdown
|
|
89
109
|
azdo get-item 12345 --markdown
|
|
90
110
|
|
|
91
|
-
# Disable markdown even if config is on
|
|
92
|
-
azdo get-item 12345 --no-markdown
|
|
93
111
|
```
|
|
94
112
|
|
|
95
113
|
```bash
|
|
@@ -118,7 +136,7 @@ azdo list-fields 12345 --json
|
|
|
118
136
|
|
|
119
137
|
The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
|
|
120
138
|
|
|
121
|
-
1. `--markdown`
|
|
139
|
+
1. `--markdown` flag enables markdown for the current call
|
|
122
140
|
2. Config setting: `azdo config set markdown true`
|
|
123
141
|
3. Default: off (HTML stripped to plain text)
|
|
124
142
|
|
|
@@ -155,7 +173,7 @@ azdo pr comments
|
|
|
155
173
|
|
|
156
174
|
`azdo pr status`
|
|
157
175
|
|
|
158
|
-
- Lists
|
|
176
|
+
- Lists pull requests for the current branch
|
|
159
177
|
- Prints `No pull requests found for branch <branch>.` when no PRs exist
|
|
160
178
|
- Supports `--json` for machine-readable output
|
|
161
179
|
|
|
@@ -175,13 +193,48 @@ azdo pr comments
|
|
|
175
193
|
- Prints `Pull request #<id> has no active comments.` when the PR has no active comment threads
|
|
176
194
|
- Fails instead of guessing when no active PR or multiple active PRs exist
|
|
177
195
|
|
|
196
|
+
### Work Item Comment Commands
|
|
197
|
+
|
|
198
|
+
The `comments` command group works on a specific work item ID. It requires a PAT with `Work Items (read)` scope for listing comments and `Work Items (Read & Write)` to add a new comment.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Read the visible comment history for a work item
|
|
202
|
+
azdo comments list 12345
|
|
203
|
+
|
|
204
|
+
# Read the same history as JSON
|
|
205
|
+
azdo comments list 12345 --json
|
|
206
|
+
|
|
207
|
+
# Post a progress update
|
|
208
|
+
azdo comments add 12345 "Investigation complete. Working on the fix next."
|
|
209
|
+
|
|
210
|
+
# Post the update and return JSON
|
|
211
|
+
azdo comments add 12345 "Queued validation run." --json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`azdo comments list`
|
|
215
|
+
|
|
216
|
+
- Resolves the target work item directly from the provided ID
|
|
217
|
+
- Retrieves the full visible comment history and follows Azure DevOps pagination internally
|
|
218
|
+
- Prints comments newest first with comment ID, author, timestamp, and body text
|
|
219
|
+
- Prints `Work item #<id> has no comments.` when the work item has no visible comments
|
|
220
|
+
|
|
221
|
+
`azdo comments add`
|
|
222
|
+
|
|
223
|
+
- Requires a non-empty `<text>` positional argument
|
|
224
|
+
- Preserves the supplied comment text as the submitted body
|
|
225
|
+
- Prints `Added comment #<commentId> to work item #<id>` on success
|
|
226
|
+
- Fails locally before any API call when the text is empty or whitespace-only
|
|
227
|
+
|
|
178
228
|
## azdo upsert
|
|
179
229
|
|
|
180
|
-
`azdo upsert` accepts a single markdown
|
|
230
|
+
`azdo upsert` accepts a single markdown work-item document and either creates a new Azure DevOps work item or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place. Create mode defaults to `Task`, and `--type <work item type>` lets you create `Bug`, `User Story`, `Feature`, `Epic`, and other Azure DevOps work item types.
|
|
181
231
|
|
|
182
232
|
```bash
|
|
183
|
-
# Create from inline content
|
|
184
|
-
azdo upsert --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
233
|
+
# Create a Bug from inline content
|
|
234
|
+
azdo upsert --type Bug --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
235
|
+
|
|
236
|
+
# Preserve the default Task create behavior
|
|
237
|
+
azdo upsert --content $'---\nTitle: Follow-up task\nAssigned To: user@example.com\nState: New\n---'
|
|
185
238
|
|
|
186
239
|
# Update from a file
|
|
187
240
|
azdo upsert 12345 --file ./task-import.md
|
|
@@ -194,9 +247,16 @@ The command requires exactly one source flag:
|
|
|
194
247
|
|
|
195
248
|
- `azdo upsert [id] --content <markdown>`
|
|
196
249
|
- `azdo upsert [id] --file <path>`
|
|
250
|
+
- `azdo upsert --type <work-item-type> --content <markdown>`
|
|
197
251
|
|
|
198
252
|
If `--file` succeeds, the source file is deleted after the Azure DevOps write completes. If parsing, validation, or the API call fails, the file is preserved. If deletion fails after a successful write, the command still succeeds and prints a warning.
|
|
199
253
|
|
|
254
|
+
Type rules:
|
|
255
|
+
|
|
256
|
+
- `--type` is optional for create and defaults to `Task`.
|
|
257
|
+
- `--type` is only valid when creating a new work item.
|
|
258
|
+
- Human-readable and JSON success output include the resulting work item type.
|
|
259
|
+
|
|
200
260
|
### Task Document Format
|
|
201
261
|
|
|
202
262
|
The document starts with YAML front matter for scalar fields, followed by optional `##` heading sections for markdown rich-text fields.
|
|
@@ -245,6 +305,7 @@ Clear semantics:
|
|
|
245
305
|
{
|
|
246
306
|
"action": "created",
|
|
247
307
|
"id": 12345,
|
|
308
|
+
"workItemType": "User Story",
|
|
248
309
|
"fields": {
|
|
249
310
|
"System.Title": "Improve markdown import UX",
|
|
250
311
|
"System.Description": "Implement a single-command task import flow."
|
|
@@ -288,6 +349,9 @@ These commands support `--json` for machine-readable output:
|
|
|
288
349
|
- `assign`
|
|
289
350
|
- `set-field`
|
|
290
351
|
- `set-md-field`
|
|
352
|
+
- `upsert`
|
|
353
|
+
- `comments list|add`
|
|
354
|
+
- `pr status|open|comments`
|
|
291
355
|
- `config set|get|list|unset`
|
|
292
356
|
|
|
293
357
|
## Development
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -39,7 +39,15 @@ async function fetchWithErrors(url, init) {
|
|
|
39
39
|
}
|
|
40
40
|
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
41
41
|
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
42
|
-
if (response.status === 404)
|
|
42
|
+
if (response.status === 404) {
|
|
43
|
+
let detail = "";
|
|
44
|
+
try {
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
detail = ` | url=${url} | body=${body}`;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`NOT_FOUND${detail}`);
|
|
50
|
+
}
|
|
43
51
|
return response;
|
|
44
52
|
}
|
|
45
53
|
async function readResponseMessage(response) {
|
|
@@ -88,6 +96,42 @@ function writeHeaders(pat) {
|
|
|
88
96
|
"Content-Type": "application/json-patch+json"
|
|
89
97
|
};
|
|
90
98
|
}
|
|
99
|
+
function buildWorkItemCommentsListUrl(context, id, continuationToken) {
|
|
100
|
+
const url = new URL(
|
|
101
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workItems/${id}/comments`
|
|
102
|
+
);
|
|
103
|
+
url.searchParams.set("api-version", "7.1-preview.4");
|
|
104
|
+
url.searchParams.set("order", "desc");
|
|
105
|
+
if (continuationToken) {
|
|
106
|
+
url.searchParams.set("continuationToken", continuationToken);
|
|
107
|
+
}
|
|
108
|
+
return url;
|
|
109
|
+
}
|
|
110
|
+
function buildWorkItemCommentsUrl(context, id) {
|
|
111
|
+
const url = new URL(
|
|
112
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workItems/${id}/comments`
|
|
113
|
+
);
|
|
114
|
+
url.searchParams.set("api-version", "7.1-preview.4");
|
|
115
|
+
return url;
|
|
116
|
+
}
|
|
117
|
+
function mapWorkItemComment(comment, fallbackWorkItemId) {
|
|
118
|
+
return {
|
|
119
|
+
id: comment.id ?? comment.commentId ?? 0,
|
|
120
|
+
workItemId: comment.workItemId ?? fallbackWorkItemId,
|
|
121
|
+
text: typeof comment.text === "string" ? comment.text : "",
|
|
122
|
+
author: comment.createdBy?.displayName ?? null,
|
|
123
|
+
createdAt: comment.createdDate ?? null,
|
|
124
|
+
modifiedAt: comment.modifiedDate ?? null,
|
|
125
|
+
isDeleted: comment.isDeleted === true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function readContinuationToken(response, data) {
|
|
129
|
+
if (typeof data.continuationToken === "string" && data.continuationToken.trim() !== "") {
|
|
130
|
+
return data.continuationToken;
|
|
131
|
+
}
|
|
132
|
+
const headerToken = response.headers?.get("x-ms-continuationtoken") ?? response.headers?.get("continuationtoken") ?? null;
|
|
133
|
+
return headerToken && headerToken.trim() !== "" ? headerToken : null;
|
|
134
|
+
}
|
|
91
135
|
async function readWriteResponse(response, errorCode) {
|
|
92
136
|
if (response.status === 400) {
|
|
93
137
|
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
@@ -196,6 +240,55 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
196
240
|
}
|
|
197
241
|
return stringifyFieldValue(value);
|
|
198
242
|
}
|
|
243
|
+
async function listWorkItemComments(context, id, pat) {
|
|
244
|
+
const comments = [];
|
|
245
|
+
let continuationToken = null;
|
|
246
|
+
do {
|
|
247
|
+
const response = await fetchWithErrors(
|
|
248
|
+
buildWorkItemCommentsListUrl(context, id, continuationToken ?? void 0).toString(),
|
|
249
|
+
{ headers: authHeaders(pat) }
|
|
250
|
+
);
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
throw new Error(`HTTP_${response.status}`);
|
|
253
|
+
}
|
|
254
|
+
const data = await response.json();
|
|
255
|
+
comments.push(
|
|
256
|
+
...(data.comments ?? []).map((comment) => mapWorkItemComment(comment, id)).filter((comment) => !comment.isDeleted)
|
|
257
|
+
);
|
|
258
|
+
continuationToken = readContinuationToken(response, data);
|
|
259
|
+
} while (continuationToken !== null);
|
|
260
|
+
return {
|
|
261
|
+
workItemId: id,
|
|
262
|
+
count: comments.length,
|
|
263
|
+
comments
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function addWorkItemComment(context, id, pat, text) {
|
|
267
|
+
const response = await fetchWithErrors(buildWorkItemCommentsUrl(context, id).toString(), {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: {
|
|
270
|
+
...authHeaders(pat),
|
|
271
|
+
"Content-Type": "application/json"
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify({ text })
|
|
274
|
+
});
|
|
275
|
+
if (response.status === 400) {
|
|
276
|
+
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
277
|
+
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
278
|
+
}
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
throw new Error(`HTTP_${response.status}`);
|
|
281
|
+
}
|
|
282
|
+
const data = await response.json();
|
|
283
|
+
return {
|
|
284
|
+
workItemId: data.workItemId ?? id,
|
|
285
|
+
commentId: data.commentId ?? data.id ?? 0,
|
|
286
|
+
text: typeof data.text === "string" ? data.text : text,
|
|
287
|
+
author: data.createdBy?.displayName ?? null,
|
|
288
|
+
createdAt: data.createdDate ?? null,
|
|
289
|
+
url: data.url ?? null
|
|
290
|
+
};
|
|
291
|
+
}
|
|
199
292
|
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
200
293
|
const result = await applyWorkItemPatch(context, id, pat, operations);
|
|
201
294
|
const title = result.fields["System.Title"];
|
|
@@ -633,7 +726,7 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
633
726
|
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
634
727
|
`
|
|
635
728
|
);
|
|
636
|
-
} else if (msg
|
|
729
|
+
} else if (msg.startsWith("NOT_FOUND")) {
|
|
637
730
|
process.stderr.write(
|
|
638
731
|
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
639
732
|
`
|
|
@@ -1383,7 +1476,7 @@ function buildAppliedFields(fields) {
|
|
|
1383
1476
|
function ensureTitleForCreate(fields) {
|
|
1384
1477
|
const titleField = fields.find((field) => field.refName === "System.Title");
|
|
1385
1478
|
if (!titleField || titleField.op === "clear" || titleField.value === null || titleField.value.trim() === "") {
|
|
1386
|
-
fail2("Title is required when creating a
|
|
1479
|
+
fail2("Title is required when creating a work item.");
|
|
1387
1480
|
}
|
|
1388
1481
|
}
|
|
1389
1482
|
function writeSuccess(result, options) {
|
|
@@ -1395,7 +1488,7 @@ function writeSuccess(result, options) {
|
|
|
1395
1488
|
const verb = result.action === "created" ? "Created" : "Updated";
|
|
1396
1489
|
const fields = Object.keys(result.fields).join(", ");
|
|
1397
1490
|
const suffix = fields ? ` (${fields})` : "";
|
|
1398
|
-
process.stdout.write(`${verb}
|
|
1491
|
+
process.stdout.write(`${verb} ${result.workItemType} #${result.id}${suffix}
|
|
1399
1492
|
`);
|
|
1400
1493
|
}
|
|
1401
1494
|
function cleanupSourceFile(sourceFile) {
|
|
@@ -1409,16 +1502,31 @@ function cleanupSourceFile(sourceFile) {
|
|
|
1409
1502
|
`);
|
|
1410
1503
|
}
|
|
1411
1504
|
}
|
|
1412
|
-
function
|
|
1505
|
+
function resolveCreateType(id, options) {
|
|
1506
|
+
if (options.type === void 0) {
|
|
1507
|
+
return "Task";
|
|
1508
|
+
}
|
|
1509
|
+
if (id !== void 0) {
|
|
1510
|
+
fail2("--type can only be used when creating a work item.");
|
|
1511
|
+
}
|
|
1512
|
+
const trimmedType = options.type.trim();
|
|
1513
|
+
if (trimmedType === "") {
|
|
1514
|
+
fail2("--type must be a non-empty work item type.");
|
|
1515
|
+
}
|
|
1516
|
+
return trimmedType;
|
|
1517
|
+
}
|
|
1518
|
+
function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
|
|
1413
1519
|
const appliedFields = buildAppliedFields(fields);
|
|
1520
|
+
const workItemType = writeResult.fields["System.WorkItemType"];
|
|
1414
1521
|
return {
|
|
1415
1522
|
action,
|
|
1416
1523
|
id: writeResult.id,
|
|
1524
|
+
workItemType: typeof workItemType === "string" && workItemType.trim() !== "" ? workItemType : fallbackWorkItemType,
|
|
1417
1525
|
fields: appliedFields
|
|
1418
1526
|
};
|
|
1419
1527
|
}
|
|
1420
1528
|
function isUpdateWriteError(err) {
|
|
1421
|
-
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message
|
|
1529
|
+
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message.startsWith("NOT_FOUND") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
|
|
1422
1530
|
}
|
|
1423
1531
|
function isCreateWriteError(err) {
|
|
1424
1532
|
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
|
|
@@ -1448,10 +1556,11 @@ function handleUpsertError(err, id, context) {
|
|
|
1448
1556
|
}
|
|
1449
1557
|
function createUpsertCommand() {
|
|
1450
1558
|
const command = new Command9("upsert");
|
|
1451
|
-
command.description("Create or update a
|
|
1559
|
+
command.description("Create or update a work item from a markdown document").argument("[id]", "work item ID to update; omit to create a new work item").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--type <workItemType>", "create mode work item type (defaults to Task)").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
|
|
1452
1560
|
validateOrgProjectPair(options);
|
|
1453
1561
|
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
1454
1562
|
const { content, sourceFile } = loadSourceContent(options);
|
|
1563
|
+
const createType = resolveCreateType(id, options);
|
|
1455
1564
|
let context;
|
|
1456
1565
|
try {
|
|
1457
1566
|
context = resolveContext(options);
|
|
@@ -1464,14 +1573,19 @@ function createUpsertCommand() {
|
|
|
1464
1573
|
const credential = await resolvePat();
|
|
1465
1574
|
let writeResult;
|
|
1466
1575
|
if (action === "created") {
|
|
1467
|
-
writeResult = await createWorkItem(context,
|
|
1576
|
+
writeResult = await createWorkItem(context, createType, credential.pat, operations);
|
|
1468
1577
|
} else {
|
|
1469
1578
|
if (id === void 0) {
|
|
1470
1579
|
fail2("Work item ID is required for updates.");
|
|
1471
1580
|
}
|
|
1472
1581
|
writeResult = await applyWorkItemPatch(context, id, credential.pat, operations);
|
|
1473
1582
|
}
|
|
1474
|
-
const result = buildUpsertResult(
|
|
1583
|
+
const result = buildUpsertResult(
|
|
1584
|
+
action,
|
|
1585
|
+
writeResult,
|
|
1586
|
+
document.fields,
|
|
1587
|
+
action === "created" ? createType : "Work item"
|
|
1588
|
+
);
|
|
1475
1589
|
writeSuccess(result, options);
|
|
1476
1590
|
cleanupSourceFile(sourceFile);
|
|
1477
1591
|
} catch (err) {
|
|
@@ -1684,7 +1798,7 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
1684
1798
|
if (error.message === "NETWORK_ERROR") {
|
|
1685
1799
|
writeError("Could not connect to Azure DevOps. Check your network connection.");
|
|
1686
1800
|
}
|
|
1687
|
-
if (error.message
|
|
1801
|
+
if (error.message.startsWith("NOT_FOUND")) {
|
|
1688
1802
|
writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`);
|
|
1689
1803
|
}
|
|
1690
1804
|
if (error.message.startsWith("HTTP_")) {
|
|
@@ -1851,8 +1965,89 @@ function createPrCommand() {
|
|
|
1851
1965
|
return command;
|
|
1852
1966
|
}
|
|
1853
1967
|
|
|
1968
|
+
// src/commands/comments.ts
|
|
1969
|
+
import { Command as Command12 } from "commander";
|
|
1970
|
+
function writeError2(message) {
|
|
1971
|
+
process.stderr.write(`Error: ${message}
|
|
1972
|
+
`);
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1975
|
+
function formatCommentHeader(comment) {
|
|
1976
|
+
const author = comment.author ?? "Unknown";
|
|
1977
|
+
const timestamp = comment.modifiedAt ?? comment.createdAt ?? "Unknown time";
|
|
1978
|
+
return `Comment #${comment.id} by ${author} at ${timestamp}`;
|
|
1979
|
+
}
|
|
1980
|
+
function formatComments(result) {
|
|
1981
|
+
const lines = [`Comments for work item #${result.workItemId}`];
|
|
1982
|
+
for (const comment of result.comments) {
|
|
1983
|
+
lines.push("", formatCommentHeader(comment), comment.text);
|
|
1984
|
+
}
|
|
1985
|
+
return lines.join("\n");
|
|
1986
|
+
}
|
|
1987
|
+
function createCommentsListCommand() {
|
|
1988
|
+
const command = new Command12("list");
|
|
1989
|
+
command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, options) => {
|
|
1990
|
+
validateOrgProjectPair(options);
|
|
1991
|
+
const id = parseWorkItemId(idStr);
|
|
1992
|
+
let context;
|
|
1993
|
+
try {
|
|
1994
|
+
context = resolveContext(options);
|
|
1995
|
+
const credential = await resolvePat();
|
|
1996
|
+
const result = await listWorkItemComments(context, id, credential.pat);
|
|
1997
|
+
if (options.json) {
|
|
1998
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1999
|
+
`);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (result.comments.length === 0) {
|
|
2003
|
+
process.stdout.write(`Work item #${id} has no comments.
|
|
2004
|
+
`);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
process.stdout.write(`${formatComments(result)}
|
|
2008
|
+
`);
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
handleCommandError(err, id, context, "read");
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
return command;
|
|
2014
|
+
}
|
|
2015
|
+
function createCommentsAddCommand() {
|
|
2016
|
+
const command = new Command12("add");
|
|
2017
|
+
command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, text, options) => {
|
|
2018
|
+
validateOrgProjectPair(options);
|
|
2019
|
+
const id = parseWorkItemId(idStr);
|
|
2020
|
+
if (text.trim() === "") {
|
|
2021
|
+
writeError2("Comment text must be a non-empty string.");
|
|
2022
|
+
}
|
|
2023
|
+
let context;
|
|
2024
|
+
try {
|
|
2025
|
+
context = resolveContext(options);
|
|
2026
|
+
const credential = await resolvePat();
|
|
2027
|
+
const result = await addWorkItemComment(context, id, credential.pat, text);
|
|
2028
|
+
if (options.json) {
|
|
2029
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2030
|
+
`);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
process.stdout.write(`Added comment #${result.commentId} to work item #${result.workItemId}
|
|
2034
|
+
`);
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
handleCommandError(err, id, context, "write");
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
return command;
|
|
2040
|
+
}
|
|
2041
|
+
function createCommentsCommand() {
|
|
2042
|
+
const command = new Command12("comments");
|
|
2043
|
+
command.description("Manage Azure DevOps work item comments");
|
|
2044
|
+
command.addCommand(createCommentsListCommand());
|
|
2045
|
+
command.addCommand(createCommentsAddCommand());
|
|
2046
|
+
return command;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1854
2049
|
// src/index.ts
|
|
1855
|
-
var program = new
|
|
2050
|
+
var program = new Command13();
|
|
1856
2051
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
1857
2052
|
program.addCommand(createGetItemCommand());
|
|
1858
2053
|
program.addCommand(createClearPatCommand());
|
|
@@ -1865,6 +2060,7 @@ program.addCommand(createSetMdFieldCommand());
|
|
|
1865
2060
|
program.addCommand(createUpsertCommand());
|
|
1866
2061
|
program.addCommand(createListFieldsCommand());
|
|
1867
2062
|
program.addCommand(createPrCommand());
|
|
2063
|
+
program.addCommand(createCommentsCommand());
|
|
1868
2064
|
program.showHelpAfterError();
|
|
1869
2065
|
program.parse();
|
|
1870
2066
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-010-work-item-comments.149",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"format": "prettier --check src/",
|
|
17
17
|
"test": "npm run build && vitest run tests/unit",
|
|
18
|
-
"test:integration": "npm run build && vitest run tests/integration"
|
|
18
|
+
"test:integration": "npm run build && vitest run tests/integration",
|
|
19
|
+
"test:integration:full": "bash scripts/setup-keyring.sh && npm run test:integration"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|