azdo-cli 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1
- package/dist/index.js +517 -106
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ 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 Tasks from markdown documents (`upsert`)
|
|
15
16
|
- Read rich-text fields as markdown (`get-md-field`)
|
|
16
17
|
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
17
18
|
- Persist org/project/default fields in local config (`config`)
|
|
@@ -47,16 +48,20 @@ azdo get-item 12345
|
|
|
47
48
|
|
|
48
49
|
# 3) Update state
|
|
49
50
|
azdo set-state 12345 "Active"
|
|
51
|
+
|
|
52
|
+
# 4) Create or update a Task from markdown
|
|
53
|
+
azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
## Command Cheat Sheet
|
|
53
57
|
|
|
54
58
|
| Command | Purpose | Common Flags |
|
|
55
59
|
| --- | --- | --- |
|
|
56
|
-
| `azdo get-item <id>` | Read a work item | `--short`, `--fields`, `--org`, `--project` |
|
|
60
|
+
| `azdo get-item <id>` | Read a work item | `--short`, `--fields`, `--markdown`, `--org`, `--project` |
|
|
57
61
|
| `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
|
|
58
62
|
| `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
|
|
59
63
|
| `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
|
|
64
|
+
| `azdo upsert [id]` | Create or update a Task from markdown | `--content`, `--file`, `--json`, `--org`, `--project` |
|
|
60
65
|
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
61
66
|
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
62
67
|
| `azdo config <subcommand>` | Manage saved settings | `set`, `get`, `list`, `unset`, `wizard`, `--json` |
|
|
@@ -75,6 +80,12 @@ azdo get-item 12345 --short
|
|
|
75
80
|
|
|
76
81
|
# Include extra fields for this call
|
|
77
82
|
azdo get-item 12345 --fields "System.Tags,Microsoft.VSTS.Common.Priority"
|
|
83
|
+
|
|
84
|
+
# Convert rich text fields to markdown
|
|
85
|
+
azdo get-item 12345 --markdown
|
|
86
|
+
|
|
87
|
+
# Disable markdown even if config is on
|
|
88
|
+
azdo get-item 12345 --no-markdown
|
|
78
89
|
```
|
|
79
90
|
|
|
80
91
|
```bash
|
|
@@ -89,6 +100,14 @@ azdo assign 12345 --unassign
|
|
|
89
100
|
azdo set-field 12345 System.Title "Updated title"
|
|
90
101
|
```
|
|
91
102
|
|
|
103
|
+
### Markdown Display
|
|
104
|
+
|
|
105
|
+
The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
|
|
106
|
+
|
|
107
|
+
1. `--markdown` / `--no-markdown` flag (highest priority)
|
|
108
|
+
2. Config setting: `azdo config set markdown true`
|
|
109
|
+
3. Default: off (HTML stripped to plain text)
|
|
110
|
+
|
|
92
111
|
### Markdown Field Commands
|
|
93
112
|
|
|
94
113
|
```bash
|
|
@@ -105,6 +124,83 @@ azdo set-md-field 12345 System.Description --file ./description.md
|
|
|
105
124
|
cat description.md | azdo set-md-field 12345 System.Description
|
|
106
125
|
```
|
|
107
126
|
|
|
127
|
+
## azdo upsert
|
|
128
|
+
|
|
129
|
+
`azdo upsert` accepts a single markdown task document and either creates a new Azure DevOps Task or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Create from inline content
|
|
133
|
+
azdo upsert --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
134
|
+
|
|
135
|
+
# Update from a file
|
|
136
|
+
azdo upsert 12345 --file ./task-import.md
|
|
137
|
+
|
|
138
|
+
# JSON output
|
|
139
|
+
azdo upsert 12345 --content $'---\nSystem.Title: Improve markdown import UX\n---' --json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The command requires exactly one source flag:
|
|
143
|
+
|
|
144
|
+
- `azdo upsert [id] --content <markdown>`
|
|
145
|
+
- `azdo upsert [id] --file <path>`
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
### Task Document Format
|
|
150
|
+
|
|
151
|
+
The document starts with YAML front matter for scalar fields, followed by optional `##` heading sections for markdown rich-text fields.
|
|
152
|
+
|
|
153
|
+
```md
|
|
154
|
+
---
|
|
155
|
+
Title: Improve markdown import UX
|
|
156
|
+
Assigned To: user@example.com
|
|
157
|
+
State: New
|
|
158
|
+
Tags: cli; markdown
|
|
159
|
+
Priority: null
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Description
|
|
163
|
+
|
|
164
|
+
Implement a single-command task import flow.
|
|
165
|
+
|
|
166
|
+
## Acceptance Criteria
|
|
167
|
+
|
|
168
|
+
- Supports create when no ID is passed
|
|
169
|
+
- Supports update when an ID is passed
|
|
170
|
+
- Deletes imported files only after success
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Supported friendly field names:
|
|
174
|
+
|
|
175
|
+
- `Title`
|
|
176
|
+
- `Assigned To` / `assignedTo`
|
|
177
|
+
- `State`
|
|
178
|
+
- `Description`
|
|
179
|
+
- `Acceptance Criteria` / `acceptanceCriteria`
|
|
180
|
+
- `Tags`
|
|
181
|
+
- `Priority`
|
|
182
|
+
|
|
183
|
+
Raw Azure DevOps reference names are also accepted anywhere a field name is expected, for example `System.Title` or `Microsoft.VSTS.Common.AcceptanceCriteria`.
|
|
184
|
+
|
|
185
|
+
Clear semantics:
|
|
186
|
+
|
|
187
|
+
- Scalar YAML fields with `null` or an empty value are treated as clears on update.
|
|
188
|
+
- Rich-text heading sections with an empty body are treated as clears on update.
|
|
189
|
+
- Omitted fields are untouched on update.
|
|
190
|
+
|
|
191
|
+
`--json` output shape:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"action": "created",
|
|
196
|
+
"id": 12345,
|
|
197
|
+
"fields": {
|
|
198
|
+
"System.Title": "Improve markdown import UX",
|
|
199
|
+
"System.Description": "Implement a single-command task import flow."
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
108
204
|
### Configuration
|
|
109
205
|
|
|
110
206
|
```bash
|
|
@@ -114,6 +210,9 @@ azdo config list
|
|
|
114
210
|
# Interactive setup
|
|
115
211
|
azdo config wizard
|
|
116
212
|
|
|
213
|
+
# Enable markdown display for all get-item calls
|
|
214
|
+
azdo config set markdown true
|
|
215
|
+
|
|
117
216
|
# Set/get/unset values
|
|
118
217
|
azdo config set fields "System.Tags,Custom.Priority"
|
|
119
218
|
azdo config get fields
|
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 Command10 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -42,6 +42,19 @@ async function fetchWithErrors(url, init) {
|
|
|
42
42
|
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
43
43
|
return response;
|
|
44
44
|
}
|
|
45
|
+
async function readResponseMessage(response) {
|
|
46
|
+
try {
|
|
47
|
+
const body = await response.json();
|
|
48
|
+
if (typeof body.message === "string" && body.message.trim() !== "") {
|
|
49
|
+
return body.message.trim();
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function normalizeFieldList(fields) {
|
|
56
|
+
return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
|
|
57
|
+
}
|
|
45
58
|
function buildExtraFields(fields, requested) {
|
|
46
59
|
const result = {};
|
|
47
60
|
for (const name of requested) {
|
|
@@ -52,16 +65,44 @@ function buildExtraFields(fields, requested) {
|
|
|
52
65
|
}
|
|
53
66
|
return Object.keys(result).length > 0 ? result : null;
|
|
54
67
|
}
|
|
68
|
+
function writeHeaders(pat) {
|
|
69
|
+
return {
|
|
70
|
+
...authHeaders(pat),
|
|
71
|
+
"Content-Type": "application/json-patch+json"
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function readWriteResponse(response, errorCode) {
|
|
75
|
+
if (response.status === 400) {
|
|
76
|
+
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
77
|
+
throw new Error(`${errorCode}: ${serverMessage}`);
|
|
78
|
+
}
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`HTTP_${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
return {
|
|
84
|
+
id: data.id,
|
|
85
|
+
rev: data.rev,
|
|
86
|
+
fields: data.fields
|
|
87
|
+
};
|
|
88
|
+
}
|
|
55
89
|
async function getWorkItem(context, id, pat, extraFields) {
|
|
56
90
|
const url = new URL(
|
|
57
91
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
58
92
|
);
|
|
59
93
|
url.searchParams.set("api-version", "7.1");
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
95
|
+
if (normalizedExtraFields.length > 0) {
|
|
96
|
+
const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
|
|
62
97
|
url.searchParams.set("fields", allFields.join(","));
|
|
63
98
|
}
|
|
64
99
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
100
|
+
if (response.status === 400) {
|
|
101
|
+
const serverMessage = await readResponseMessage(response);
|
|
102
|
+
if (serverMessage) {
|
|
103
|
+
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
65
106
|
if (!response.ok) {
|
|
66
107
|
throw new Error(`HTTP_${response.status}`);
|
|
67
108
|
}
|
|
@@ -93,7 +134,7 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
93
134
|
areaPath: data.fields["System.AreaPath"],
|
|
94
135
|
iterationPath: data.fields["System.IterationPath"],
|
|
95
136
|
url: data._links.html.href,
|
|
96
|
-
extraFields:
|
|
137
|
+
extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
|
|
97
138
|
};
|
|
98
139
|
}
|
|
99
140
|
async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
@@ -103,6 +144,12 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
103
144
|
url.searchParams.set("api-version", "7.1");
|
|
104
145
|
url.searchParams.set("fields", fieldName);
|
|
105
146
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
147
|
+
if (response.status === 400) {
|
|
148
|
+
const serverMessage = await readResponseMessage(response);
|
|
149
|
+
if (serverMessage) {
|
|
150
|
+
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
106
153
|
if (!response.ok) {
|
|
107
154
|
throw new Error(`HTTP_${response.status}`);
|
|
108
155
|
}
|
|
@@ -114,40 +161,41 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
114
161
|
return typeof value === "object" ? JSON.stringify(value) : `${value}`;
|
|
115
162
|
}
|
|
116
163
|
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
164
|
+
const result = await applyWorkItemPatch(context, id, pat, operations);
|
|
165
|
+
const title = result.fields["System.Title"];
|
|
166
|
+
const lastOp = operations[operations.length - 1];
|
|
167
|
+
const fieldValue = lastOp.value ?? null;
|
|
168
|
+
return {
|
|
169
|
+
id: result.id,
|
|
170
|
+
rev: result.rev,
|
|
171
|
+
title: typeof title === "string" ? title : "",
|
|
172
|
+
fieldName,
|
|
173
|
+
fieldValue
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function createWorkItem(context, workItemType, pat, operations) {
|
|
177
|
+
const url = new URL(
|
|
178
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}`
|
|
179
|
+
);
|
|
180
|
+
url.searchParams.set("api-version", "7.1");
|
|
181
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: writeHeaders(pat),
|
|
184
|
+
body: JSON.stringify(operations)
|
|
185
|
+
});
|
|
186
|
+
return readWriteResponse(response, "CREATE_REJECTED");
|
|
187
|
+
}
|
|
188
|
+
async function applyWorkItemPatch(context, id, pat, operations) {
|
|
117
189
|
const url = new URL(
|
|
118
190
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
119
191
|
);
|
|
120
192
|
url.searchParams.set("api-version", "7.1");
|
|
121
193
|
const response = await fetchWithErrors(url.toString(), {
|
|
122
194
|
method: "PATCH",
|
|
123
|
-
headers:
|
|
124
|
-
...authHeaders(pat),
|
|
125
|
-
"Content-Type": "application/json-patch+json"
|
|
126
|
-
},
|
|
195
|
+
headers: writeHeaders(pat),
|
|
127
196
|
body: JSON.stringify(operations)
|
|
128
197
|
});
|
|
129
|
-
|
|
130
|
-
let serverMessage = "Unknown error";
|
|
131
|
-
try {
|
|
132
|
-
const body = await response.json();
|
|
133
|
-
if (body.message) serverMessage = body.message;
|
|
134
|
-
} catch {
|
|
135
|
-
}
|
|
136
|
-
throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
|
|
137
|
-
}
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
throw new Error(`HTTP_${response.status}`);
|
|
140
|
-
}
|
|
141
|
-
const data = await response.json();
|
|
142
|
-
const lastOp = operations[operations.length - 1];
|
|
143
|
-
const fieldValue = lastOp.value ?? null;
|
|
144
|
-
return {
|
|
145
|
-
id: data.id,
|
|
146
|
-
rev: data.rev,
|
|
147
|
-
title: data.fields["System.Title"],
|
|
148
|
-
fieldName,
|
|
149
|
-
fieldValue
|
|
150
|
-
};
|
|
198
|
+
return readWriteResponse(response, "UPDATE_REJECTED");
|
|
151
199
|
}
|
|
152
200
|
|
|
153
201
|
// src/services/auth.ts
|
|
@@ -315,6 +363,13 @@ var SETTINGS = [
|
|
|
315
363
|
type: "string[]",
|
|
316
364
|
example: "System.Tags,Custom.Priority",
|
|
317
365
|
required: false
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
key: "markdown",
|
|
369
|
+
description: "Convert rich text fields to markdown on display",
|
|
370
|
+
type: "boolean",
|
|
371
|
+
example: "true",
|
|
372
|
+
required: false
|
|
318
373
|
}
|
|
319
374
|
];
|
|
320
375
|
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
@@ -348,7 +403,7 @@ function saveConfig(config) {
|
|
|
348
403
|
}
|
|
349
404
|
function validateKey(key) {
|
|
350
405
|
if (!VALID_KEYS.includes(key)) {
|
|
351
|
-
throw new Error(`Unknown setting key "${key}". Valid keys:
|
|
406
|
+
throw new Error(`Unknown setting key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
352
407
|
}
|
|
353
408
|
}
|
|
354
409
|
function getConfigValue(key) {
|
|
@@ -361,6 +416,11 @@ function setConfigValue(key, value) {
|
|
|
361
416
|
const config = loadConfig();
|
|
362
417
|
if (value === "") {
|
|
363
418
|
delete config[key];
|
|
419
|
+
} else if (key === "markdown") {
|
|
420
|
+
if (value !== "true" && value !== "false") {
|
|
421
|
+
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
422
|
+
}
|
|
423
|
+
config.markdown = value === "true";
|
|
364
424
|
} else if (key === "fields") {
|
|
365
425
|
config.fields = value.split(",").map((s) => s.trim());
|
|
366
426
|
} else {
|
|
@@ -399,6 +459,61 @@ function resolveContext(options) {
|
|
|
399
459
|
);
|
|
400
460
|
}
|
|
401
461
|
|
|
462
|
+
// src/services/md-convert.ts
|
|
463
|
+
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
464
|
+
|
|
465
|
+
// src/services/html-detect.ts
|
|
466
|
+
var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
|
|
467
|
+
var HTML_TAGS = /* @__PURE__ */ new Set([
|
|
468
|
+
"p",
|
|
469
|
+
"br",
|
|
470
|
+
"div",
|
|
471
|
+
"span",
|
|
472
|
+
"strong",
|
|
473
|
+
"em",
|
|
474
|
+
"b",
|
|
475
|
+
"i",
|
|
476
|
+
"u",
|
|
477
|
+
"a",
|
|
478
|
+
"ul",
|
|
479
|
+
"ol",
|
|
480
|
+
"li",
|
|
481
|
+
"h1",
|
|
482
|
+
"h2",
|
|
483
|
+
"h3",
|
|
484
|
+
"h4",
|
|
485
|
+
"h5",
|
|
486
|
+
"h6",
|
|
487
|
+
"table",
|
|
488
|
+
"tr",
|
|
489
|
+
"td",
|
|
490
|
+
"th",
|
|
491
|
+
"img",
|
|
492
|
+
"pre",
|
|
493
|
+
"code"
|
|
494
|
+
]);
|
|
495
|
+
function isHtml(content) {
|
|
496
|
+
let match;
|
|
497
|
+
HTML_TAG_REGEX.lastIndex = 0;
|
|
498
|
+
while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
|
|
499
|
+
if (HTML_TAGS.has(match[1].toLowerCase())) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/services/md-convert.ts
|
|
507
|
+
function htmlToMarkdown(html) {
|
|
508
|
+
return NodeHtmlMarkdown.translate(html);
|
|
509
|
+
}
|
|
510
|
+
function toMarkdown(content) {
|
|
511
|
+
if (isHtml(content)) {
|
|
512
|
+
return htmlToMarkdown(content);
|
|
513
|
+
}
|
|
514
|
+
return content;
|
|
515
|
+
}
|
|
516
|
+
|
|
402
517
|
// src/services/command-helpers.ts
|
|
403
518
|
function parseWorkItemId(idStr) {
|
|
404
519
|
const id = Number.parseInt(idStr, 10);
|
|
@@ -421,7 +536,25 @@ function validateOrgProjectPair(options) {
|
|
|
421
536
|
process.exit(1);
|
|
422
537
|
}
|
|
423
538
|
}
|
|
424
|
-
function
|
|
539
|
+
function validateSource(options) {
|
|
540
|
+
const hasContent = options.content !== void 0;
|
|
541
|
+
const hasFile = options.file !== void 0;
|
|
542
|
+
if (hasContent === hasFile) {
|
|
543
|
+
process.stderr.write("Error: provide exactly one of --content or --file\n");
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function formatCreateError(err) {
|
|
548
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
549
|
+
const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
|
|
550
|
+
const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
|
|
551
|
+
if (requiredMatches.length > 0) {
|
|
552
|
+
const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
|
|
553
|
+
return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
|
|
554
|
+
}
|
|
555
|
+
return `Create rejected: ${message}`;
|
|
556
|
+
}
|
|
557
|
+
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
425
558
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
426
559
|
const msg = error.message;
|
|
427
560
|
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
@@ -444,6 +577,13 @@ function handleCommandError(err, id, context, scope = "write") {
|
|
|
444
577
|
process.stderr.write(
|
|
445
578
|
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
446
579
|
);
|
|
580
|
+
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
581
|
+
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
582
|
+
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
583
|
+
`);
|
|
584
|
+
} else if (msg.startsWith("CREATE_REJECTED:")) {
|
|
585
|
+
process.stderr.write(`Error: ${formatCreateError(error)}
|
|
586
|
+
`);
|
|
447
587
|
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
448
588
|
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
449
589
|
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
@@ -452,10 +592,21 @@ function handleCommandError(err, id, context, scope = "write") {
|
|
|
452
592
|
process.stderr.write(`Error: ${msg}
|
|
453
593
|
`);
|
|
454
594
|
}
|
|
455
|
-
|
|
595
|
+
if (exit) {
|
|
596
|
+
process.exit(1);
|
|
597
|
+
} else {
|
|
598
|
+
process.exitCode = 1;
|
|
599
|
+
}
|
|
456
600
|
}
|
|
457
601
|
|
|
458
602
|
// src/commands/get-item.ts
|
|
603
|
+
function parseRequestedFields(raw) {
|
|
604
|
+
if (raw === void 0) return void 0;
|
|
605
|
+
const source = Array.isArray(raw) ? raw : [raw];
|
|
606
|
+
const tokens = source.flatMap((entry) => entry.split(/[,\s]+/)).map((field) => field.trim()).filter((field) => field.length > 0);
|
|
607
|
+
if (tokens.length === 0) return void 0;
|
|
608
|
+
return Array.from(new Set(tokens));
|
|
609
|
+
}
|
|
459
610
|
function stripHtml(html) {
|
|
460
611
|
let text = html;
|
|
461
612
|
text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
|
|
@@ -472,7 +623,24 @@ function stripHtml(html) {
|
|
|
472
623
|
text = text.replace(/\n{3,}/g, "\n\n");
|
|
473
624
|
return text.trim();
|
|
474
625
|
}
|
|
475
|
-
function
|
|
626
|
+
function convertRichText(html, markdown) {
|
|
627
|
+
if (!html) return "";
|
|
628
|
+
return markdown ? toMarkdown(html) : stripHtml(html);
|
|
629
|
+
}
|
|
630
|
+
function formatExtraFields(extraFields, markdown) {
|
|
631
|
+
return Object.entries(extraFields).map(([refName, value]) => {
|
|
632
|
+
const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
|
|
633
|
+
const displayValue = markdown ? toMarkdown(value) : value;
|
|
634
|
+
return `${fieldLabel.padEnd(13)}${displayValue}`;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function summarizeDescription(text, label) {
|
|
638
|
+
const descLines = text.split("\n").filter((l) => l.trim() !== "");
|
|
639
|
+
const firstThree = descLines.slice(0, 3);
|
|
640
|
+
const suffix = descLines.length > 3 ? "\n..." : "";
|
|
641
|
+
return [`${label("Description:")}${firstThree.join("\n")}${suffix}`];
|
|
642
|
+
}
|
|
643
|
+
function formatWorkItem(workItem, short, markdown = false) {
|
|
476
644
|
const lines = [];
|
|
477
645
|
const label = (name) => name.padEnd(13);
|
|
478
646
|
lines.push(`${label("ID:")}${workItem.id}`);
|
|
@@ -486,19 +654,12 @@ function formatWorkItem(workItem, short) {
|
|
|
486
654
|
}
|
|
487
655
|
lines.push(`${label("URL:")}${workItem.url}`);
|
|
488
656
|
if (workItem.extraFields) {
|
|
489
|
-
|
|
490
|
-
const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
|
|
491
|
-
lines.push(`${fieldLabel.padEnd(13)}${value}`);
|
|
492
|
-
}
|
|
657
|
+
lines.push(...formatExtraFields(workItem.extraFields, markdown));
|
|
493
658
|
}
|
|
494
659
|
lines.push("");
|
|
495
|
-
const descriptionText = workItem.description
|
|
660
|
+
const descriptionText = convertRichText(workItem.description, markdown);
|
|
496
661
|
if (short) {
|
|
497
|
-
|
|
498
|
-
const firstThree = descLines.slice(0, 3);
|
|
499
|
-
const truncated = descLines.length > 3;
|
|
500
|
-
const descSummary = firstThree.join("\n") + (truncated ? "\n..." : "");
|
|
501
|
-
lines.push(`${label("Description:")}${descSummary}`);
|
|
662
|
+
lines.push(...summarizeDescription(descriptionText, label));
|
|
502
663
|
} else {
|
|
503
664
|
lines.push("Description:");
|
|
504
665
|
lines.push(descriptionText);
|
|
@@ -507,7 +668,7 @@ function formatWorkItem(workItem, short) {
|
|
|
507
668
|
}
|
|
508
669
|
function createGetItemCommand() {
|
|
509
670
|
const command = new Command("get-item");
|
|
510
|
-
command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").action(
|
|
671
|
+
command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").option("--fields <fields>", "comma-separated additional field reference names").option("--markdown", "convert rich text fields to markdown").action(
|
|
511
672
|
async (idStr, options) => {
|
|
512
673
|
const id = parseWorkItemId(idStr);
|
|
513
674
|
validateOrgProjectPair(options);
|
|
@@ -515,12 +676,13 @@ function createGetItemCommand() {
|
|
|
515
676
|
try {
|
|
516
677
|
context = resolveContext(options);
|
|
517
678
|
const credential = await resolvePat();
|
|
518
|
-
const fieldsList = options.fields ? options.fields
|
|
679
|
+
const fieldsList = options.fields !== void 0 ? parseRequestedFields(options.fields) : parseRequestedFields(loadConfig().fields);
|
|
519
680
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
520
|
-
const
|
|
681
|
+
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
682
|
+
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
521
683
|
process.stdout.write(output + "\n");
|
|
522
684
|
} catch (err) {
|
|
523
|
-
handleCommandError(err, id, context, "read");
|
|
685
|
+
handleCommandError(err, id, context, "read", false);
|
|
524
686
|
}
|
|
525
687
|
}
|
|
526
688
|
);
|
|
@@ -821,63 +983,6 @@ function createSetFieldCommand() {
|
|
|
821
983
|
|
|
822
984
|
// src/commands/get-md-field.ts
|
|
823
985
|
import { Command as Command7 } from "commander";
|
|
824
|
-
|
|
825
|
-
// src/services/md-convert.ts
|
|
826
|
-
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
827
|
-
|
|
828
|
-
// src/services/html-detect.ts
|
|
829
|
-
var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
|
|
830
|
-
var HTML_TAGS = /* @__PURE__ */ new Set([
|
|
831
|
-
"p",
|
|
832
|
-
"br",
|
|
833
|
-
"div",
|
|
834
|
-
"span",
|
|
835
|
-
"strong",
|
|
836
|
-
"em",
|
|
837
|
-
"b",
|
|
838
|
-
"i",
|
|
839
|
-
"u",
|
|
840
|
-
"a",
|
|
841
|
-
"ul",
|
|
842
|
-
"ol",
|
|
843
|
-
"li",
|
|
844
|
-
"h1",
|
|
845
|
-
"h2",
|
|
846
|
-
"h3",
|
|
847
|
-
"h4",
|
|
848
|
-
"h5",
|
|
849
|
-
"h6",
|
|
850
|
-
"table",
|
|
851
|
-
"tr",
|
|
852
|
-
"td",
|
|
853
|
-
"th",
|
|
854
|
-
"img",
|
|
855
|
-
"pre",
|
|
856
|
-
"code"
|
|
857
|
-
]);
|
|
858
|
-
function isHtml(content) {
|
|
859
|
-
let match;
|
|
860
|
-
HTML_TAG_REGEX.lastIndex = 0;
|
|
861
|
-
while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
|
|
862
|
-
if (HTML_TAGS.has(match[1].toLowerCase())) {
|
|
863
|
-
return true;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// src/services/md-convert.ts
|
|
870
|
-
function htmlToMarkdown(html) {
|
|
871
|
-
return NodeHtmlMarkdown.translate(html);
|
|
872
|
-
}
|
|
873
|
-
function toMarkdown(content) {
|
|
874
|
-
if (isHtml(content)) {
|
|
875
|
-
return htmlToMarkdown(content);
|
|
876
|
-
}
|
|
877
|
-
return content;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/commands/get-md-field.ts
|
|
881
986
|
function createGetMdFieldCommand() {
|
|
882
987
|
const command = new Command7("get-md-field");
|
|
883
988
|
command.description("Get a work item field value, converting HTML to markdown").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
|
|
@@ -990,8 +1095,313 @@ function createSetMdFieldCommand() {
|
|
|
990
1095
|
return command;
|
|
991
1096
|
}
|
|
992
1097
|
|
|
1098
|
+
// src/commands/upsert.ts
|
|
1099
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
1100
|
+
import { Command as Command9 } from "commander";
|
|
1101
|
+
|
|
1102
|
+
// src/services/task-document.ts
|
|
1103
|
+
var FIELD_ALIASES = /* @__PURE__ */ new Map([
|
|
1104
|
+
["title", "System.Title"],
|
|
1105
|
+
["assignedto", "System.AssignedTo"],
|
|
1106
|
+
["assigned to", "System.AssignedTo"],
|
|
1107
|
+
["state", "System.State"],
|
|
1108
|
+
["description", "System.Description"],
|
|
1109
|
+
["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
|
|
1110
|
+
["acceptance criteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
|
|
1111
|
+
["tags", "System.Tags"],
|
|
1112
|
+
["priority", "Microsoft.VSTS.Common.Priority"]
|
|
1113
|
+
]);
|
|
1114
|
+
var RICH_TEXT_FIELDS = /* @__PURE__ */ new Set([
|
|
1115
|
+
"System.Description",
|
|
1116
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria"
|
|
1117
|
+
]);
|
|
1118
|
+
var REFERENCE_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$/;
|
|
1119
|
+
function normalizeAlias(name) {
|
|
1120
|
+
return name.trim().replaceAll(/\s+/g, " ").toLowerCase();
|
|
1121
|
+
}
|
|
1122
|
+
function parseScalarValue(rawValue, fieldName) {
|
|
1123
|
+
if (rawValue === void 0) {
|
|
1124
|
+
throw new Error(`Malformed YAML front matter: missing value for "${fieldName}"`);
|
|
1125
|
+
}
|
|
1126
|
+
const trimmed = rawValue.trim();
|
|
1127
|
+
if (trimmed === "" || trimmed === "null" || trimmed === "~") {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
1131
|
+
return trimmed.slice(1, -1);
|
|
1132
|
+
}
|
|
1133
|
+
if (/^[[{]|^[>|]-?$/.test(trimmed)) {
|
|
1134
|
+
throw new Error(`Malformed YAML front matter: unsupported value for "${fieldName}"`);
|
|
1135
|
+
}
|
|
1136
|
+
return trimmed;
|
|
1137
|
+
}
|
|
1138
|
+
function parseFrontMatter(content) {
|
|
1139
|
+
if (!content.startsWith("---")) {
|
|
1140
|
+
return { frontMatter: "", remainder: content };
|
|
1141
|
+
}
|
|
1142
|
+
const frontMatterPattern = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
1143
|
+
const match = frontMatterPattern.exec(content);
|
|
1144
|
+
if (!match) {
|
|
1145
|
+
throw new Error('Malformed YAML front matter: missing closing "---"');
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
frontMatter: match[1],
|
|
1149
|
+
remainder: content.slice(match[0].length)
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function assertKnownField(name, kind) {
|
|
1153
|
+
const resolved = resolveFieldName(name);
|
|
1154
|
+
if (!resolved) {
|
|
1155
|
+
const prefix = kind === "rich-text" ? "Unknown rich-text field" : "Unknown field";
|
|
1156
|
+
throw new Error(`${prefix}: ${name}`);
|
|
1157
|
+
}
|
|
1158
|
+
if (kind === "rich-text" && !RICH_TEXT_FIELDS.has(resolved)) {
|
|
1159
|
+
throw new Error(`Unknown rich-text field: ${name}`);
|
|
1160
|
+
}
|
|
1161
|
+
return resolved;
|
|
1162
|
+
}
|
|
1163
|
+
function pushField(fields, seen, refName, value, kind) {
|
|
1164
|
+
if (seen.has(refName)) {
|
|
1165
|
+
throw new Error(`Duplicate field: ${refName}`);
|
|
1166
|
+
}
|
|
1167
|
+
seen.add(refName);
|
|
1168
|
+
fields.push({
|
|
1169
|
+
refName,
|
|
1170
|
+
value,
|
|
1171
|
+
op: value === null ? "clear" : "set",
|
|
1172
|
+
kind
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
function parseScalarFields(frontMatter, fields, seen) {
|
|
1176
|
+
if (frontMatter.trim() === "") {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
for (const rawLine of frontMatter.split(/\r?\n/)) {
|
|
1180
|
+
const line = rawLine.trim();
|
|
1181
|
+
if (line === "") {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const separatorIndex = rawLine.indexOf(":");
|
|
1185
|
+
if (separatorIndex <= 0) {
|
|
1186
|
+
throw new Error(`Malformed YAML front matter: ${rawLine.trim()}`);
|
|
1187
|
+
}
|
|
1188
|
+
const rawName = rawLine.slice(0, separatorIndex).trim();
|
|
1189
|
+
const rawValue = rawLine.slice(separatorIndex + 1);
|
|
1190
|
+
const refName = assertKnownField(rawName, "scalar");
|
|
1191
|
+
const value = parseScalarValue(rawValue, rawName);
|
|
1192
|
+
pushField(fields, seen, refName, value, "scalar");
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function parseRichTextSections(content, fields, seen) {
|
|
1196
|
+
const normalizedContent = content.replaceAll("\r\n", "\n");
|
|
1197
|
+
const lines = normalizedContent.split("\n");
|
|
1198
|
+
const headings = [];
|
|
1199
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1200
|
+
const line = lines[index];
|
|
1201
|
+
if (!line.startsWith("##")) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
const headingBody = line.slice(2);
|
|
1205
|
+
if (headingBody.trim() === "" || !headingBody.startsWith(" ") && !headingBody.startsWith(" ")) {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
headings.push({
|
|
1209
|
+
lineIndex: index,
|
|
1210
|
+
rawName: headingBody.trim()
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
if (headings.length === 0) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
for (let index = 0; index < headings[0].lineIndex; index += 1) {
|
|
1217
|
+
if (lines[index].trim() !== "") {
|
|
1218
|
+
throw new Error("Unexpected content before the first markdown heading section");
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
1222
|
+
const { lineIndex, rawName } = headings[index];
|
|
1223
|
+
const refName = assertKnownField(rawName, "rich-text");
|
|
1224
|
+
const bodyStart = lineIndex + 1;
|
|
1225
|
+
const bodyEnd = index + 1 < headings.length ? headings[index + 1].lineIndex : lines.length;
|
|
1226
|
+
const rawBody = lines.slice(bodyStart, bodyEnd).join("\n");
|
|
1227
|
+
const value = rawBody.trim() === "" ? null : rawBody.trimEnd();
|
|
1228
|
+
pushField(fields, seen, refName, value, "rich-text");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function resolveFieldName(name) {
|
|
1232
|
+
const trimmed = name.trim();
|
|
1233
|
+
if (trimmed === "") {
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
const alias = FIELD_ALIASES.get(normalizeAlias(trimmed));
|
|
1237
|
+
if (alias) {
|
|
1238
|
+
return alias;
|
|
1239
|
+
}
|
|
1240
|
+
return REFERENCE_NAME_PATTERN.test(trimmed) ? trimmed : null;
|
|
1241
|
+
}
|
|
1242
|
+
function parseTaskDocument(content) {
|
|
1243
|
+
const { frontMatter, remainder } = parseFrontMatter(content);
|
|
1244
|
+
const fields = [];
|
|
1245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1246
|
+
parseScalarFields(frontMatter, fields, seen);
|
|
1247
|
+
parseRichTextSections(remainder, fields, seen);
|
|
1248
|
+
return { fields };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/commands/upsert.ts
|
|
1252
|
+
function fail2(message) {
|
|
1253
|
+
process.stderr.write(`Error: ${message}
|
|
1254
|
+
`);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
function loadSourceContent(options) {
|
|
1258
|
+
validateSource(options);
|
|
1259
|
+
if (options.content !== void 0) {
|
|
1260
|
+
return { content: options.content };
|
|
1261
|
+
}
|
|
1262
|
+
const filePath = options.file;
|
|
1263
|
+
if (!existsSync2(filePath)) {
|
|
1264
|
+
fail2(`File not found: ${filePath}`);
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
return {
|
|
1268
|
+
content: readFileSync3(filePath, "utf-8"),
|
|
1269
|
+
sourceFile: filePath
|
|
1270
|
+
};
|
|
1271
|
+
} catch {
|
|
1272
|
+
fail2(`Cannot read file: ${filePath}`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function toPatchOperations(fields, action) {
|
|
1276
|
+
const operations = [];
|
|
1277
|
+
for (const field of fields) {
|
|
1278
|
+
if (field.op === "clear") {
|
|
1279
|
+
if (action === "updated") {
|
|
1280
|
+
operations.push({ op: "remove", path: `/fields/${field.refName}` });
|
|
1281
|
+
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
operations.push({ op: "add", path: `/fields/${field.refName}`, value: field.value ?? "" });
|
|
1285
|
+
if (field.kind === "rich-text") {
|
|
1286
|
+
operations.push({
|
|
1287
|
+
op: "add",
|
|
1288
|
+
path: `/multilineFieldsFormat/${field.refName}`,
|
|
1289
|
+
value: "Markdown"
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return operations;
|
|
1294
|
+
}
|
|
1295
|
+
function buildAppliedFields(fields) {
|
|
1296
|
+
const applied = {};
|
|
1297
|
+
for (const field of fields) {
|
|
1298
|
+
applied[field.refName] = field.value;
|
|
1299
|
+
}
|
|
1300
|
+
return applied;
|
|
1301
|
+
}
|
|
1302
|
+
function ensureTitleForCreate(fields) {
|
|
1303
|
+
const titleField = fields.find((field) => field.refName === "System.Title");
|
|
1304
|
+
if (!titleField || titleField.op === "clear" || titleField.value === null || titleField.value.trim() === "") {
|
|
1305
|
+
fail2("Title is required when creating a task.");
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function writeSuccess(result, options) {
|
|
1309
|
+
if (options.json) {
|
|
1310
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
1311
|
+
`);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const verb = result.action === "created" ? "Created" : "Updated";
|
|
1315
|
+
const fields = Object.keys(result.fields).join(", ");
|
|
1316
|
+
const suffix = fields ? ` (${fields})` : "";
|
|
1317
|
+
process.stdout.write(`${verb} task #${result.id}${suffix}
|
|
1318
|
+
`);
|
|
1319
|
+
}
|
|
1320
|
+
function cleanupSourceFile(sourceFile) {
|
|
1321
|
+
if (!sourceFile) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
unlinkSync(sourceFile);
|
|
1326
|
+
} catch {
|
|
1327
|
+
process.stderr.write(`Warning: upsert succeeded but could not delete source file: ${sourceFile}
|
|
1328
|
+
`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function buildUpsertResult(action, writeResult, fields) {
|
|
1332
|
+
const appliedFields = buildAppliedFields(fields);
|
|
1333
|
+
return {
|
|
1334
|
+
action,
|
|
1335
|
+
id: writeResult.id,
|
|
1336
|
+
fields: appliedFields
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
function isUpdateWriteError(err) {
|
|
1340
|
+
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NOT_FOUND" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
|
|
1341
|
+
}
|
|
1342
|
+
function isCreateWriteError(err) {
|
|
1343
|
+
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
|
|
1344
|
+
}
|
|
1345
|
+
function handleUpsertError(err, id, context) {
|
|
1346
|
+
if (!(err instanceof Error)) {
|
|
1347
|
+
process.stderr.write(`Error: ${String(err)}
|
|
1348
|
+
`);
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
if (id === void 0 && err.message.startsWith("CREATE_REJECTED:")) {
|
|
1352
|
+
process.stderr.write(`Error: ${formatCreateError(err)}
|
|
1353
|
+
`);
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
if (id !== void 0 && isUpdateWriteError(err)) {
|
|
1357
|
+
handleCommandError(err, id, context, "write");
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (id === void 0 && isCreateWriteError(err)) {
|
|
1361
|
+
handleCommandError(err, 0, context, "write");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
process.stderr.write(`Error: ${err.message}
|
|
1365
|
+
`);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
}
|
|
1368
|
+
function createUpsertCommand() {
|
|
1369
|
+
const command = new Command9("upsert");
|
|
1370
|
+
command.description("Create or update a Task from a markdown document").argument("[id]", "work item ID to update; omit to create a new Task").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
|
|
1371
|
+
validateOrgProjectPair(options);
|
|
1372
|
+
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
1373
|
+
const { content, sourceFile } = loadSourceContent(options);
|
|
1374
|
+
let context;
|
|
1375
|
+
try {
|
|
1376
|
+
context = resolveContext(options);
|
|
1377
|
+
const document = parseTaskDocument(content);
|
|
1378
|
+
const action = id === void 0 ? "created" : "updated";
|
|
1379
|
+
if (action === "created") {
|
|
1380
|
+
ensureTitleForCreate(document.fields);
|
|
1381
|
+
}
|
|
1382
|
+
const operations = toPatchOperations(document.fields, action);
|
|
1383
|
+
const credential = await resolvePat();
|
|
1384
|
+
let writeResult;
|
|
1385
|
+
if (action === "created") {
|
|
1386
|
+
writeResult = await createWorkItem(context, "Task", credential.pat, operations);
|
|
1387
|
+
} else {
|
|
1388
|
+
if (id === void 0) {
|
|
1389
|
+
fail2("Work item ID is required for updates.");
|
|
1390
|
+
}
|
|
1391
|
+
writeResult = await applyWorkItemPatch(context, id, credential.pat, operations);
|
|
1392
|
+
}
|
|
1393
|
+
const result = buildUpsertResult(action, writeResult, document.fields);
|
|
1394
|
+
writeSuccess(result, options);
|
|
1395
|
+
cleanupSourceFile(sourceFile);
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
handleUpsertError(err, id, context);
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
return command;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
993
1403
|
// src/index.ts
|
|
994
|
-
var program = new
|
|
1404
|
+
var program = new Command10();
|
|
995
1405
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
996
1406
|
program.addCommand(createGetItemCommand());
|
|
997
1407
|
program.addCommand(createClearPatCommand());
|
|
@@ -1001,6 +1411,7 @@ program.addCommand(createAssignCommand());
|
|
|
1001
1411
|
program.addCommand(createSetFieldCommand());
|
|
1002
1412
|
program.addCommand(createGetMdFieldCommand());
|
|
1003
1413
|
program.addCommand(createSetMdFieldCommand());
|
|
1414
|
+
program.addCommand(createUpsertCommand());
|
|
1004
1415
|
program.showHelpAfterError();
|
|
1005
1416
|
program.parse();
|
|
1006
1417
|
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.3.0",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@eslint/js": "^10.0.1",
|
|
31
|
-
"@types/node": "^25.
|
|
32
|
-
"eslint": "^10.0
|
|
31
|
+
"@types/node": "^25.5.0",
|
|
32
|
+
"eslint": "^10.1.0",
|
|
33
33
|
"prettier": "^3.8.1",
|
|
34
34
|
"tsup": "^8.5.1",
|
|
35
35
|
"typescript": "^5.9.3",
|
|
36
|
-
"typescript-eslint": "^8.
|
|
37
|
-
"vitest": "^4.
|
|
36
|
+
"typescript-eslint": "^8.57.2",
|
|
37
|
+
"vitest": "^4.1.2"
|
|
38
38
|
}
|
|
39
39
|
}
|