azdo-cli 0.2.0 → 0.2.5
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 +122 -26
- package/dist/index.js +49 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,31 +1,143 @@
|
|
|
1
1
|
# azdo-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Azure DevOps CLI focused on work item read/write workflows.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/azdo-cli)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://sonarcloud.io/summary/new_code?id=alkampfergit_azdo-cli)
|
|
8
8
|
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Retrieve work items with readable output (`get-item`)
|
|
12
|
+
- Update work item state (`set-state`)
|
|
13
|
+
- Assign and unassign work items (`assign`)
|
|
14
|
+
- Set any work item field by reference name (`set-field`)
|
|
15
|
+
- Read rich-text fields as markdown (`get-md-field`)
|
|
16
|
+
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
17
|
+
- Persist org/project/default fields in local config (`config`)
|
|
18
|
+
- Store PAT in OS credential store (or use `AZDO_PAT`)
|
|
19
|
+
|
|
9
20
|
## Installation
|
|
10
21
|
|
|
11
22
|
```bash
|
|
12
23
|
npm install -g azdo-cli
|
|
13
24
|
```
|
|
14
25
|
|
|
15
|
-
##
|
|
26
|
+
## Authentication and Context Resolution
|
|
27
|
+
|
|
28
|
+
PAT resolution order:
|
|
29
|
+
1. `AZDO_PAT` environment variable
|
|
30
|
+
2. Stored credential from OS keyring
|
|
31
|
+
3. Interactive PAT prompt (then stored for next runs)
|
|
32
|
+
|
|
33
|
+
Org/project resolution order:
|
|
34
|
+
1. `--org` + `--project` flags
|
|
35
|
+
2. Saved config (`azdo config set org ...`, `azdo config set project ...`)
|
|
36
|
+
3. Azure DevOps `origin` git remote auto-detection
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 1) Configure defaults once
|
|
42
|
+
azdo config set org myorg
|
|
43
|
+
azdo config set project myproject
|
|
44
|
+
|
|
45
|
+
# 2) Read a work item
|
|
46
|
+
azdo get-item 12345
|
|
47
|
+
|
|
48
|
+
# 3) Update state
|
|
49
|
+
azdo set-state 12345 "Active"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Command Cheat Sheet
|
|
53
|
+
|
|
54
|
+
| Command | Purpose | Common Flags |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `azdo get-item <id>` | Read a work item | `--short`, `--fields`, `--org`, `--project` |
|
|
57
|
+
| `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
|
|
58
|
+
| `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
|
|
59
|
+
| `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
|
|
60
|
+
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
61
|
+
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
62
|
+
| `azdo config <subcommand>` | Manage saved settings | `set`, `get`, `list`, `unset`, `wizard`, `--json` |
|
|
63
|
+
| `azdo clear-pat` | Remove stored PAT | none |
|
|
64
|
+
|
|
65
|
+
## Command Reference
|
|
66
|
+
|
|
67
|
+
### Core
|
|
16
68
|
|
|
17
69
|
```bash
|
|
18
|
-
#
|
|
19
|
-
azdo
|
|
70
|
+
# Get full work item
|
|
71
|
+
azdo get-item 12345
|
|
20
72
|
|
|
21
|
-
#
|
|
22
|
-
azdo --
|
|
23
|
-
|
|
73
|
+
# Get short view
|
|
74
|
+
azdo get-item 12345 --short
|
|
75
|
+
|
|
76
|
+
# Include extra fields for this call
|
|
77
|
+
azdo get-item 12345 --fields "System.Tags,Microsoft.VSTS.Common.Priority"
|
|
24
78
|
```
|
|
25
79
|
|
|
26
|
-
|
|
80
|
+
```bash
|
|
81
|
+
# Set state
|
|
82
|
+
azdo set-state 12345 "Closed"
|
|
83
|
+
|
|
84
|
+
# Assign / unassign
|
|
85
|
+
azdo assign 12345 "someone@company.com"
|
|
86
|
+
azdo assign 12345 --unassign
|
|
87
|
+
|
|
88
|
+
# Set generic field
|
|
89
|
+
azdo set-field 12345 System.Title "Updated title"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Markdown Field Commands
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Read field and auto-convert HTML -> markdown
|
|
96
|
+
azdo get-md-field 12345 System.Description
|
|
27
97
|
|
|
28
|
-
|
|
98
|
+
# Set markdown inline
|
|
99
|
+
azdo set-md-field 12345 System.Description "# Title\n\nSome **bold** text"
|
|
100
|
+
|
|
101
|
+
# Set markdown from file
|
|
102
|
+
azdo set-md-field 12345 System.Description --file ./description.md
|
|
103
|
+
|
|
104
|
+
# Set markdown from stdin
|
|
105
|
+
cat description.md | azdo set-md-field 12345 System.Description
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Configuration
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# List settings
|
|
112
|
+
azdo config list
|
|
113
|
+
|
|
114
|
+
# Interactive setup
|
|
115
|
+
azdo config wizard
|
|
116
|
+
|
|
117
|
+
# Set/get/unset values
|
|
118
|
+
azdo config set fields "System.Tags,Custom.Priority"
|
|
119
|
+
azdo config get fields
|
|
120
|
+
azdo config unset fields
|
|
121
|
+
|
|
122
|
+
# JSON output
|
|
123
|
+
azdo config list --json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Credential Management
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Remove stored PAT from keyring
|
|
130
|
+
azdo clear-pat
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## JSON Output
|
|
134
|
+
|
|
135
|
+
These commands support `--json` for machine-readable output:
|
|
136
|
+
- `set-state`
|
|
137
|
+
- `assign`
|
|
138
|
+
- `set-field`
|
|
139
|
+
- `set-md-field`
|
|
140
|
+
- `config set|get|list|unset`
|
|
29
141
|
|
|
30
142
|
## Development
|
|
31
143
|
|
|
@@ -47,27 +159,11 @@ npm install
|
|
|
47
159
|
| Command | Description |
|
|
48
160
|
| --- | --- |
|
|
49
161
|
| `npm run build` | Build the CLI with tsup |
|
|
50
|
-
| `npm test` |
|
|
162
|
+
| `npm test` | Build and run tests with vitest |
|
|
51
163
|
| `npm run lint` | Lint source files with ESLint |
|
|
52
164
|
| `npm run typecheck` | Type-check with tsc (no emit) |
|
|
53
165
|
| `npm run format` | Check formatting with Prettier |
|
|
54
166
|
|
|
55
|
-
### Tech Stack
|
|
56
|
-
|
|
57
|
-
- **TypeScript 5.x** (strict mode, ES modules)
|
|
58
|
-
- **commander.js** — CLI framework
|
|
59
|
-
- **tsup** — Bundler (single-file ESM output)
|
|
60
|
-
- **vitest** — Test runner
|
|
61
|
-
- **ESLint + Prettier** — Linting and formatting
|
|
62
|
-
|
|
63
|
-
### Branch Strategy
|
|
64
|
-
|
|
65
|
-
This project follows **GitFlow**:
|
|
66
|
-
|
|
67
|
-
- `master` — stable releases
|
|
68
|
-
- `develop` — integration branch
|
|
69
|
-
- `feature/*` — feature branches off `develop`
|
|
70
|
-
|
|
71
167
|
## License
|
|
72
168
|
|
|
73
169
|
[MIT](LICENSE)
|
package/dist/index.js
CHANGED
|
@@ -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) {
|
|
@@ -57,11 +70,18 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
57
70
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
58
71
|
);
|
|
59
72
|
url.searchParams.set("api-version", "7.1");
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
74
|
+
if (normalizedExtraFields.length > 0) {
|
|
75
|
+
const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
|
|
62
76
|
url.searchParams.set("fields", allFields.join(","));
|
|
63
77
|
}
|
|
64
78
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
79
|
+
if (response.status === 400) {
|
|
80
|
+
const serverMessage = await readResponseMessage(response);
|
|
81
|
+
if (serverMessage) {
|
|
82
|
+
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
65
85
|
if (!response.ok) {
|
|
66
86
|
throw new Error(`HTTP_${response.status}`);
|
|
67
87
|
}
|
|
@@ -93,7 +113,7 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
93
113
|
areaPath: data.fields["System.AreaPath"],
|
|
94
114
|
iterationPath: data.fields["System.IterationPath"],
|
|
95
115
|
url: data._links.html.href,
|
|
96
|
-
extraFields:
|
|
116
|
+
extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
|
|
97
117
|
};
|
|
98
118
|
}
|
|
99
119
|
async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
@@ -103,6 +123,12 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
103
123
|
url.searchParams.set("api-version", "7.1");
|
|
104
124
|
url.searchParams.set("fields", fieldName);
|
|
105
125
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
126
|
+
if (response.status === 400) {
|
|
127
|
+
const serverMessage = await readResponseMessage(response);
|
|
128
|
+
if (serverMessage) {
|
|
129
|
+
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
106
132
|
if (!response.ok) {
|
|
107
133
|
throw new Error(`HTTP_${response.status}`);
|
|
108
134
|
}
|
|
@@ -127,12 +153,7 @@ async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
|
127
153
|
body: JSON.stringify(operations)
|
|
128
154
|
});
|
|
129
155
|
if (response.status === 400) {
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const body = await response.json();
|
|
133
|
-
if (body.message) serverMessage = body.message;
|
|
134
|
-
} catch {
|
|
135
|
-
}
|
|
156
|
+
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
136
157
|
throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
|
|
137
158
|
}
|
|
138
159
|
if (!response.ok) {
|
|
@@ -421,7 +442,7 @@ function validateOrgProjectPair(options) {
|
|
|
421
442
|
process.exit(1);
|
|
422
443
|
}
|
|
423
444
|
}
|
|
424
|
-
function handleCommandError(err, id, context, scope = "write") {
|
|
445
|
+
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
425
446
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
426
447
|
const msg = error.message;
|
|
427
448
|
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
@@ -444,6 +465,10 @@ function handleCommandError(err, id, context, scope = "write") {
|
|
|
444
465
|
process.stderr.write(
|
|
445
466
|
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
446
467
|
);
|
|
468
|
+
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
469
|
+
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
470
|
+
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
471
|
+
`);
|
|
447
472
|
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
448
473
|
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
449
474
|
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
@@ -452,10 +477,21 @@ function handleCommandError(err, id, context, scope = "write") {
|
|
|
452
477
|
process.stderr.write(`Error: ${msg}
|
|
453
478
|
`);
|
|
454
479
|
}
|
|
455
|
-
|
|
480
|
+
if (exit) {
|
|
481
|
+
process.exit(1);
|
|
482
|
+
} else {
|
|
483
|
+
process.exitCode = 1;
|
|
484
|
+
}
|
|
456
485
|
}
|
|
457
486
|
|
|
458
487
|
// src/commands/get-item.ts
|
|
488
|
+
function parseRequestedFields(raw) {
|
|
489
|
+
if (raw === void 0) return void 0;
|
|
490
|
+
const source = Array.isArray(raw) ? raw : [raw];
|
|
491
|
+
const tokens = source.flatMap((entry) => entry.split(/[,\s]+/)).map((field) => field.trim()).filter((field) => field.length > 0);
|
|
492
|
+
if (tokens.length === 0) return void 0;
|
|
493
|
+
return Array.from(new Set(tokens));
|
|
494
|
+
}
|
|
459
495
|
function stripHtml(html) {
|
|
460
496
|
let text = html;
|
|
461
497
|
text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
|
|
@@ -515,12 +551,12 @@ function createGetItemCommand() {
|
|
|
515
551
|
try {
|
|
516
552
|
context = resolveContext(options);
|
|
517
553
|
const credential = await resolvePat();
|
|
518
|
-
const fieldsList = options.fields ? options.fields
|
|
554
|
+
const fieldsList = options.fields !== void 0 ? parseRequestedFields(options.fields) : parseRequestedFields(loadConfig().fields);
|
|
519
555
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
520
556
|
const output = formatWorkItem(workItem, options.short ?? false);
|
|
521
557
|
process.stdout.write(output + "\n");
|
|
522
558
|
} catch (err) {
|
|
523
|
-
handleCommandError(err, id, context, "read");
|
|
559
|
+
handleCommandError(err, id, context, "read", false);
|
|
524
560
|
}
|
|
525
561
|
}
|
|
526
562
|
);
|