azdo-cli 0.2.0-develop.8 → 0.2.0-hotfix-0-2-5.63

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 +141 -51
  2. package/dist/index.js +1025 -2
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -1,79 +1,169 @@
1
- # bd - Beads
1
+ # azdo-cli
2
2
 
3
- **Distributed, git-backed graph issue tracker for AI agents.**
3
+ Azure DevOps CLI focused on work item read/write workflows.
4
4
 
5
- **Platforms:** macOS, Linux, Windows, FreeBSD
5
+ [![npm version](https://img.shields.io/npm/v/azdo-cli)](https://www.npmjs.com/package/azdo-cli)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
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)
6
8
 
7
- [![License](https://img.shields.io/github/license/steveyegge/beads)](LICENSE)
8
- [![Go Report Card](https://goreportcard.com/badge/github.com/steveyegge/beads)](https://goreportcard.com/report/github.com/steveyegge/beads)
9
- [![Release](https://img.shields.io/github/v/release/steveyegge/beads)](https://github.com/steveyegge/beads/releases)
10
- [![npm version](https://img.shields.io/npm/v/@beads/bd)](https://www.npmjs.com/package/@beads/bd)
11
- [![PyPI](https://img.shields.io/pypi/v/beads-mcp)](https://pypi.org/project/beads-mcp/)
9
+ ## Features
12
10
 
13
- Beads provides a persistent, structured memory for coding agents. It replaces messy markdown plans with a dependency-aware graph, allowing agents to handle long-horizon tasks without losing context.
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`)
14
19
 
15
- ## ⚡ Quick Start
20
+ ## Installation
16
21
 
17
22
  ```bash
18
- # Install beads CLI (system-wide - don't clone this repo into your project)
19
- curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
23
+ npm install -g azdo-cli
24
+ ```
25
+
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)
20
32
 
21
- # Initialize in YOUR project
22
- cd your-project
23
- bd init
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
24
37
 
25
- # Tell your agent
26
- echo "Use 'bd' for task tracking" >> AGENTS.md
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"
27
50
  ```
28
51
 
29
- **Note:** Beads is a CLI tool you install once and use everywhere. You don't need to clone this repository into your project.
52
+ ## Command Cheat Sheet
30
53
 
31
- ## 🛠 Features
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 |
32
64
 
33
- * **Git as Database:** Issues stored as JSONL in `.beads/`. Versioned, branched, and merged like code.
34
- * **Agent-Optimized:** JSON output, dependency tracking, and auto-ready task detection.
35
- * **Zero Conflict:** Hash-based IDs (`bd-a1b2`) prevent merge collisions in multi-agent/multi-branch workflows.
36
- * **Invisible Infrastructure:** SQLite local cache for speed; background daemon for auto-sync.
37
- * **Compaction:** Semantic "memory decay" summarizes old closed tasks to save context window.
65
+ ## Command Reference
38
66
 
39
- ## 📖 Essential Commands
67
+ ### Core
40
68
 
41
- | Command | Action |
42
- | --- | --- |
43
- | `bd ready` | List tasks with no open blockers. |
44
- | `bd create "Title" -p 0` | Create a P0 task. |
45
- | `bd update <id> --claim` | Atomically claim a task (sets assignee + in_progress). |
46
- | `bd dep add <child> <parent>` | Link tasks (blocks, related, parent-child). |
47
- | `bd show <id>` | View task details and audit trail. |
69
+ ```bash
70
+ # Get full work item
71
+ azdo get-item 12345
72
+
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"
78
+ ```
79
+
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
48
93
 
49
- ## 🔗 Hierarchy & Workflow
94
+ ```bash
95
+ # Read field and auto-convert HTML -> markdown
96
+ azdo get-md-field 12345 System.Description
97
+
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
50
116
 
51
- Beads supports hierarchical IDs for epics:
117
+ # Set/get/unset values
118
+ azdo config set fields "System.Tags,Custom.Priority"
119
+ azdo config get fields
120
+ azdo config unset fields
52
121
 
53
- * `bd-a3f8` (Epic)
54
- * `bd-a3f8.1` (Task)
55
- * `bd-a3f8.1.1` (Sub-task)
122
+ # JSON output
123
+ azdo config list --json
124
+ ```
56
125
 
57
- **Stealth Mode:** Run `bd init --stealth` to use Beads locally without committing files to the main repo. Perfect for personal use on shared projects.
126
+ ### Credential Management
58
127
 
59
- **Contributor vs Maintainer:** When working on open-source projects:
128
+ ```bash
129
+ # Remove stored PAT from keyring
130
+ azdo clear-pat
131
+ ```
60
132
 
61
- * **Contributors** (forked repos): Run `bd init --contributor` to route planning issues to a separate repo (e.g., `~/.beads-planning`). Keeps experimental work out of PRs.
62
- * **Maintainers** (write access): Beads auto-detects maintainer role via SSH URLs or HTTPS with credentials. Only need `git config beads.role maintainer` if using GitHub HTTPS without credentials but you have write access.
133
+ ## JSON Output
63
134
 
64
- ## 📦 Installation
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`
65
141
 
66
- * **npm:** `npm install -g @beads/bd`
67
- * **Homebrew:** `brew install beads`
68
- * **Go:** `go install github.com/steveyegge/beads/cmd/bd@latest`
142
+ ## Development
69
143
 
70
- **Requirements:** Linux, FreeBSD, macOS, or Windows.
144
+ ### Prerequisites
71
145
 
72
- ## 🌐 Community Tools
146
+ - Node.js LTS (20+)
147
+ - npm
73
148
 
74
- See [docs/COMMUNITY_TOOLS.md](docs/COMMUNITY_TOOLS.md) for a curated list of community-built UIs, extensions, and integrations—including terminal interfaces, web UIs, editor extensions, and native apps.
149
+ ### Setup
150
+
151
+ ```bash
152
+ git clone https://github.com/alkampfergit/azdo-cli.git
153
+ cd azdo-cli
154
+ npm install
155
+ ```
156
+
157
+ ### Scripts
158
+
159
+ | Command | Description |
160
+ | --- | --- |
161
+ | `npm run build` | Build the CLI with tsup |
162
+ | `npm test` | Build and run tests with vitest |
163
+ | `npm run lint` | Lint source files with ESLint |
164
+ | `npm run typecheck` | Type-check with tsc (no emit) |
165
+ | `npm run format` | Check formatting with Prettier |
75
166
 
76
- ## 📝 Documentation
167
+ ## License
77
168
 
78
- * [Installing](docs/INSTALLING.md) | [Agent Workflow](AGENT_INSTRUCTIONS.md) | [Copilot Setup](docs/COPILOT_INTEGRATION.md) | [Articles](ARTICLES.md) | [Sync Branch Mode](docs/PROTECTED_BRANCHES.md) | [Troubleshooting](docs/TROUBLESHOOTING.md) | [FAQ](docs/FAQ.md)
79
- * [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/steveyegge/beads)
169
+ [MIT](LICENSE)
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 } from "commander";
4
+ import { Command as Command9 } from "commander";
5
5
 
6
6
  // src/version.ts
7
7
  import { readFileSync } from "fs";
@@ -11,9 +11,1032 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  var pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf-8"));
12
12
  var version = pkg.version;
13
13
 
14
+ // src/commands/get-item.ts
15
+ import { Command } from "commander";
16
+
17
+ // src/services/azdo-client.ts
18
+ var DEFAULT_FIELDS = [
19
+ "System.Title",
20
+ "System.State",
21
+ "System.WorkItemType",
22
+ "System.AssignedTo",
23
+ "System.Description",
24
+ "Microsoft.VSTS.Common.AcceptanceCriteria",
25
+ "Microsoft.VSTS.TCM.ReproSteps",
26
+ "System.AreaPath",
27
+ "System.IterationPath"
28
+ ];
29
+ function authHeaders(pat) {
30
+ const token = Buffer.from(`:${pat}`).toString("base64");
31
+ return { Authorization: `Basic ${token}` };
32
+ }
33
+ async function fetchWithErrors(url, init) {
34
+ let response;
35
+ try {
36
+ response = await fetch(url, init);
37
+ } catch {
38
+ throw new Error("NETWORK_ERROR");
39
+ }
40
+ if (response.status === 401) throw new Error("AUTH_FAILED");
41
+ if (response.status === 403) throw new Error("PERMISSION_DENIED");
42
+ if (response.status === 404) throw new Error("NOT_FOUND");
43
+ return response;
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
+ }
58
+ function buildExtraFields(fields, requested) {
59
+ const result = {};
60
+ for (const name of requested) {
61
+ const val = fields[name];
62
+ if (val !== void 0 && val !== null) {
63
+ result[name] = String(val);
64
+ }
65
+ }
66
+ return Object.keys(result).length > 0 ? result : null;
67
+ }
68
+ async function getWorkItem(context, id, pat, extraFields) {
69
+ const url = new URL(
70
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
71
+ );
72
+ url.searchParams.set("api-version", "7.1");
73
+ const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
74
+ if (normalizedExtraFields.length > 0) {
75
+ const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
76
+ url.searchParams.set("fields", allFields.join(","));
77
+ }
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
+ }
85
+ if (!response.ok) {
86
+ throw new Error(`HTTP_${response.status}`);
87
+ }
88
+ const data = await response.json();
89
+ const descriptionParts = [];
90
+ if (data.fields["System.Description"]) {
91
+ descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
92
+ }
93
+ if (data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"]) {
94
+ descriptionParts.push({ label: "Acceptance Criteria", value: data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] });
95
+ }
96
+ if (data.fields["Microsoft.VSTS.TCM.ReproSteps"]) {
97
+ descriptionParts.push({ label: "Repro Steps", value: data.fields["Microsoft.VSTS.TCM.ReproSteps"] });
98
+ }
99
+ let combinedDescription = null;
100
+ if (descriptionParts.length === 1) {
101
+ combinedDescription = descriptionParts[0].value;
102
+ } else if (descriptionParts.length > 1) {
103
+ combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
104
+ }
105
+ return {
106
+ id: data.id,
107
+ rev: data.rev,
108
+ title: data.fields["System.Title"],
109
+ state: data.fields["System.State"],
110
+ type: data.fields["System.WorkItemType"],
111
+ assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
112
+ description: combinedDescription,
113
+ areaPath: data.fields["System.AreaPath"],
114
+ iterationPath: data.fields["System.IterationPath"],
115
+ url: data._links.html.href,
116
+ extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
117
+ };
118
+ }
119
+ async function getWorkItemFieldValue(context, id, pat, fieldName) {
120
+ const url = new URL(
121
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
122
+ );
123
+ url.searchParams.set("api-version", "7.1");
124
+ url.searchParams.set("fields", fieldName);
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
+ }
132
+ if (!response.ok) {
133
+ throw new Error(`HTTP_${response.status}`);
134
+ }
135
+ const data = await response.json();
136
+ const value = data.fields[fieldName];
137
+ if (value === void 0 || value === null || value === "") {
138
+ return null;
139
+ }
140
+ return typeof value === "object" ? JSON.stringify(value) : `${value}`;
141
+ }
142
+ async function updateWorkItem(context, id, pat, fieldName, operations) {
143
+ const url = new URL(
144
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
145
+ );
146
+ url.searchParams.set("api-version", "7.1");
147
+ const response = await fetchWithErrors(url.toString(), {
148
+ method: "PATCH",
149
+ headers: {
150
+ ...authHeaders(pat),
151
+ "Content-Type": "application/json-patch+json"
152
+ },
153
+ body: JSON.stringify(operations)
154
+ });
155
+ if (response.status === 400) {
156
+ const serverMessage = await readResponseMessage(response) ?? "Unknown error";
157
+ throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
158
+ }
159
+ if (!response.ok) {
160
+ throw new Error(`HTTP_${response.status}`);
161
+ }
162
+ const data = await response.json();
163
+ const lastOp = operations[operations.length - 1];
164
+ const fieldValue = lastOp.value ?? null;
165
+ return {
166
+ id: data.id,
167
+ rev: data.rev,
168
+ title: data.fields["System.Title"],
169
+ fieldName,
170
+ fieldValue
171
+ };
172
+ }
173
+
174
+ // src/services/auth.ts
175
+ import { createInterface } from "readline";
176
+
177
+ // src/services/credential-store.ts
178
+ import { Entry } from "@napi-rs/keyring";
179
+ var SERVICE = "azdo-cli";
180
+ var ACCOUNT = "pat";
181
+ async function getPat() {
182
+ try {
183
+ const entry = new Entry(SERVICE, ACCOUNT);
184
+ return entry.getPassword();
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ async function storePat(pat) {
190
+ try {
191
+ const entry = new Entry(SERVICE, ACCOUNT);
192
+ entry.setPassword(pat);
193
+ } catch {
194
+ }
195
+ }
196
+ async function deletePat() {
197
+ try {
198
+ const entry = new Entry(SERVICE, ACCOUNT);
199
+ entry.deletePassword();
200
+ return true;
201
+ } catch {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ // src/services/auth.ts
207
+ function normalizePat(rawPat) {
208
+ const trimmedPat = rawPat.trim();
209
+ return trimmedPat.length > 0 ? trimmedPat : null;
210
+ }
211
+ async function promptForPat() {
212
+ if (!process.stdin.isTTY) {
213
+ return null;
214
+ }
215
+ return new Promise((resolve2) => {
216
+ const rl = createInterface({
217
+ input: process.stdin,
218
+ output: process.stderr
219
+ });
220
+ process.stderr.write("Enter your Azure DevOps PAT: ");
221
+ process.stdin.setRawMode(true);
222
+ process.stdin.resume();
223
+ let pat = "";
224
+ const onData = (key) => {
225
+ const ch = key.toString("utf8");
226
+ if (ch === "") {
227
+ process.stdin.setRawMode(false);
228
+ process.stdin.removeListener("data", onData);
229
+ rl.close();
230
+ process.stderr.write("\n");
231
+ resolve2(null);
232
+ } else if (ch === "\r" || ch === "\n") {
233
+ process.stdin.setRawMode(false);
234
+ process.stdin.removeListener("data", onData);
235
+ rl.close();
236
+ process.stderr.write("\n");
237
+ resolve2(pat);
238
+ } else if (ch === "\x7F" || ch === "\b") {
239
+ if (pat.length > 0) {
240
+ pat = pat.slice(0, -1);
241
+ process.stderr.write("\b \b");
242
+ }
243
+ } else {
244
+ pat += ch;
245
+ process.stderr.write("*".repeat(ch.length));
246
+ }
247
+ };
248
+ process.stdin.on("data", onData);
249
+ });
250
+ }
251
+ async function resolvePat() {
252
+ const envPat = process.env.AZDO_PAT;
253
+ if (envPat) {
254
+ return { pat: envPat, source: "env" };
255
+ }
256
+ const storedPat = await getPat();
257
+ if (storedPat !== null) {
258
+ return { pat: storedPat, source: "credential-store" };
259
+ }
260
+ const promptedPat = await promptForPat();
261
+ if (promptedPat !== null) {
262
+ const normalizedPat = normalizePat(promptedPat);
263
+ if (normalizedPat !== null) {
264
+ await storePat(normalizedPat);
265
+ return { pat: normalizedPat, source: "prompt" };
266
+ }
267
+ }
268
+ throw new Error(
269
+ "Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
270
+ );
271
+ }
272
+
273
+ // src/services/git-remote.ts
274
+ import { execSync } from "child_process";
275
+ var patterns = [
276
+ // HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
277
+ /^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/.+$/,
278
+ // HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
279
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/.+$/,
280
+ // HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
281
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/.+$/,
282
+ // SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
283
+ /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/.+$/,
284
+ // SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
285
+ /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/.+$/
286
+ ];
287
+ function parseAzdoRemote(url) {
288
+ for (const pattern of patterns) {
289
+ const match = url.match(pattern);
290
+ if (match) {
291
+ const project = match[2];
292
+ if (/^DefaultCollection$/i.test(project)) {
293
+ return { org: match[1], project: "" };
294
+ }
295
+ return { org: match[1], project };
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ function detectAzdoContext() {
301
+ let remoteUrl;
302
+ try {
303
+ remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
304
+ } catch {
305
+ throw new Error("Not in a git repository. Provide --org and --project explicitly.");
306
+ }
307
+ const context = parseAzdoRemote(remoteUrl);
308
+ if (!context || !context.org && !context.project) {
309
+ throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
310
+ }
311
+ return context;
312
+ }
313
+
314
+ // src/services/config-store.ts
315
+ import fs from "fs";
316
+ import path from "path";
317
+ import os from "os";
318
+ var SETTINGS = [
319
+ {
320
+ key: "org",
321
+ description: "Azure DevOps organization name",
322
+ type: "string",
323
+ example: "mycompany",
324
+ required: true
325
+ },
326
+ {
327
+ key: "project",
328
+ description: "Azure DevOps project name",
329
+ type: "string",
330
+ example: "MyProject",
331
+ required: true
332
+ },
333
+ {
334
+ key: "fields",
335
+ description: "Extra work item fields to include (comma-separated reference names)",
336
+ type: "string[]",
337
+ example: "System.Tags,Custom.Priority",
338
+ required: false
339
+ }
340
+ ];
341
+ var VALID_KEYS = SETTINGS.map((s) => s.key);
342
+ function getConfigPath() {
343
+ return path.join(os.homedir(), ".azdo", "config.json");
344
+ }
345
+ function loadConfig() {
346
+ const configPath = getConfigPath();
347
+ let raw;
348
+ try {
349
+ raw = fs.readFileSync(configPath, "utf-8");
350
+ } catch (err) {
351
+ if (err.code === "ENOENT") {
352
+ return {};
353
+ }
354
+ throw err;
355
+ }
356
+ try {
357
+ return JSON.parse(raw);
358
+ } catch {
359
+ process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
360
+ `);
361
+ return {};
362
+ }
363
+ }
364
+ function saveConfig(config) {
365
+ const configPath = getConfigPath();
366
+ const dir = path.dirname(configPath);
367
+ fs.mkdirSync(dir, { recursive: true });
368
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
369
+ }
370
+ function validateKey(key) {
371
+ if (!VALID_KEYS.includes(key)) {
372
+ throw new Error(`Unknown setting key "${key}". Valid keys: org, project, fields`);
373
+ }
374
+ }
375
+ function getConfigValue(key) {
376
+ validateKey(key);
377
+ const config = loadConfig();
378
+ return config[key];
379
+ }
380
+ function setConfigValue(key, value) {
381
+ validateKey(key);
382
+ const config = loadConfig();
383
+ if (value === "") {
384
+ delete config[key];
385
+ } else if (key === "fields") {
386
+ config.fields = value.split(",").map((s) => s.trim());
387
+ } else {
388
+ config[key] = value;
389
+ }
390
+ saveConfig(config);
391
+ }
392
+ function unsetConfigValue(key) {
393
+ validateKey(key);
394
+ const config = loadConfig();
395
+ delete config[key];
396
+ saveConfig(config);
397
+ }
398
+
399
+ // src/services/context.ts
400
+ function resolveContext(options) {
401
+ if (options.org && options.project) {
402
+ return { org: options.org, project: options.project };
403
+ }
404
+ const config = loadConfig();
405
+ if (config.org && config.project) {
406
+ return { org: config.org, project: config.project };
407
+ }
408
+ let gitContext = null;
409
+ try {
410
+ gitContext = detectAzdoContext();
411
+ } catch {
412
+ }
413
+ const org = config.org || gitContext?.org;
414
+ const project = config.project || gitContext?.project;
415
+ if (org && project) {
416
+ return { org, project };
417
+ }
418
+ throw new Error(
419
+ 'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
420
+ );
421
+ }
422
+
423
+ // src/services/command-helpers.ts
424
+ function parseWorkItemId(idStr) {
425
+ const id = Number.parseInt(idStr, 10);
426
+ if (!Number.isInteger(id) || id <= 0) {
427
+ process.stderr.write(
428
+ `Error: Work item ID must be a positive integer. Got: "${idStr}"
429
+ `
430
+ );
431
+ process.exit(1);
432
+ }
433
+ return id;
434
+ }
435
+ function validateOrgProjectPair(options) {
436
+ const hasOrg = options.org !== void 0;
437
+ const hasProject = options.project !== void 0;
438
+ if (hasOrg !== hasProject) {
439
+ process.stderr.write(
440
+ "Error: --org and --project must both be provided, or both omitted.\n"
441
+ );
442
+ process.exit(1);
443
+ }
444
+ }
445
+ function handleCommandError(err, id, context, scope = "write", exit = true) {
446
+ const error = err instanceof Error ? err : new Error(String(err));
447
+ const msg = error.message;
448
+ const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
449
+ if (msg === "AUTH_FAILED") {
450
+ process.stderr.write(
451
+ `Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
452
+ `
453
+ );
454
+ } else if (msg === "PERMISSION_DENIED") {
455
+ process.stderr.write(
456
+ `Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
457
+ `
458
+ );
459
+ } else if (msg === "NOT_FOUND") {
460
+ process.stderr.write(
461
+ `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
462
+ `
463
+ );
464
+ } else if (msg === "NETWORK_ERROR") {
465
+ process.stderr.write(
466
+ "Error: Could not connect to Azure DevOps. Check your network connection.\n"
467
+ );
468
+ } else if (msg.startsWith("BAD_REQUEST:")) {
469
+ const serverMsg = msg.replace("BAD_REQUEST: ", "");
470
+ process.stderr.write(`Error: Request rejected: ${serverMsg}
471
+ `);
472
+ } else if (msg.startsWith("UPDATE_REJECTED:")) {
473
+ const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
474
+ process.stderr.write(`Error: Update rejected: ${serverMsg}
475
+ `);
476
+ } else {
477
+ process.stderr.write(`Error: ${msg}
478
+ `);
479
+ }
480
+ if (exit) {
481
+ process.exit(1);
482
+ } else {
483
+ process.exitCode = 1;
484
+ }
485
+ }
486
+
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
+ }
495
+ function stripHtml(html) {
496
+ let text = html;
497
+ text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
498
+ text = text.replace(/<br\s*\/?>/gi, "\n");
499
+ text = text.replace(/<\/?(p|div)>/gi, "\n");
500
+ text = text.replace(/<li>/gi, "\n");
501
+ text = text.replace(/<[^>]*>/g, "");
502
+ text = text.replace(/&amp;/g, "&");
503
+ text = text.replace(/&lt;/g, "<");
504
+ text = text.replace(/&gt;/g, ">");
505
+ text = text.replace(/&quot;/g, '"');
506
+ text = text.replace(/&#39;/g, "'");
507
+ text = text.replace(/&nbsp;/g, " ");
508
+ text = text.replace(/\n{3,}/g, "\n\n");
509
+ return text.trim();
510
+ }
511
+ function formatWorkItem(workItem, short) {
512
+ const lines = [];
513
+ const label = (name) => name.padEnd(13);
514
+ lines.push(`${label("ID:")}${workItem.id}`);
515
+ lines.push(`${label("Type:")}${workItem.type}`);
516
+ lines.push(`${label("Title:")}${workItem.title}`);
517
+ lines.push(`${label("State:")}${workItem.state}`);
518
+ lines.push(`${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`);
519
+ if (!short) {
520
+ lines.push(`${label("Area:")}${workItem.areaPath}`);
521
+ lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
522
+ }
523
+ lines.push(`${label("URL:")}${workItem.url}`);
524
+ if (workItem.extraFields) {
525
+ for (const [refName, value] of Object.entries(workItem.extraFields)) {
526
+ const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
527
+ lines.push(`${fieldLabel.padEnd(13)}${value}`);
528
+ }
529
+ }
530
+ lines.push("");
531
+ const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
532
+ if (short) {
533
+ const descLines = descriptionText.split("\n").filter((l) => l.trim() !== "");
534
+ const firstThree = descLines.slice(0, 3);
535
+ const truncated = descLines.length > 3;
536
+ const descSummary = firstThree.join("\n") + (truncated ? "\n..." : "");
537
+ lines.push(`${label("Description:")}${descSummary}`);
538
+ } else {
539
+ lines.push("Description:");
540
+ lines.push(descriptionText);
541
+ }
542
+ return lines.join("\n");
543
+ }
544
+ function createGetItemCommand() {
545
+ const command = new Command("get-item");
546
+ 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(
547
+ async (idStr, options) => {
548
+ const id = parseWorkItemId(idStr);
549
+ validateOrgProjectPair(options);
550
+ let context;
551
+ try {
552
+ context = resolveContext(options);
553
+ const credential = await resolvePat();
554
+ const fieldsList = options.fields !== void 0 ? parseRequestedFields(options.fields) : parseRequestedFields(loadConfig().fields);
555
+ const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
556
+ const output = formatWorkItem(workItem, options.short ?? false);
557
+ process.stdout.write(output + "\n");
558
+ } catch (err) {
559
+ handleCommandError(err, id, context, "read", false);
560
+ }
561
+ }
562
+ );
563
+ return command;
564
+ }
565
+
566
+ // src/commands/clear-pat.ts
567
+ import { Command as Command2 } from "commander";
568
+ function createClearPatCommand() {
569
+ const command = new Command2("clear-pat");
570
+ command.description("Remove the stored Azure DevOps PAT from the credential store").action(async () => {
571
+ const deleted = await deletePat();
572
+ if (deleted) {
573
+ process.stdout.write("PAT removed from credential store.\n");
574
+ } else {
575
+ process.stdout.write("No stored PAT found.\n");
576
+ }
577
+ });
578
+ return command;
579
+ }
580
+
581
+ // src/commands/config.ts
582
+ import { Command as Command3 } from "commander";
583
+ import { createInterface as createInterface2 } from "readline";
584
+ function createConfigCommand() {
585
+ const config = new Command3("config");
586
+ config.description("Manage CLI settings");
587
+ const set = new Command3("set");
588
+ set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields)").argument("<value>", "setting value").option("--json", "output in JSON format").action((key, value, options) => {
589
+ try {
590
+ setConfigValue(key, value);
591
+ if (options.json) {
592
+ const output = { key, value };
593
+ if (key === "fields") {
594
+ output.value = value.split(",").map((s) => s.trim());
595
+ }
596
+ process.stdout.write(JSON.stringify(output) + "\n");
597
+ } else {
598
+ process.stdout.write(`Set "${key}" to "${value}"
599
+ `);
600
+ }
601
+ } catch (err) {
602
+ const message = err instanceof Error ? err.message : String(err);
603
+ process.stderr.write(`Error: ${message}
604
+ `);
605
+ process.exit(1);
606
+ }
607
+ });
608
+ const get = new Command3("get");
609
+ get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
610
+ try {
611
+ const value = getConfigValue(key);
612
+ if (options.json) {
613
+ process.stdout.write(
614
+ JSON.stringify({ key, value: value ?? null }) + "\n"
615
+ );
616
+ } else if (value === void 0) {
617
+ process.stdout.write(`Setting "${key}" is not configured.
618
+ `);
619
+ } else if (Array.isArray(value)) {
620
+ process.stdout.write(value.join(",") + "\n");
621
+ } else {
622
+ process.stdout.write(value + "\n");
623
+ }
624
+ } catch (err) {
625
+ const message = err instanceof Error ? err.message : String(err);
626
+ process.stderr.write(`Error: ${message}
627
+ `);
628
+ process.exit(1);
629
+ }
630
+ });
631
+ const list = new Command3("list");
632
+ list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
633
+ const cfg = loadConfig();
634
+ if (options.json) {
635
+ process.stdout.write(JSON.stringify(cfg) + "\n");
636
+ } else {
637
+ const keyWidth = 10;
638
+ const valueWidth = 30;
639
+ for (const setting of SETTINGS) {
640
+ const raw = cfg[setting.key];
641
+ const value = raw === void 0 ? "(not set)" : Array.isArray(raw) ? raw.join(",") : raw;
642
+ const marker = raw === void 0 && setting.required ? " *" : "";
643
+ process.stdout.write(
644
+ `${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
645
+ `
646
+ );
647
+ }
648
+ const hasUnset = SETTINGS.some(
649
+ (s) => s.required && cfg[s.key] === void 0
650
+ );
651
+ if (hasUnset) {
652
+ process.stdout.write(
653
+ '\n* = required but not configured. Run "azdo config wizard" to set up.\n'
654
+ );
655
+ }
656
+ }
657
+ });
658
+ const unset = new Command3("unset");
659
+ unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
660
+ try {
661
+ unsetConfigValue(key);
662
+ if (options.json) {
663
+ process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
664
+ } else {
665
+ process.stdout.write(`Unset "${key}"
666
+ `);
667
+ }
668
+ } catch (err) {
669
+ const message = err instanceof Error ? err.message : String(err);
670
+ process.stderr.write(`Error: ${message}
671
+ `);
672
+ process.exit(1);
673
+ }
674
+ });
675
+ const wizard = new Command3("wizard");
676
+ wizard.description("Interactive wizard to configure all settings").action(async () => {
677
+ if (!process.stdin.isTTY) {
678
+ process.stderr.write(
679
+ "Error: Wizard requires an interactive terminal.\n"
680
+ );
681
+ process.exit(1);
682
+ }
683
+ const cfg = loadConfig();
684
+ const rl = createInterface2({
685
+ input: process.stdin,
686
+ output: process.stderr
687
+ });
688
+ const ask = (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
689
+ process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
690
+ process.stderr.write("=======================================\n\n");
691
+ for (const setting of SETTINGS) {
692
+ const current = cfg[setting.key];
693
+ const currentDisplay = current === void 0 ? "" : Array.isArray(current) ? current.join(",") : current;
694
+ const requiredTag = setting.required ? " (required)" : " (optional)";
695
+ process.stderr.write(`${setting.description}${requiredTag}
696
+ `);
697
+ if (setting.example) {
698
+ process.stderr.write(` Example: ${setting.example}
699
+ `);
700
+ }
701
+ const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
702
+ const answer = await ask(` ${setting.key}${defaultHint}: `);
703
+ const trimmed = answer.trim();
704
+ if (trimmed) {
705
+ setConfigValue(setting.key, trimmed);
706
+ process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
707
+
708
+ `);
709
+ } else if (currentDisplay) {
710
+ process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
711
+
712
+ `);
713
+ } else {
714
+ process.stderr.write(` -> Skipped "${setting.key}"
715
+
716
+ `);
717
+ }
718
+ }
719
+ rl.close();
720
+ process.stderr.write("Configuration complete!\n");
721
+ });
722
+ config.addCommand(set);
723
+ config.addCommand(get);
724
+ config.addCommand(list);
725
+ config.addCommand(unset);
726
+ config.addCommand(wizard);
727
+ return config;
728
+ }
729
+
730
+ // src/commands/set-state.ts
731
+ import { Command as Command4 } from "commander";
732
+ function createSetStateCommand() {
733
+ const command = new Command4("set-state");
734
+ command.description("Change the state of a work item").argument("<id>", "work item ID").argument("<state>", 'target state (e.g., "Active", "Closed")').option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
735
+ async (idStr, state, options) => {
736
+ const id = parseWorkItemId(idStr);
737
+ validateOrgProjectPair(options);
738
+ let context;
739
+ try {
740
+ context = resolveContext(options);
741
+ const credential = await resolvePat();
742
+ const operations = [
743
+ { op: "add", path: "/fields/System.State", value: state }
744
+ ];
745
+ const result = await updateWorkItem(context, id, credential.pat, "System.State", operations);
746
+ if (options.json) {
747
+ process.stdout.write(
748
+ JSON.stringify({
749
+ id: result.id,
750
+ rev: result.rev,
751
+ title: result.title,
752
+ field: result.fieldName,
753
+ value: result.fieldValue
754
+ }) + "\n"
755
+ );
756
+ } else {
757
+ process.stdout.write(`Updated work item ${result.id}: State -> ${state}
758
+ `);
759
+ }
760
+ } catch (err) {
761
+ handleCommandError(err, id, context, "write");
762
+ }
763
+ }
764
+ );
765
+ return command;
766
+ }
767
+
768
+ // src/commands/assign.ts
769
+ import { Command as Command5 } from "commander";
770
+ function createAssignCommand() {
771
+ const command = new Command5("assign");
772
+ command.description("Assign a work item to a user, or unassign it").argument("<id>", "work item ID").argument("[name]", "user display name or email").option("--unassign", "clear the Assigned To field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
773
+ async (idStr, name, options) => {
774
+ const id = parseWorkItemId(idStr);
775
+ if (!name && !options.unassign) {
776
+ process.stderr.write(
777
+ "Error: Either provide a user name or use --unassign.\n"
778
+ );
779
+ process.exit(1);
780
+ }
781
+ if (name && options.unassign) {
782
+ process.stderr.write(
783
+ "Error: Cannot provide both a user name and --unassign.\n"
784
+ );
785
+ process.exit(1);
786
+ }
787
+ validateOrgProjectPair(options);
788
+ let context;
789
+ try {
790
+ context = resolveContext(options);
791
+ const credential = await resolvePat();
792
+ const value = options.unassign ? "" : name;
793
+ const operations = [
794
+ { op: "add", path: "/fields/System.AssignedTo", value }
795
+ ];
796
+ const result = await updateWorkItem(context, id, credential.pat, "System.AssignedTo", operations);
797
+ if (options.json) {
798
+ process.stdout.write(
799
+ JSON.stringify({
800
+ id: result.id,
801
+ rev: result.rev,
802
+ title: result.title,
803
+ field: result.fieldName,
804
+ value: result.fieldValue
805
+ }) + "\n"
806
+ );
807
+ } else {
808
+ const displayValue = options.unassign ? "(unassigned)" : name;
809
+ process.stdout.write(`Updated work item ${result.id}: Assigned To -> ${displayValue}
810
+ `);
811
+ }
812
+ } catch (err) {
813
+ handleCommandError(err, id, context, "write");
814
+ }
815
+ }
816
+ );
817
+ return command;
818
+ }
819
+
820
+ // src/commands/set-field.ts
821
+ import { Command as Command6 } from "commander";
822
+ function createSetFieldCommand() {
823
+ const command = new Command6("set-field");
824
+ command.description("Set any work item field by its reference name").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Title)").argument("<value>", "new value for the field").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
825
+ async (idStr, field, value, options) => {
826
+ const id = parseWorkItemId(idStr);
827
+ validateOrgProjectPair(options);
828
+ let context;
829
+ try {
830
+ context = resolveContext(options);
831
+ const credential = await resolvePat();
832
+ const operations = [
833
+ { op: "add", path: `/fields/${field}`, value }
834
+ ];
835
+ const result = await updateWorkItem(context, id, credential.pat, field, operations);
836
+ if (options.json) {
837
+ process.stdout.write(
838
+ JSON.stringify({
839
+ id: result.id,
840
+ rev: result.rev,
841
+ title: result.title,
842
+ field: result.fieldName,
843
+ value: result.fieldValue
844
+ }) + "\n"
845
+ );
846
+ } else {
847
+ process.stdout.write(`Updated work item ${result.id}: ${field} -> ${value}
848
+ `);
849
+ }
850
+ } catch (err) {
851
+ handleCommandError(err, id, context, "write");
852
+ }
853
+ }
854
+ );
855
+ return command;
856
+ }
857
+
858
+ // src/commands/get-md-field.ts
859
+ import { Command as Command7 } from "commander";
860
+
861
+ // src/services/md-convert.ts
862
+ import { NodeHtmlMarkdown } from "node-html-markdown";
863
+
864
+ // src/services/html-detect.ts
865
+ var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
866
+ var HTML_TAGS = /* @__PURE__ */ new Set([
867
+ "p",
868
+ "br",
869
+ "div",
870
+ "span",
871
+ "strong",
872
+ "em",
873
+ "b",
874
+ "i",
875
+ "u",
876
+ "a",
877
+ "ul",
878
+ "ol",
879
+ "li",
880
+ "h1",
881
+ "h2",
882
+ "h3",
883
+ "h4",
884
+ "h5",
885
+ "h6",
886
+ "table",
887
+ "tr",
888
+ "td",
889
+ "th",
890
+ "img",
891
+ "pre",
892
+ "code"
893
+ ]);
894
+ function isHtml(content) {
895
+ let match;
896
+ HTML_TAG_REGEX.lastIndex = 0;
897
+ while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
898
+ if (HTML_TAGS.has(match[1].toLowerCase())) {
899
+ return true;
900
+ }
901
+ }
902
+ return false;
903
+ }
904
+
905
+ // src/services/md-convert.ts
906
+ function htmlToMarkdown(html) {
907
+ return NodeHtmlMarkdown.translate(html);
908
+ }
909
+ function toMarkdown(content) {
910
+ if (isHtml(content)) {
911
+ return htmlToMarkdown(content);
912
+ }
913
+ return content;
914
+ }
915
+
916
+ // src/commands/get-md-field.ts
917
+ function createGetMdFieldCommand() {
918
+ const command = new Command7("get-md-field");
919
+ 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(
920
+ async (idStr, field, options) => {
921
+ const id = parseWorkItemId(idStr);
922
+ validateOrgProjectPair(options);
923
+ let context;
924
+ try {
925
+ context = resolveContext(options);
926
+ const credential = await resolvePat();
927
+ const value = await getWorkItemFieldValue(context, id, credential.pat, field);
928
+ if (value === null) {
929
+ process.stdout.write("\n");
930
+ } else {
931
+ process.stdout.write(toMarkdown(value) + "\n");
932
+ }
933
+ } catch (err) {
934
+ handleCommandError(err, id, context, "read");
935
+ }
936
+ }
937
+ );
938
+ return command;
939
+ }
940
+
941
+ // src/commands/set-md-field.ts
942
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
943
+ import { Command as Command8 } from "commander";
944
+ function fail(message) {
945
+ process.stderr.write(`Error: ${message}
946
+ `);
947
+ process.exit(1);
948
+ }
949
+ function resolveContent(inlineContent, options) {
950
+ if (inlineContent && options.file) {
951
+ fail("Cannot specify both inline content and --file.");
952
+ }
953
+ if (options.file) {
954
+ return readFileContent(options.file);
955
+ }
956
+ if (inlineContent) {
957
+ return inlineContent;
958
+ }
959
+ return null;
960
+ }
961
+ function readFileContent(filePath) {
962
+ if (!existsSync(filePath)) {
963
+ fail(`File not found: ${filePath}`);
964
+ }
965
+ try {
966
+ return readFileSync2(filePath, "utf-8");
967
+ } catch {
968
+ fail(`Cannot read file: ${filePath}`);
969
+ }
970
+ }
971
+ async function readStdinContent() {
972
+ if (process.stdin.isTTY) {
973
+ fail(
974
+ "No content provided. Pass markdown content as the third argument, use --file, or pipe via stdin."
975
+ );
976
+ }
977
+ const chunks = [];
978
+ for await (const chunk of process.stdin) {
979
+ chunks.push(chunk);
980
+ }
981
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trimEnd();
982
+ if (!stdinContent) {
983
+ fail(
984
+ "No content provided via stdin. Pipe markdown content or use inline content or --file."
985
+ );
986
+ }
987
+ return stdinContent;
988
+ }
989
+ function formatOutput(result, options, field) {
990
+ if (options.json) {
991
+ process.stdout.write(
992
+ JSON.stringify({
993
+ id: result.id,
994
+ rev: result.rev,
995
+ field: result.fieldName,
996
+ value: result.fieldValue
997
+ }) + "\n"
998
+ );
999
+ } else {
1000
+ process.stdout.write(`Updated work item ${result.id}: ${field} set with markdown content
1001
+ `);
1002
+ }
1003
+ }
1004
+ function createSetMdFieldCommand() {
1005
+ const command = new Command8("set-md-field");
1006
+ command.description("Set a work item field with markdown content").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").argument("[content]", "markdown content to set").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").option("--file <path>", "read markdown content from file").action(
1007
+ async (idStr, field, inlineContent, options) => {
1008
+ const id = parseWorkItemId(idStr);
1009
+ validateOrgProjectPair(options);
1010
+ const content = resolveContent(inlineContent, options) ?? await readStdinContent();
1011
+ let context;
1012
+ try {
1013
+ context = resolveContext(options);
1014
+ const credential = await resolvePat();
1015
+ const operations = [
1016
+ { op: "add", path: `/fields/${field}`, value: content },
1017
+ { op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
1018
+ ];
1019
+ const result = await updateWorkItem(context, id, credential.pat, field, operations);
1020
+ formatOutput(result, options, field);
1021
+ } catch (err) {
1022
+ handleCommandError(err, id, context, "write");
1023
+ }
1024
+ }
1025
+ );
1026
+ return command;
1027
+ }
1028
+
14
1029
  // src/index.ts
15
- var program = new Command();
1030
+ var program = new Command9();
16
1031
  program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
1032
+ program.addCommand(createGetItemCommand());
1033
+ program.addCommand(createClearPatCommand());
1034
+ program.addCommand(createConfigCommand());
1035
+ program.addCommand(createSetStateCommand());
1036
+ program.addCommand(createAssignCommand());
1037
+ program.addCommand(createSetFieldCommand());
1038
+ program.addCommand(createGetMdFieldCommand());
1039
+ program.addCommand(createSetMdFieldCommand());
17
1040
  program.showHelpAfterError();
18
1041
  program.parse();
19
1042
  if (process.argv.length <= 2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.2.0-develop.8",
3
+ "version": "0.2.0-hotfix-0-2-5.63",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "lint": "eslint src/",
15
15
  "typecheck": "tsc --noEmit",
16
16
  "format": "prettier --check src/",
17
- "test": "vitest run"
17
+ "test": "npm run build && vitest run"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -22,7 +22,9 @@
22
22
  },
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "commander": "^14.0.3"
25
+ "@napi-rs/keyring": "^1.2.0",
26
+ "commander": "^14.0.3",
27
+ "node-html-markdown": "^2.0.0"
26
28
  },
27
29
  "devDependencies": {
28
30
  "@eslint/js": "^10.0.1",