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.
Files changed (3) hide show
  1. package/README.md +122 -26
  2. package/dist/index.js +49 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,31 +1,143 @@
1
1
  # azdo-cli
2
2
 
3
- A command-line interface for Azure DevOps.
3
+ Azure DevOps CLI focused on work item read/write workflows.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/azdo-cli)](https://www.npmjs.com/package/azdo-cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=alkampfergit_azdo-cli&metric=alert_status)](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
- ## Usage
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
- # Show help
19
- azdo --help
70
+ # Get full work item
71
+ azdo get-item 12345
20
72
 
21
- # Show version
22
- azdo --version
23
- azdo -v
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
- ## Current Status
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
- This project is in early development (v0.2.0). The base CLI scaffold is in place with support for `--help` and `--version`. Azure DevOps commands (work items, pipelines, repos, etc.) will be added in future releases.
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` | Run tests with vitest |
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
- if (extraFields && extraFields.length > 0) {
61
- const allFields = [...DEFAULT_FIELDS, ...extraFields];
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: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
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
- let serverMessage = "Unknown error";
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
- process.exit(1);
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.split(",").map((f) => f.trim()) : loadConfig().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
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {