azdo-cli 0.8.0-develop.167 → 0.8.0-develop.176
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 +14 -342
- package/dist/index.js +67 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,13 +9,10 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
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`)
|
|
12
|
+
- Update work item state, assignee, or any field (`set-state`, `assign`, `set-field`)
|
|
15
13
|
- Create or update work items from markdown documents (`upsert`)
|
|
16
14
|
- Read and post work item comments (`comments`)
|
|
17
|
-
- Read rich-text fields as markdown (`get-md-field`)
|
|
18
|
-
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
15
|
+
- Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
|
|
19
16
|
- Check branch pull request status, open PRs to `develop`, and review active comments (`pr`)
|
|
20
17
|
- Persist org/project/default fields in local config (`config`)
|
|
21
18
|
- List all fields of a work item (`list-fields`)
|
|
@@ -27,360 +24,35 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
27
24
|
npm install -g azdo-cli
|
|
28
25
|
```
|
|
29
26
|
|
|
30
|
-
## Utility Scripts
|
|
31
|
-
|
|
32
|
-
The repository also includes a helper script for syncing local `.env` entries into GitHub Actions secrets for the current repository:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
./scripts/sync-env-to-gh-secrets.zsh
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
It walks upward from the current directory until it finds a `.env`, then sets each valid `KEY=VALUE` entry with `gh secret set`. You can also limit the sync to selected keys:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
./scripts/sync-env-to-gh-secrets.zsh FOO BAR
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Authentication and Context Resolution
|
|
45
|
-
|
|
46
|
-
PAT resolution order:
|
|
47
|
-
1. `AZDO_PAT` environment variable
|
|
48
|
-
2. Stored credential from OS keyring
|
|
49
|
-
3. Interactive PAT prompt (then stored for next runs)
|
|
50
|
-
|
|
51
|
-
Org/project resolution order:
|
|
52
|
-
1. `--org` + `--project` flags
|
|
53
|
-
2. Saved config (`azdo config set org ...`, `azdo config set project ...`)
|
|
54
|
-
3. Azure DevOps `origin` git remote auto-detection
|
|
55
|
-
|
|
56
27
|
## Quick Start
|
|
57
28
|
|
|
58
29
|
```bash
|
|
59
|
-
#
|
|
30
|
+
# Configure defaults once
|
|
60
31
|
azdo config set org myorg
|
|
61
32
|
azdo config set project myproject
|
|
62
33
|
|
|
63
|
-
#
|
|
34
|
+
# Read a work item
|
|
64
35
|
azdo get-item 12345
|
|
65
36
|
|
|
66
|
-
#
|
|
37
|
+
# Update state
|
|
67
38
|
azdo set-state 12345 "Active"
|
|
68
39
|
|
|
69
|
-
#
|
|
40
|
+
# Create a work item from markdown
|
|
70
41
|
azdo upsert --type "User Story" --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
71
42
|
|
|
72
|
-
#
|
|
43
|
+
# Read and post comments
|
|
73
44
|
azdo comments list 12345
|
|
74
45
|
azdo comments add 12345 "Investigating the root cause now."
|
|
75
46
|
```
|
|
76
47
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
| Command | Purpose | Common Flags |
|
|
80
|
-
| --- | --- | --- |
|
|
81
|
-
| `azdo get-item <id>` | Read a work item | `--short`, `--fields`, `--markdown`, `--org`, `--project` |
|
|
82
|
-
| `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
|
|
83
|
-
| `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
|
|
84
|
-
| `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
|
|
85
|
-
| `azdo upsert [id]` | Create or update a work item from markdown | `--content`, `--file`, `--type`, `--json`, `--org`, `--project` |
|
|
86
|
-
| `azdo comments <subcommand>` | Read or add work item comments | `list`, `add`, `--json`, `--org`, `--project` |
|
|
87
|
-
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
88
|
-
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
89
|
-
| `azdo list-fields <id>` | List all fields of a work item | `--json`, `--org`, `--project` |
|
|
90
|
-
| `azdo pr <subcommand>` | Manage pull requests for the current branch | `status`, `open`, `comments`, `--json`, `--org`, `--project` |
|
|
91
|
-
| `azdo config <subcommand>` | Manage saved settings | `set`, `get`, `list`, `unset`, `wizard`, `--json` |
|
|
92
|
-
| `azdo clear-pat` | Remove stored PAT | none |
|
|
93
|
-
|
|
94
|
-
## Command Reference
|
|
95
|
-
|
|
96
|
-
### Core
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
# Get full work item
|
|
100
|
-
azdo get-item 12345
|
|
101
|
-
|
|
102
|
-
# Get short view
|
|
103
|
-
azdo get-item 12345 --short
|
|
104
|
-
|
|
105
|
-
# Include extra fields for this call
|
|
106
|
-
azdo get-item 12345 --fields "System.Tags,Microsoft.VSTS.Common.Priority"
|
|
107
|
-
|
|
108
|
-
# Convert rich text fields to markdown
|
|
109
|
-
azdo get-item 12345 --markdown
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
# Set state
|
|
115
|
-
azdo set-state 12345 "Closed"
|
|
116
|
-
|
|
117
|
-
# Assign / unassign
|
|
118
|
-
azdo assign 12345 "someone@company.com"
|
|
119
|
-
azdo assign 12345 --unassign
|
|
120
|
-
|
|
121
|
-
# Set generic field
|
|
122
|
-
azdo set-field 12345 System.Title "Updated title"
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### List Fields
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
# List all fields with values (rich text fields preview first 5 lines)
|
|
129
|
-
azdo list-fields 12345
|
|
130
|
-
|
|
131
|
-
# JSON output
|
|
132
|
-
azdo list-fields 12345 --json
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Markdown Display
|
|
136
|
-
|
|
137
|
-
The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
|
|
138
|
-
|
|
139
|
-
1. `--markdown` flag enables markdown for the current call
|
|
140
|
-
2. Config setting: `azdo config set markdown true`
|
|
141
|
-
3. Default: off (HTML stripped to plain text)
|
|
142
|
-
|
|
143
|
-
### Markdown Field Commands
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
# Read field and auto-convert HTML -> markdown
|
|
147
|
-
azdo get-md-field 12345 System.Description
|
|
148
|
-
|
|
149
|
-
# Set markdown inline
|
|
150
|
-
azdo set-md-field 12345 System.Description "# Title\n\nSome **bold** text"
|
|
151
|
-
|
|
152
|
-
# Set markdown from file
|
|
153
|
-
azdo set-md-field 12345 System.Description --file ./description.md
|
|
154
|
-
|
|
155
|
-
# Set markdown from stdin
|
|
156
|
-
cat description.md | azdo set-md-field 12345 System.Description
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Pull Request Commands
|
|
160
|
-
|
|
161
|
-
The `pr` command group uses the current git branch and the Azure DevOps `origin` remote automatically. It requires a PAT with `Code (Read)` scope for read operations and `Code (Read & Write)` for pull request creation.
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
# Check whether the current branch already has pull requests
|
|
165
|
-
azdo pr status
|
|
166
|
-
|
|
167
|
-
# Open a pull request to develop
|
|
168
|
-
azdo pr open --title "Add PR handling" --description "Implements pr status, pr open, pr comments commands"
|
|
169
|
-
|
|
170
|
-
# Review active comments for the current branch's active pull request
|
|
171
|
-
azdo pr comments
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
`azdo pr status`
|
|
175
|
-
|
|
176
|
-
- Lists pull requests for the current branch
|
|
177
|
-
- Includes Azure DevOps pull request checks under each returned pull request
|
|
178
|
-
- Prints `Checks: none reported by Azure DevOps` when a pull request has no returned checks
|
|
179
|
-
- Shows `Detail: ...` lines for failed or errored checks when Azure DevOps provides description text
|
|
180
|
-
- Prints `No pull requests found for branch <branch>.` when no PRs exist
|
|
181
|
-
- Supports `--json` for machine-readable output, including a `checks` array per pull request
|
|
182
|
-
|
|
183
|
-
`azdo pr open`
|
|
184
|
-
|
|
185
|
-
- Requires both `--title <title>` and `--description <description>`
|
|
186
|
-
- Targets `develop` automatically
|
|
187
|
-
- Creates a new active pull request when none exists
|
|
188
|
-
- Reuses the existing active PR when one already matches the branch and target
|
|
189
|
-
- Fails with a clear error when run from `develop` or when multiple active PRs already exist
|
|
190
|
-
|
|
191
|
-
`azdo pr comments`
|
|
192
|
-
|
|
193
|
-
- Resolves the single active pull request for the current branch
|
|
194
|
-
- Returns only active or pending threads with visible, non-deleted comments
|
|
195
|
-
- Groups text output by thread and shows file context when available
|
|
196
|
-
- Prints `Pull request #<id> has no active comments.` when the PR has no active comment threads
|
|
197
|
-
- Fails instead of guessing when no active PR or multiple active PRs exist
|
|
198
|
-
|
|
199
|
-
### Work Item Comment Commands
|
|
200
|
-
|
|
201
|
-
The `comments` command group works on a specific work item ID. It requires a PAT with `Work Items (read)` scope for listing comments and `Work Items (Read & Write)` to add a new comment.
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
# Read the visible comment history for a work item
|
|
205
|
-
azdo comments list 12345
|
|
206
|
-
|
|
207
|
-
# Read the same history as JSON
|
|
208
|
-
azdo comments list 12345 --json
|
|
209
|
-
|
|
210
|
-
# Post a progress update
|
|
211
|
-
azdo comments add 12345 "Investigation complete. Working on the fix next."
|
|
212
|
-
|
|
213
|
-
# Post the update and return JSON
|
|
214
|
-
azdo comments add 12345 "Queued validation run." --json
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
`azdo comments list`
|
|
218
|
-
|
|
219
|
-
- Resolves the target work item directly from the provided ID
|
|
220
|
-
- Retrieves the full visible comment history and follows Azure DevOps pagination internally
|
|
221
|
-
- Prints comments newest first with comment ID, author, timestamp, and body text
|
|
222
|
-
- Prints `Work item #<id> has no comments.` when the work item has no visible comments
|
|
223
|
-
|
|
224
|
-
`azdo comments add`
|
|
225
|
-
|
|
226
|
-
- Requires a non-empty `<text>` positional argument
|
|
227
|
-
- Preserves the supplied comment text as the submitted body
|
|
228
|
-
- Prints `Added comment #<commentId> to work item #<id>` on success
|
|
229
|
-
- Fails locally before any API call when the text is empty or whitespace-only
|
|
230
|
-
|
|
231
|
-
## azdo upsert
|
|
232
|
-
|
|
233
|
-
`azdo upsert` accepts a single markdown work-item document and either creates a new Azure DevOps work item or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place. Create mode defaults to `Task`, and `--type <work item type>` lets you create `Bug`, `User Story`, `Feature`, `Epic`, and other Azure DevOps work item types.
|
|
234
|
-
|
|
235
|
-
```bash
|
|
236
|
-
# Create a Bug from inline content
|
|
237
|
-
azdo upsert --type Bug --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
238
|
-
|
|
239
|
-
# Preserve the default Task create behavior
|
|
240
|
-
azdo upsert --content $'---\nTitle: Follow-up task\nAssigned To: user@example.com\nState: New\n---'
|
|
241
|
-
|
|
242
|
-
# Update from a file
|
|
243
|
-
azdo upsert 12345 --file ./task-import.md
|
|
244
|
-
|
|
245
|
-
# JSON output
|
|
246
|
-
azdo upsert 12345 --content $'---\nSystem.Title: Improve markdown import UX\n---' --json
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
The command requires exactly one source flag:
|
|
250
|
-
|
|
251
|
-
- `azdo upsert [id] --content <markdown>`
|
|
252
|
-
- `azdo upsert [id] --file <path>`
|
|
253
|
-
- `azdo upsert --type <work-item-type> --content <markdown>`
|
|
254
|
-
|
|
255
|
-
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.
|
|
256
|
-
|
|
257
|
-
Type rules:
|
|
258
|
-
|
|
259
|
-
- `--type` is optional for create and defaults to `Task`.
|
|
260
|
-
- `--type` is only valid when creating a new work item.
|
|
261
|
-
- Human-readable and JSON success output include the resulting work item type.
|
|
262
|
-
|
|
263
|
-
### Task Document Format
|
|
264
|
-
|
|
265
|
-
The document starts with YAML front matter for scalar fields, followed by optional `##` heading sections for markdown rich-text fields.
|
|
266
|
-
|
|
267
|
-
```md
|
|
268
|
-
---
|
|
269
|
-
Title: Improve markdown import UX
|
|
270
|
-
Assigned To: user@example.com
|
|
271
|
-
State: New
|
|
272
|
-
Tags: cli; markdown
|
|
273
|
-
Priority: null
|
|
274
|
-
---
|
|
275
|
-
|
|
276
|
-
## Description
|
|
277
|
-
|
|
278
|
-
Implement a single-command task import flow.
|
|
279
|
-
|
|
280
|
-
## Acceptance Criteria
|
|
281
|
-
|
|
282
|
-
- Supports create when no ID is passed
|
|
283
|
-
- Supports update when an ID is passed
|
|
284
|
-
- Deletes imported files only after success
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
Supported friendly field names:
|
|
288
|
-
|
|
289
|
-
- `Title`
|
|
290
|
-
- `Assigned To` / `assignedTo`
|
|
291
|
-
- `State`
|
|
292
|
-
- `Description`
|
|
293
|
-
- `Acceptance Criteria` / `acceptanceCriteria`
|
|
294
|
-
- `Tags`
|
|
295
|
-
- `Priority`
|
|
296
|
-
|
|
297
|
-
Raw Azure DevOps reference names are also accepted anywhere a field name is expected, for example `System.Title` or `Microsoft.VSTS.Common.AcceptanceCriteria`.
|
|
298
|
-
|
|
299
|
-
Clear semantics:
|
|
300
|
-
|
|
301
|
-
- Scalar YAML fields with `null` or an empty value are treated as clears on update.
|
|
302
|
-
- Rich-text heading sections with an empty body are treated as clears on update.
|
|
303
|
-
- Omitted fields are untouched on update.
|
|
304
|
-
|
|
305
|
-
`--json` output shape:
|
|
306
|
-
|
|
307
|
-
```json
|
|
308
|
-
{
|
|
309
|
-
"action": "created",
|
|
310
|
-
"id": 12345,
|
|
311
|
-
"workItemType": "User Story",
|
|
312
|
-
"fields": {
|
|
313
|
-
"System.Title": "Improve markdown import UX",
|
|
314
|
-
"System.Description": "Implement a single-command task import flow."
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Configuration
|
|
320
|
-
|
|
321
|
-
```bash
|
|
322
|
-
# List settings
|
|
323
|
-
azdo config list
|
|
324
|
-
|
|
325
|
-
# Interactive setup
|
|
326
|
-
azdo config wizard
|
|
327
|
-
|
|
328
|
-
# Enable markdown display for all get-item calls
|
|
329
|
-
azdo config set markdown true
|
|
330
|
-
|
|
331
|
-
# Set/get/unset values
|
|
332
|
-
azdo config set fields "System.Tags,Custom.Priority"
|
|
333
|
-
azdo config get fields
|
|
334
|
-
azdo config unset fields
|
|
335
|
-
|
|
336
|
-
# JSON output
|
|
337
|
-
azdo config list --json
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### Credential Management
|
|
341
|
-
|
|
342
|
-
```bash
|
|
343
|
-
# Remove stored PAT from keyring
|
|
344
|
-
azdo clear-pat
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## JSON Output
|
|
348
|
-
|
|
349
|
-
These commands support `--json` for machine-readable output:
|
|
350
|
-
- `list-fields`
|
|
351
|
-
- `set-state`
|
|
352
|
-
- `assign`
|
|
353
|
-
- `set-field`
|
|
354
|
-
- `set-md-field`
|
|
355
|
-
- `upsert`
|
|
356
|
-
- `comments list|add`
|
|
357
|
-
- `pr status|open|comments`
|
|
358
|
-
- `config set|get|list|unset`
|
|
359
|
-
|
|
360
|
-
## Development
|
|
361
|
-
|
|
362
|
-
### Prerequisites
|
|
363
|
-
|
|
364
|
-
- Node.js LTS (20+)
|
|
365
|
-
- npm
|
|
366
|
-
|
|
367
|
-
### Setup
|
|
368
|
-
|
|
369
|
-
```bash
|
|
370
|
-
git clone https://github.com/alkampfergit/azdo-cli.git
|
|
371
|
-
cd azdo-cli
|
|
372
|
-
npm install
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### Scripts
|
|
48
|
+
## Documentation
|
|
376
49
|
|
|
377
|
-
|
|
|
378
|
-
|
|
379
|
-
|
|
|
380
|
-
|
|
|
381
|
-
|
|
|
382
|
-
|
|
|
383
|
-
| `npm run format` | Check formatting with Prettier |
|
|
50
|
+
| Topic | File |
|
|
51
|
+
|-------|------|
|
|
52
|
+
| Authentication & PAT storage | [docs/authentication.md](docs/authentication.md) |
|
|
53
|
+
| Linux credential store setup | [docs/linux-credential-store.md](docs/linux-credential-store.md) |
|
|
54
|
+
| Full command reference | [docs/commands.md](docs/commands.md) |
|
|
55
|
+
| Development setup | [docs/development.md](docs/development.md) |
|
|
384
56
|
|
|
385
57
|
## License
|
|
386
58
|
|
package/dist/index.js
CHANGED
|
@@ -263,8 +263,10 @@ async function listWorkItemComments(context, id, pat) {
|
|
|
263
263
|
comments
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
|
-
async function addWorkItemComment(context, id, pat, text) {
|
|
267
|
-
const
|
|
266
|
+
async function addWorkItemComment(context, id, pat, text, format = "html") {
|
|
267
|
+
const url = buildWorkItemCommentsUrl(context, id);
|
|
268
|
+
url.searchParams.set("format", format);
|
|
269
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
268
270
|
method: "POST",
|
|
269
271
|
headers: {
|
|
270
272
|
...authHeaders(pat),
|
|
@@ -329,6 +331,8 @@ async function applyWorkItemPatch(context, id, pat, operations) {
|
|
|
329
331
|
|
|
330
332
|
// src/services/auth.ts
|
|
331
333
|
import { createInterface } from "readline";
|
|
334
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
335
|
+
import { dirname as dirname2, join } from "path";
|
|
332
336
|
|
|
333
337
|
// src/services/credential-store.ts
|
|
334
338
|
import { Entry } from "@napi-rs/keyring";
|
|
@@ -346,7 +350,9 @@ async function storePat(pat) {
|
|
|
346
350
|
try {
|
|
347
351
|
const entry = new Entry(SERVICE, ACCOUNT);
|
|
348
352
|
entry.setPassword(pat);
|
|
353
|
+
return true;
|
|
349
354
|
} catch {
|
|
355
|
+
return false;
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
358
|
async function deletePat() {
|
|
@@ -360,10 +366,19 @@ async function deletePat() {
|
|
|
360
366
|
}
|
|
361
367
|
|
|
362
368
|
// src/services/auth.ts
|
|
369
|
+
var PAT_PROMPT = "Enter your Azure DevOps PAT: ";
|
|
370
|
+
var VISIBLE_CHARS = 5;
|
|
363
371
|
function normalizePat(rawPat) {
|
|
364
372
|
const trimmedPat = rawPat.trim();
|
|
365
373
|
return trimmedPat.length > 0 ? trimmedPat : null;
|
|
366
374
|
}
|
|
375
|
+
function maskedDisplay(pat) {
|
|
376
|
+
if (pat.length <= VISIBLE_CHARS * 2) {
|
|
377
|
+
return pat;
|
|
378
|
+
}
|
|
379
|
+
const hiddenCount = pat.length - VISIBLE_CHARS * 2;
|
|
380
|
+
return pat.slice(0, VISIBLE_CHARS) + "*".repeat(hiddenCount) + pat.slice(-VISIBLE_CHARS);
|
|
381
|
+
}
|
|
367
382
|
async function promptForPat() {
|
|
368
383
|
if (!process.stdin.isTTY) {
|
|
369
384
|
return null;
|
|
@@ -373,10 +388,13 @@ async function promptForPat() {
|
|
|
373
388
|
input: process.stdin,
|
|
374
389
|
output: process.stderr
|
|
375
390
|
});
|
|
376
|
-
process.stderr.write(
|
|
391
|
+
process.stderr.write(PAT_PROMPT);
|
|
377
392
|
process.stdin.setRawMode(true);
|
|
378
393
|
process.stdin.resume();
|
|
379
394
|
let pat = "";
|
|
395
|
+
const redraw = () => {
|
|
396
|
+
process.stderr.write(`\r${PAT_PROMPT}${maskedDisplay(pat)}\x1B[K`);
|
|
397
|
+
};
|
|
380
398
|
const onData = (key) => {
|
|
381
399
|
const ch = key.toString("utf8");
|
|
382
400
|
if (ch === "") {
|
|
@@ -394,17 +412,37 @@ async function promptForPat() {
|
|
|
394
412
|
} else if (ch === "\x7F" || ch === "\b") {
|
|
395
413
|
if (pat.length > 0) {
|
|
396
414
|
pat = pat.slice(0, -1);
|
|
397
|
-
|
|
415
|
+
redraw();
|
|
398
416
|
}
|
|
399
417
|
} else {
|
|
400
418
|
pat += ch;
|
|
401
|
-
|
|
419
|
+
redraw();
|
|
402
420
|
}
|
|
403
421
|
};
|
|
404
422
|
process.stdin.on("data", onData);
|
|
405
423
|
});
|
|
406
424
|
}
|
|
407
|
-
|
|
425
|
+
function findDotEnvPat(startDir = process.cwd()) {
|
|
426
|
+
let current = startDir;
|
|
427
|
+
while (true) {
|
|
428
|
+
const envFile = join(current, ".env");
|
|
429
|
+
if (existsSync(envFile)) {
|
|
430
|
+
const contents = readFileSync2(envFile, "utf8");
|
|
431
|
+
for (const line of contents.split("\n")) {
|
|
432
|
+
const match = line.match(/^AZDO_PAT\s*=\s*(.+)$/);
|
|
433
|
+
if (match) {
|
|
434
|
+
const value = match[1].trim().replace(/^["']|["']$/g, "");
|
|
435
|
+
if (value.length > 0) return value;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const parent = dirname2(current);
|
|
440
|
+
if (parent === current) break;
|
|
441
|
+
current = parent;
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
async function resolvePat(promptFn = promptForPat) {
|
|
408
446
|
const envPat = process.env.AZDO_PAT;
|
|
409
447
|
if (envPat) {
|
|
410
448
|
return { pat: envPat, source: "env" };
|
|
@@ -413,11 +451,18 @@ async function resolvePat() {
|
|
|
413
451
|
if (storedPat !== null) {
|
|
414
452
|
return { pat: storedPat, source: "credential-store" };
|
|
415
453
|
}
|
|
416
|
-
const
|
|
454
|
+
const dotEnvPat = findDotEnvPat();
|
|
455
|
+
if (dotEnvPat !== null) {
|
|
456
|
+
return { pat: dotEnvPat, source: "env" };
|
|
457
|
+
}
|
|
458
|
+
const promptedPat = await promptFn();
|
|
417
459
|
if (promptedPat !== null) {
|
|
418
460
|
const normalizedPat = normalizePat(promptedPat);
|
|
419
461
|
if (normalizedPat !== null) {
|
|
420
|
-
await storePat(normalizedPat);
|
|
462
|
+
const saved = await storePat(normalizedPat);
|
|
463
|
+
if (!saved) {
|
|
464
|
+
process.stderr.write("Warning: Could not save PAT to credential store. You may need to enter it again next time.\n");
|
|
465
|
+
}
|
|
421
466
|
return { pat: normalizedPat, source: "prompt" };
|
|
422
467
|
}
|
|
423
468
|
}
|
|
@@ -1196,7 +1241,7 @@ function createGetMdFieldCommand() {
|
|
|
1196
1241
|
}
|
|
1197
1242
|
|
|
1198
1243
|
// src/commands/set-md-field.ts
|
|
1199
|
-
import { existsSync, readFileSync as
|
|
1244
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1200
1245
|
import { Command as Command8 } from "commander";
|
|
1201
1246
|
function fail(message) {
|
|
1202
1247
|
process.stderr.write(`Error: ${message}
|
|
@@ -1216,11 +1261,11 @@ function resolveContent(inlineContent, options) {
|
|
|
1216
1261
|
return null;
|
|
1217
1262
|
}
|
|
1218
1263
|
function readFileContent(filePath) {
|
|
1219
|
-
if (!
|
|
1264
|
+
if (!existsSync2(filePath)) {
|
|
1220
1265
|
fail(`File not found: ${filePath}`);
|
|
1221
1266
|
}
|
|
1222
1267
|
try {
|
|
1223
|
-
return
|
|
1268
|
+
return readFileSync3(filePath, "utf-8");
|
|
1224
1269
|
} catch {
|
|
1225
1270
|
fail(`Cannot read file: ${filePath}`);
|
|
1226
1271
|
}
|
|
@@ -1284,7 +1329,7 @@ function createSetMdFieldCommand() {
|
|
|
1284
1329
|
}
|
|
1285
1330
|
|
|
1286
1331
|
// src/commands/upsert.ts
|
|
1287
|
-
import { existsSync as
|
|
1332
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1288
1333
|
import { Command as Command9 } from "commander";
|
|
1289
1334
|
|
|
1290
1335
|
// src/services/task-document.ts
|
|
@@ -1448,12 +1493,12 @@ function loadSourceContent(options) {
|
|
|
1448
1493
|
return { content: options.content };
|
|
1449
1494
|
}
|
|
1450
1495
|
const filePath = options.file;
|
|
1451
|
-
if (!
|
|
1496
|
+
if (!existsSync3(filePath)) {
|
|
1452
1497
|
fail2(`File not found: ${filePath}`);
|
|
1453
1498
|
}
|
|
1454
1499
|
try {
|
|
1455
1500
|
return {
|
|
1456
|
-
content:
|
|
1501
|
+
content: readFileSync4(filePath, "utf-8"),
|
|
1457
1502
|
sourceFile: filePath
|
|
1458
1503
|
};
|
|
1459
1504
|
} catch {
|
|
@@ -2055,16 +2100,17 @@ function formatCommentHeader(comment) {
|
|
|
2055
2100
|
const timestamp = comment.modifiedAt ?? comment.createdAt ?? "Unknown time";
|
|
2056
2101
|
return `Comment #${comment.id} by ${author} at ${timestamp}`;
|
|
2057
2102
|
}
|
|
2058
|
-
function formatComments(result) {
|
|
2103
|
+
function formatComments(result, convertMarkdown) {
|
|
2059
2104
|
const lines = [`Comments for work item #${result.workItemId}`];
|
|
2060
2105
|
for (const comment of result.comments) {
|
|
2061
|
-
|
|
2106
|
+
const text = convertMarkdown ? toMarkdown(comment.text) : comment.text;
|
|
2107
|
+
lines.push("", formatCommentHeader(comment), text);
|
|
2062
2108
|
}
|
|
2063
2109
|
return lines.join("\n");
|
|
2064
2110
|
}
|
|
2065
2111
|
function createCommentsListCommand() {
|
|
2066
2112
|
const command = new Command12("list");
|
|
2067
|
-
command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, options) => {
|
|
2113
|
+
command.description("List visible comments for a work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "convert HTML comment bodies to markdown").action(async (idStr, options) => {
|
|
2068
2114
|
validateOrgProjectPair(options);
|
|
2069
2115
|
const id = parseWorkItemId(idStr);
|
|
2070
2116
|
let context;
|
|
@@ -2082,7 +2128,7 @@ function createCommentsListCommand() {
|
|
|
2082
2128
|
`);
|
|
2083
2129
|
return;
|
|
2084
2130
|
}
|
|
2085
|
-
process.stdout.write(`${formatComments(result)}
|
|
2131
|
+
process.stdout.write(`${formatComments(result, options.markdown === true)}
|
|
2086
2132
|
`);
|
|
2087
2133
|
} catch (err) {
|
|
2088
2134
|
handleCommandError(err, id, context, "read");
|
|
@@ -2092,7 +2138,7 @@ function createCommentsListCommand() {
|
|
|
2092
2138
|
}
|
|
2093
2139
|
function createCommentsAddCommand() {
|
|
2094
2140
|
const command = new Command12("add");
|
|
2095
|
-
command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (idStr, text, options) => {
|
|
2141
|
+
command.description("Add a comment to a work item").argument("<id>", "work item ID").argument("<text>", "comment text").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").option("--markdown", "post comment as markdown").action(async (idStr, text, options) => {
|
|
2096
2142
|
validateOrgProjectPair(options);
|
|
2097
2143
|
const id = parseWorkItemId(idStr);
|
|
2098
2144
|
if (text.trim() === "") {
|
|
@@ -2102,7 +2148,8 @@ function createCommentsAddCommand() {
|
|
|
2102
2148
|
try {
|
|
2103
2149
|
context = resolveContext(options);
|
|
2104
2150
|
const credential = await resolvePat();
|
|
2105
|
-
const
|
|
2151
|
+
const format = options.markdown === true ? "markdown" : "html";
|
|
2152
|
+
const result = await addWorkItemComment(context, id, credential.pat, text, format);
|
|
2106
2153
|
if (options.json) {
|
|
2107
2154
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2108
2155
|
`);
|