azdo-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/index.js +537 -84
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,7 +15,9 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
15
15
|
- Create or update Tasks from markdown documents (`upsert`)
|
|
16
16
|
- Read rich-text fields as markdown (`get-md-field`)
|
|
17
17
|
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
18
|
+
- Check branch pull request status, open PRs to `develop`, and review active comments (`pr`)
|
|
18
19
|
- Persist org/project/default fields in local config (`config`)
|
|
20
|
+
- List all fields of a work item (`list-fields`)
|
|
19
21
|
- Store PAT in OS credential store (or use `AZDO_PAT`)
|
|
20
22
|
|
|
21
23
|
## Installation
|
|
@@ -64,6 +66,8 @@ azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
|
64
66
|
| `azdo upsert [id]` | Create or update a Task from markdown | `--content`, `--file`, `--json`, `--org`, `--project` |
|
|
65
67
|
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
66
68
|
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
69
|
+
| `azdo list-fields <id>` | List all fields of a work item | `--json`, `--org`, `--project` |
|
|
70
|
+
| `azdo pr <subcommand>` | Manage pull requests for the current branch | `status`, `open`, `comments`, `--json`, `--org`, `--project` |
|
|
67
71
|
| `azdo config <subcommand>` | Manage saved settings | `set`, `get`, `list`, `unset`, `wizard`, `--json` |
|
|
68
72
|
| `azdo clear-pat` | Remove stored PAT | none |
|
|
69
73
|
|
|
@@ -100,6 +104,16 @@ azdo assign 12345 --unassign
|
|
|
100
104
|
azdo set-field 12345 System.Title "Updated title"
|
|
101
105
|
```
|
|
102
106
|
|
|
107
|
+
### List Fields
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# List all fields with values (rich text fields preview first 5 lines)
|
|
111
|
+
azdo list-fields 12345
|
|
112
|
+
|
|
113
|
+
# JSON output
|
|
114
|
+
azdo list-fields 12345 --json
|
|
115
|
+
```
|
|
116
|
+
|
|
103
117
|
### Markdown Display
|
|
104
118
|
|
|
105
119
|
The `get-item` command can convert HTML rich-text fields to readable markdown. Resolution order:
|
|
@@ -124,6 +138,43 @@ azdo set-md-field 12345 System.Description --file ./description.md
|
|
|
124
138
|
cat description.md | azdo set-md-field 12345 System.Description
|
|
125
139
|
```
|
|
126
140
|
|
|
141
|
+
### Pull Request Commands
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Check whether the current branch already has pull requests
|
|
147
|
+
azdo pr status
|
|
148
|
+
|
|
149
|
+
# Open a pull request to develop
|
|
150
|
+
azdo pr open --title "Add PR handling" --description "Implements pr status, pr open, pr comments commands"
|
|
151
|
+
|
|
152
|
+
# Review active comments for the current branch's active pull request
|
|
153
|
+
azdo pr comments
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`azdo pr status`
|
|
157
|
+
|
|
158
|
+
- Lists all pull requests for the current branch, including active, completed, and abandoned PRs
|
|
159
|
+
- Prints `No pull requests found for branch <branch>.` when no PRs exist
|
|
160
|
+
- Supports `--json` for machine-readable output
|
|
161
|
+
|
|
162
|
+
`azdo pr open`
|
|
163
|
+
|
|
164
|
+
- Requires both `--title <title>` and `--description <description>`
|
|
165
|
+
- Targets `develop` automatically
|
|
166
|
+
- Creates a new active pull request when none exists
|
|
167
|
+
- Reuses the existing active PR when one already matches the branch and target
|
|
168
|
+
- Fails with a clear error when run from `develop` or when multiple active PRs already exist
|
|
169
|
+
|
|
170
|
+
`azdo pr comments`
|
|
171
|
+
|
|
172
|
+
- Resolves the single active pull request for the current branch
|
|
173
|
+
- Returns only active or pending threads with visible, non-deleted comments
|
|
174
|
+
- Groups text output by thread and shows file context when available
|
|
175
|
+
- Prints `Pull request #<id> has no active comments.` when the PR has no active comment threads
|
|
176
|
+
- Fails instead of guessing when no active PR or multiple active PRs exist
|
|
177
|
+
|
|
127
178
|
## azdo upsert
|
|
128
179
|
|
|
129
180
|
`azdo upsert` accepts a single markdown task document and either creates a new Azure DevOps Task or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place.
|
|
@@ -232,6 +283,7 @@ azdo clear-pat
|
|
|
232
283
|
## JSON Output
|
|
233
284
|
|
|
234
285
|
These commands support `--json` for machine-readable output:
|
|
286
|
+
- `list-fields`
|
|
235
287
|
- `set-state`
|
|
236
288
|
- `assign`
|
|
237
289
|
- `set-field`
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command12 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -55,12 +55,29 @@ async function readResponseMessage(response) {
|
|
|
55
55
|
function normalizeFieldList(fields) {
|
|
56
56
|
return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
|
|
57
57
|
}
|
|
58
|
+
function stringifyFieldValue(value) {
|
|
59
|
+
if (typeof value === "object" && value !== null) {
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
58
64
|
function buildExtraFields(fields, requested) {
|
|
59
65
|
const result = {};
|
|
60
66
|
for (const name of requested) {
|
|
61
|
-
|
|
67
|
+
let val = fields[name];
|
|
68
|
+
let resolvedName = name;
|
|
69
|
+
if (val === void 0) {
|
|
70
|
+
const nameSuffix = name.split(".").pop().toLowerCase();
|
|
71
|
+
const match = Object.keys(fields).find(
|
|
72
|
+
(k) => k.split(".").pop().toLowerCase() === nameSuffix
|
|
73
|
+
);
|
|
74
|
+
if (match !== void 0) {
|
|
75
|
+
val = fields[match];
|
|
76
|
+
resolvedName = match;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
62
79
|
if (val !== void 0 && val !== null) {
|
|
63
|
-
result[
|
|
80
|
+
result[resolvedName] = stringifyFieldValue(val);
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
return Object.keys(result).length > 0 ? result : null;
|
|
@@ -86,6 +103,25 @@ async function readWriteResponse(response, errorCode) {
|
|
|
86
103
|
fields: data.fields
|
|
87
104
|
};
|
|
88
105
|
}
|
|
106
|
+
async function getWorkItemFields(context, id, pat) {
|
|
107
|
+
const url = new URL(
|
|
108
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
109
|
+
);
|
|
110
|
+
url.searchParams.set("api-version", "7.1");
|
|
111
|
+
url.searchParams.set("$expand", "all");
|
|
112
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
113
|
+
if (response.status === 400) {
|
|
114
|
+
const serverMessage = await readResponseMessage(response);
|
|
115
|
+
if (serverMessage) {
|
|
116
|
+
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`HTTP_${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
return data.fields;
|
|
124
|
+
}
|
|
89
125
|
async function getWorkItem(context, id, pat, extraFields) {
|
|
90
126
|
const url = new URL(
|
|
91
127
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
@@ -119,7 +155,7 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
119
155
|
}
|
|
120
156
|
let combinedDescription = null;
|
|
121
157
|
if (descriptionParts.length === 1) {
|
|
122
|
-
combinedDescription = descriptionParts
|
|
158
|
+
combinedDescription = descriptionParts.at(0)?.value ?? null;
|
|
123
159
|
} else if (descriptionParts.length > 1) {
|
|
124
160
|
combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
|
|
125
161
|
}
|
|
@@ -158,13 +194,13 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
158
194
|
if (value === void 0 || value === null || value === "") {
|
|
159
195
|
return null;
|
|
160
196
|
}
|
|
161
|
-
return
|
|
197
|
+
return stringifyFieldValue(value);
|
|
162
198
|
}
|
|
163
199
|
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
164
200
|
const result = await applyWorkItemPatch(context, id, pat, operations);
|
|
165
201
|
const title = result.fields["System.Title"];
|
|
166
|
-
const lastOp = operations
|
|
167
|
-
const fieldValue = lastOp
|
|
202
|
+
const lastOp = operations.at(-1);
|
|
203
|
+
const fieldValue = lastOp?.value ?? null;
|
|
168
204
|
return {
|
|
169
205
|
id: result.id,
|
|
170
206
|
rev: result.rev,
|
|
@@ -301,19 +337,19 @@ async function resolvePat() {
|
|
|
301
337
|
import { execSync } from "child_process";
|
|
302
338
|
var patterns = [
|
|
303
339
|
// HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
|
|
304
|
-
/^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git
|
|
340
|
+
/^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+)$/,
|
|
305
341
|
// HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
|
|
306
|
-
/^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git
|
|
342
|
+
/^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/([^/]+)$/,
|
|
307
343
|
// HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
|
|
308
|
-
/^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git
|
|
344
|
+
/^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+)$/,
|
|
309
345
|
// SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
|
|
310
|
-
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)
|
|
346
|
+
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+)$/,
|
|
311
347
|
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
|
|
312
|
-
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)
|
|
348
|
+
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+)$/
|
|
313
349
|
];
|
|
314
350
|
function parseAzdoRemote(url) {
|
|
315
351
|
for (const pattern of patterns) {
|
|
316
|
-
const match =
|
|
352
|
+
const match = pattern.exec(url);
|
|
317
353
|
if (match) {
|
|
318
354
|
const project = match[2];
|
|
319
355
|
if (/^DefaultCollection$/i.test(project)) {
|
|
@@ -337,6 +373,35 @@ function detectAzdoContext() {
|
|
|
337
373
|
}
|
|
338
374
|
return context;
|
|
339
375
|
}
|
|
376
|
+
function parseRepoName(url) {
|
|
377
|
+
for (const pattern of patterns) {
|
|
378
|
+
const match = pattern.exec(url);
|
|
379
|
+
if (match) {
|
|
380
|
+
return match[3];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function detectRepoName() {
|
|
386
|
+
let remoteUrl;
|
|
387
|
+
try {
|
|
388
|
+
remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
389
|
+
} catch {
|
|
390
|
+
throw new Error('Not in a git repository. Check that git remote "origin" exists and try again.');
|
|
391
|
+
}
|
|
392
|
+
const repo = parseRepoName(remoteUrl);
|
|
393
|
+
if (!repo) {
|
|
394
|
+
throw new Error('Git remote "origin" is not an Azure DevOps URL. Check that origin points to Azure DevOps and try again.');
|
|
395
|
+
}
|
|
396
|
+
return repo;
|
|
397
|
+
}
|
|
398
|
+
function getCurrentBranch() {
|
|
399
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
400
|
+
if (branch === "HEAD") {
|
|
401
|
+
throw new Error("Not on a named branch. Check out a named branch and try again.");
|
|
402
|
+
}
|
|
403
|
+
return branch;
|
|
404
|
+
}
|
|
340
405
|
|
|
341
406
|
// src/services/config-store.ts
|
|
342
407
|
import fs from "fs";
|
|
@@ -609,18 +674,18 @@ function parseRequestedFields(raw) {
|
|
|
609
674
|
}
|
|
610
675
|
function stripHtml(html) {
|
|
611
676
|
let text = html;
|
|
612
|
-
text = text.
|
|
613
|
-
text = text.
|
|
614
|
-
text = text.
|
|
615
|
-
text = text.
|
|
616
|
-
text = text.
|
|
617
|
-
text = text.
|
|
618
|
-
text = text.
|
|
619
|
-
text = text.
|
|
620
|
-
text = text.
|
|
621
|
-
text = text.
|
|
622
|
-
text = text.
|
|
623
|
-
text = text.
|
|
677
|
+
text = text.replaceAll(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
|
|
678
|
+
text = text.replaceAll(/<br\s*\/?>/gi, "\n");
|
|
679
|
+
text = text.replaceAll(/<\/?(p|div)>/gi, "\n");
|
|
680
|
+
text = text.replaceAll(/<li>/gi, "\n");
|
|
681
|
+
text = text.replaceAll(/<[^>]*>/g, "");
|
|
682
|
+
text = text.replaceAll("&", "&");
|
|
683
|
+
text = text.replaceAll("<", "<");
|
|
684
|
+
text = text.replaceAll(">", ">");
|
|
685
|
+
text = text.replaceAll(""", '"');
|
|
686
|
+
text = text.replaceAll("'", "'");
|
|
687
|
+
text = text.replaceAll(" ", " ");
|
|
688
|
+
text = text.replaceAll(/\n{3,}/g, "\n\n");
|
|
624
689
|
return text.trim();
|
|
625
690
|
}
|
|
626
691
|
function convertRichText(html, markdown) {
|
|
@@ -641,16 +706,19 @@ function summarizeDescription(text, label) {
|
|
|
641
706
|
return [`${label("Description:")}${firstThree.join("\n")}${suffix}`];
|
|
642
707
|
}
|
|
643
708
|
function formatWorkItem(workItem, short, markdown = false) {
|
|
644
|
-
const lines = [];
|
|
645
709
|
const label = (name) => name.padEnd(13);
|
|
646
|
-
lines
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
710
|
+
const lines = [
|
|
711
|
+
`${label("ID:")}${workItem.id}`,
|
|
712
|
+
`${label("Type:")}${workItem.type}`,
|
|
713
|
+
`${label("Title:")}${workItem.title}`,
|
|
714
|
+
`${label("State:")}${workItem.state}`,
|
|
715
|
+
`${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`
|
|
716
|
+
];
|
|
651
717
|
if (!short) {
|
|
652
|
-
lines.push(
|
|
653
|
-
|
|
718
|
+
lines.push(
|
|
719
|
+
`${label("Area:")}${workItem.areaPath}`,
|
|
720
|
+
`${label("Iteration:")}${workItem.iterationPath}`
|
|
721
|
+
);
|
|
654
722
|
}
|
|
655
723
|
lines.push(`${label("URL:")}${workItem.url}`);
|
|
656
724
|
if (workItem.extraFields) {
|
|
@@ -661,8 +729,7 @@ function formatWorkItem(workItem, short, markdown = false) {
|
|
|
661
729
|
if (short) {
|
|
662
730
|
lines.push(...summarizeDescription(descriptionText, label));
|
|
663
731
|
} else {
|
|
664
|
-
lines.push("Description:");
|
|
665
|
-
lines.push(descriptionText);
|
|
732
|
+
lines.push("Description:", descriptionText);
|
|
666
733
|
}
|
|
667
734
|
return lines.join("\n");
|
|
668
735
|
}
|
|
@@ -676,7 +743,7 @@ function createGetItemCommand() {
|
|
|
676
743
|
try {
|
|
677
744
|
context = resolveContext(options);
|
|
678
745
|
const credential = await resolvePat();
|
|
679
|
-
const fieldsList = options.fields
|
|
746
|
+
const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
|
|
680
747
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
681
748
|
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
682
749
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
@@ -707,6 +774,63 @@ function createClearPatCommand() {
|
|
|
707
774
|
// src/commands/config.ts
|
|
708
775
|
import { Command as Command3 } from "commander";
|
|
709
776
|
import { createInterface as createInterface2 } from "readline";
|
|
777
|
+
function formatConfigValue(value, unsetFallback = "") {
|
|
778
|
+
if (value === void 0) {
|
|
779
|
+
return unsetFallback;
|
|
780
|
+
}
|
|
781
|
+
return Array.isArray(value) ? value.join(",") : value;
|
|
782
|
+
}
|
|
783
|
+
function writeConfigList(cfg) {
|
|
784
|
+
const keyWidth = 10;
|
|
785
|
+
const valueWidth = 30;
|
|
786
|
+
for (const setting of SETTINGS) {
|
|
787
|
+
const raw = cfg[setting.key];
|
|
788
|
+
const value = formatConfigValue(raw, "(not set)");
|
|
789
|
+
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
790
|
+
process.stdout.write(
|
|
791
|
+
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
792
|
+
`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
|
|
796
|
+
if (hasUnset) {
|
|
797
|
+
process.stdout.write(
|
|
798
|
+
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function createAsk(rl) {
|
|
803
|
+
return (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
804
|
+
}
|
|
805
|
+
async function promptForSetting(cfg, setting, ask) {
|
|
806
|
+
const currentDisplay = String(formatConfigValue(cfg[setting.key], ""));
|
|
807
|
+
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
808
|
+
process.stderr.write(`${setting.description}${requiredTag}
|
|
809
|
+
`);
|
|
810
|
+
if (setting.example) {
|
|
811
|
+
process.stderr.write(` Example: ${setting.example}
|
|
812
|
+
`);
|
|
813
|
+
}
|
|
814
|
+
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
815
|
+
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
816
|
+
const trimmed = answer.trim();
|
|
817
|
+
if (trimmed) {
|
|
818
|
+
setConfigValue(setting.key, trimmed);
|
|
819
|
+
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
820
|
+
|
|
821
|
+
`);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (currentDisplay) {
|
|
825
|
+
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
826
|
+
|
|
827
|
+
`);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
process.stderr.write(` -> Skipped "${setting.key}"
|
|
831
|
+
|
|
832
|
+
`);
|
|
833
|
+
}
|
|
710
834
|
function createConfigCommand() {
|
|
711
835
|
const config = new Command3("config");
|
|
712
836
|
config.description("Manage CLI settings");
|
|
@@ -759,27 +883,9 @@ function createConfigCommand() {
|
|
|
759
883
|
const cfg = loadConfig();
|
|
760
884
|
if (options.json) {
|
|
761
885
|
process.stdout.write(JSON.stringify(cfg) + "\n");
|
|
762
|
-
|
|
763
|
-
const keyWidth = 10;
|
|
764
|
-
const valueWidth = 30;
|
|
765
|
-
for (const setting of SETTINGS) {
|
|
766
|
-
const raw = cfg[setting.key];
|
|
767
|
-
const value = raw === void 0 ? "(not set)" : Array.isArray(raw) ? raw.join(",") : raw;
|
|
768
|
-
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
769
|
-
process.stdout.write(
|
|
770
|
-
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
771
|
-
`
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
const hasUnset = SETTINGS.some(
|
|
775
|
-
(s) => s.required && cfg[s.key] === void 0
|
|
776
|
-
);
|
|
777
|
-
if (hasUnset) {
|
|
778
|
-
process.stdout.write(
|
|
779
|
-
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
780
|
-
);
|
|
781
|
-
}
|
|
886
|
+
return;
|
|
782
887
|
}
|
|
888
|
+
writeConfigList(cfg);
|
|
783
889
|
});
|
|
784
890
|
const unset = new Command3("unset");
|
|
785
891
|
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
@@ -811,36 +917,11 @@ function createConfigCommand() {
|
|
|
811
917
|
input: process.stdin,
|
|
812
918
|
output: process.stderr
|
|
813
919
|
});
|
|
814
|
-
const ask = (
|
|
920
|
+
const ask = createAsk(rl);
|
|
815
921
|
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
816
922
|
process.stderr.write("=======================================\n\n");
|
|
817
923
|
for (const setting of SETTINGS) {
|
|
818
|
-
|
|
819
|
-
const currentDisplay = current === void 0 ? "" : Array.isArray(current) ? current.join(",") : current;
|
|
820
|
-
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
821
|
-
process.stderr.write(`${setting.description}${requiredTag}
|
|
822
|
-
`);
|
|
823
|
-
if (setting.example) {
|
|
824
|
-
process.stderr.write(` Example: ${setting.example}
|
|
825
|
-
`);
|
|
826
|
-
}
|
|
827
|
-
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
828
|
-
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
829
|
-
const trimmed = answer.trim();
|
|
830
|
-
if (trimmed) {
|
|
831
|
-
setConfigValue(setting.key, trimmed);
|
|
832
|
-
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
833
|
-
|
|
834
|
-
`);
|
|
835
|
-
} else if (currentDisplay) {
|
|
836
|
-
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
837
|
-
|
|
838
|
-
`);
|
|
839
|
-
} else {
|
|
840
|
-
process.stderr.write(` -> Skipped "${setting.key}"
|
|
841
|
-
|
|
842
|
-
`);
|
|
843
|
-
}
|
|
924
|
+
await promptForSetting(cfg, setting, ask);
|
|
844
925
|
}
|
|
845
926
|
rl.close();
|
|
846
927
|
process.stderr.write("Configuration complete!\n");
|
|
@@ -1400,8 +1481,378 @@ function createUpsertCommand() {
|
|
|
1400
1481
|
return command;
|
|
1401
1482
|
}
|
|
1402
1483
|
|
|
1484
|
+
// src/commands/list-fields.ts
|
|
1485
|
+
import { Command as Command10 } from "commander";
|
|
1486
|
+
function stringifyValue(value) {
|
|
1487
|
+
if (value === null || value === void 0) return "";
|
|
1488
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1489
|
+
return String(value);
|
|
1490
|
+
}
|
|
1491
|
+
function formatRichValue(raw) {
|
|
1492
|
+
const md = htmlToMarkdown(raw);
|
|
1493
|
+
const lines = md.split("\n").filter((l) => l.trim() !== "");
|
|
1494
|
+
const preview = lines.slice(0, 5);
|
|
1495
|
+
const suffix = lines.length > 5 ? `
|
|
1496
|
+
\u2026 (${lines.length - 5} more lines)` : "";
|
|
1497
|
+
return preview.join("\n ") + suffix;
|
|
1498
|
+
}
|
|
1499
|
+
function formatFieldList(fields) {
|
|
1500
|
+
const entries = Object.entries(fields).sort(([a], [b]) => a.localeCompare(b));
|
|
1501
|
+
const maxKeyLen = Math.min(
|
|
1502
|
+
Math.max(...entries.map(([k]) => k.length)),
|
|
1503
|
+
50
|
|
1504
|
+
);
|
|
1505
|
+
return entries.map(([key, value]) => {
|
|
1506
|
+
const raw = stringifyValue(value);
|
|
1507
|
+
if (raw === "") return `${key.padEnd(maxKeyLen + 2)}(empty)`;
|
|
1508
|
+
if (typeof value === "string" && isHtml(value)) {
|
|
1509
|
+
const preview = formatRichValue(value);
|
|
1510
|
+
return `${key.padEnd(maxKeyLen + 2)}[rich text]
|
|
1511
|
+
${preview}`;
|
|
1512
|
+
}
|
|
1513
|
+
return `${key.padEnd(maxKeyLen + 2)}${raw}`;
|
|
1514
|
+
}).join("\n");
|
|
1515
|
+
}
|
|
1516
|
+
function createListFieldsCommand() {
|
|
1517
|
+
const command = new Command10("list-fields");
|
|
1518
|
+
command.description("List all fields of an Azure DevOps work item").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").action(
|
|
1519
|
+
async (idStr, options) => {
|
|
1520
|
+
const id = parseWorkItemId(idStr);
|
|
1521
|
+
validateOrgProjectPair(options);
|
|
1522
|
+
let context;
|
|
1523
|
+
try {
|
|
1524
|
+
context = resolveContext(options);
|
|
1525
|
+
const credential = await resolvePat();
|
|
1526
|
+
const fields = await getWorkItemFields(context, id, credential.pat);
|
|
1527
|
+
if (options.json) {
|
|
1528
|
+
process.stdout.write(JSON.stringify({ id, fields }, null, 2) + "\n");
|
|
1529
|
+
} else {
|
|
1530
|
+
process.stdout.write(`Work Item ${id} \u2014 ${Object.keys(fields).length} fields
|
|
1531
|
+
|
|
1532
|
+
`);
|
|
1533
|
+
process.stdout.write(formatFieldList(fields) + "\n");
|
|
1534
|
+
}
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
handleCommandError(err, id, context, "read");
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
);
|
|
1540
|
+
return command;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/commands/pr.ts
|
|
1544
|
+
import { Command as Command11 } from "commander";
|
|
1545
|
+
|
|
1546
|
+
// src/services/pr-client.ts
|
|
1547
|
+
function buildPullRequestsUrl(context, repo, sourceBranch, opts) {
|
|
1548
|
+
const url = new URL(
|
|
1549
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
1550
|
+
);
|
|
1551
|
+
url.searchParams.set("api-version", "7.1");
|
|
1552
|
+
url.searchParams.set("searchCriteria.sourceRefName", `refs/heads/${sourceBranch}`);
|
|
1553
|
+
if (opts?.status) {
|
|
1554
|
+
url.searchParams.set("searchCriteria.status", opts.status);
|
|
1555
|
+
}
|
|
1556
|
+
if (opts?.targetBranch) {
|
|
1557
|
+
url.searchParams.set("searchCriteria.targetRefName", `refs/heads/${opts.targetBranch}`);
|
|
1558
|
+
}
|
|
1559
|
+
return url;
|
|
1560
|
+
}
|
|
1561
|
+
function mapPullRequest(repo, pullRequest) {
|
|
1562
|
+
return {
|
|
1563
|
+
id: pullRequest.pullRequestId,
|
|
1564
|
+
title: pullRequest.title,
|
|
1565
|
+
repository: repo,
|
|
1566
|
+
sourceRefName: pullRequest.sourceRefName,
|
|
1567
|
+
targetRefName: pullRequest.targetRefName,
|
|
1568
|
+
status: pullRequest.status,
|
|
1569
|
+
createdBy: pullRequest.createdBy?.displayName ?? null,
|
|
1570
|
+
url: pullRequest._links.web.href
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function mapComment(comment) {
|
|
1574
|
+
const content = comment.content?.trim();
|
|
1575
|
+
if (comment.isDeleted || !content) {
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
return {
|
|
1579
|
+
id: comment.id,
|
|
1580
|
+
author: comment.author?.displayName ?? null,
|
|
1581
|
+
content,
|
|
1582
|
+
publishedAt: comment.publishedDate ?? null
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
function mapThread(thread) {
|
|
1586
|
+
if (thread.status !== "active" && thread.status !== "pending") {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
const comments = thread.comments.map(mapComment).filter((comment) => comment !== null);
|
|
1590
|
+
if (comments.length === 0) {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
id: thread.id,
|
|
1595
|
+
status: thread.status,
|
|
1596
|
+
threadContext: thread.threadContext?.filePath ?? null,
|
|
1597
|
+
comments
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
async function readJsonResponse(response) {
|
|
1601
|
+
if (!response.ok) {
|
|
1602
|
+
throw new Error(`HTTP_${response.status}`);
|
|
1603
|
+
}
|
|
1604
|
+
return response.json();
|
|
1605
|
+
}
|
|
1606
|
+
async function listPullRequests(context, repo, pat, sourceBranch, opts) {
|
|
1607
|
+
const response = await fetchWithErrors(
|
|
1608
|
+
buildPullRequestsUrl(context, repo, sourceBranch, opts).toString(),
|
|
1609
|
+
{ headers: authHeaders(pat) }
|
|
1610
|
+
);
|
|
1611
|
+
const data = await readJsonResponse(response);
|
|
1612
|
+
return data.value.map((pullRequest) => mapPullRequest(repo, pullRequest));
|
|
1613
|
+
}
|
|
1614
|
+
async function openPullRequest(context, repo, pat, sourceBranch, title, description) {
|
|
1615
|
+
const existing = await listPullRequests(context, repo, pat, sourceBranch, {
|
|
1616
|
+
status: "active",
|
|
1617
|
+
targetBranch: "develop"
|
|
1618
|
+
});
|
|
1619
|
+
if (existing.length === 1) {
|
|
1620
|
+
return {
|
|
1621
|
+
branch: sourceBranch,
|
|
1622
|
+
targetBranch: "develop",
|
|
1623
|
+
created: false,
|
|
1624
|
+
pullRequest: existing[0]
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
if (existing.length > 1) {
|
|
1628
|
+
throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
|
|
1629
|
+
}
|
|
1630
|
+
const payload = {
|
|
1631
|
+
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
1632
|
+
targetRefName: "refs/heads/develop",
|
|
1633
|
+
title,
|
|
1634
|
+
description
|
|
1635
|
+
};
|
|
1636
|
+
const url = new URL(
|
|
1637
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
1638
|
+
);
|
|
1639
|
+
url.searchParams.set("api-version", "7.1");
|
|
1640
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
1641
|
+
method: "POST",
|
|
1642
|
+
headers: {
|
|
1643
|
+
...authHeaders(pat),
|
|
1644
|
+
"Content-Type": "application/json"
|
|
1645
|
+
},
|
|
1646
|
+
body: JSON.stringify(payload)
|
|
1647
|
+
});
|
|
1648
|
+
const data = await readJsonResponse(response);
|
|
1649
|
+
return {
|
|
1650
|
+
branch: sourceBranch,
|
|
1651
|
+
targetBranch: "develop",
|
|
1652
|
+
created: true,
|
|
1653
|
+
pullRequest: mapPullRequest(repo, data)
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
async function getPullRequestThreads(context, repo, pat, prId) {
|
|
1657
|
+
const url = new URL(
|
|
1658
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
|
|
1659
|
+
);
|
|
1660
|
+
url.searchParams.set("api-version", "7.1");
|
|
1661
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
|
|
1662
|
+
const data = await readJsonResponse(response);
|
|
1663
|
+
return data.value.map(mapThread).filter((thread) => thread !== null);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/commands/pr.ts
|
|
1667
|
+
function formatBranchName(refName) {
|
|
1668
|
+
return refName.startsWith("refs/heads/") ? refName.slice("refs/heads/".length) : refName;
|
|
1669
|
+
}
|
|
1670
|
+
function writeError(message) {
|
|
1671
|
+
process.stderr.write(`Error: ${message}
|
|
1672
|
+
`);
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
function handlePrCommandError(err, context, mode = "read") {
|
|
1676
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1677
|
+
if (error.message === "AUTH_FAILED") {
|
|
1678
|
+
const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
|
|
1679
|
+
writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`);
|
|
1680
|
+
}
|
|
1681
|
+
if (error.message === "PERMISSION_DENIED") {
|
|
1682
|
+
writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`);
|
|
1683
|
+
}
|
|
1684
|
+
if (error.message === "NETWORK_ERROR") {
|
|
1685
|
+
writeError("Could not connect to Azure DevOps. Check your network connection.");
|
|
1686
|
+
}
|
|
1687
|
+
if (error.message === "NOT_FOUND") {
|
|
1688
|
+
writeError(`Azure DevOps repository not found in ${context?.org}/${context?.project}.`);
|
|
1689
|
+
}
|
|
1690
|
+
if (error.message.startsWith("HTTP_")) {
|
|
1691
|
+
writeError(`Azure DevOps request failed with ${error.message}.`);
|
|
1692
|
+
}
|
|
1693
|
+
writeError(error.message);
|
|
1694
|
+
}
|
|
1695
|
+
function formatPullRequestBlock(pullRequest) {
|
|
1696
|
+
return [
|
|
1697
|
+
`#${pullRequest.id} [${pullRequest.status}] ${pullRequest.title}`,
|
|
1698
|
+
`${formatBranchName(pullRequest.sourceRefName)} -> ${formatBranchName(pullRequest.targetRefName)}`,
|
|
1699
|
+
pullRequest.url
|
|
1700
|
+
].join("\n");
|
|
1701
|
+
}
|
|
1702
|
+
function formatThreads(prId, title, threads) {
|
|
1703
|
+
const lines = [`Active comments for pull request #${prId}: ${title}`];
|
|
1704
|
+
for (const thread of threads) {
|
|
1705
|
+
lines.push("", `Thread #${thread.id} [${thread.status}] ${thread.threadContext ?? "(general)"}`);
|
|
1706
|
+
for (const comment of thread.comments) {
|
|
1707
|
+
lines.push(` ${comment.author ?? "Unknown"}: ${comment.content}`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return lines.join("\n");
|
|
1711
|
+
}
|
|
1712
|
+
async function resolvePrCommandContext(options) {
|
|
1713
|
+
const context = resolveContext(options);
|
|
1714
|
+
const repo = detectRepoName();
|
|
1715
|
+
const branch = getCurrentBranch();
|
|
1716
|
+
const credential = await resolvePat();
|
|
1717
|
+
return {
|
|
1718
|
+
context,
|
|
1719
|
+
repo,
|
|
1720
|
+
branch,
|
|
1721
|
+
pat: credential.pat
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function createPrStatusCommand() {
|
|
1725
|
+
const command = new Command11("status");
|
|
1726
|
+
command.description("Check pull requests for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
|
|
1727
|
+
validateOrgProjectPair(options);
|
|
1728
|
+
let context;
|
|
1729
|
+
try {
|
|
1730
|
+
const resolved = await resolvePrCommandContext(options);
|
|
1731
|
+
context = resolved.context;
|
|
1732
|
+
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch);
|
|
1733
|
+
const { branch, repo } = resolved;
|
|
1734
|
+
const result = { branch, repository: repo, pullRequests };
|
|
1735
|
+
if (options.json) {
|
|
1736
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1737
|
+
`);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
if (pullRequests.length === 0) {
|
|
1741
|
+
process.stdout.write(`No pull requests found for branch ${branch}.
|
|
1742
|
+
`);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
process.stdout.write(`${pullRequests.map(formatPullRequestBlock).join("\n\n")}
|
|
1746
|
+
`);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
handlePrCommandError(err, context, "read");
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
return command;
|
|
1752
|
+
}
|
|
1753
|
+
function createPrOpenCommand() {
|
|
1754
|
+
const command = new Command11("open");
|
|
1755
|
+
command.description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option("--description <description>", "pull request description").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
|
|
1756
|
+
validateOrgProjectPair(options);
|
|
1757
|
+
const title = options.title?.trim();
|
|
1758
|
+
if (!title) {
|
|
1759
|
+
writeError("--title is required for pull request creation.");
|
|
1760
|
+
}
|
|
1761
|
+
const description = options.description?.trim();
|
|
1762
|
+
if (!description) {
|
|
1763
|
+
writeError("--description is required for pull request creation.");
|
|
1764
|
+
}
|
|
1765
|
+
let context;
|
|
1766
|
+
try {
|
|
1767
|
+
const resolved = await resolvePrCommandContext(options);
|
|
1768
|
+
context = resolved.context;
|
|
1769
|
+
if (resolved.branch === "develop") {
|
|
1770
|
+
writeError("Pull request creation requires a source branch other than develop.");
|
|
1771
|
+
}
|
|
1772
|
+
const result = await openPullRequest(
|
|
1773
|
+
resolved.context,
|
|
1774
|
+
resolved.repo,
|
|
1775
|
+
resolved.pat,
|
|
1776
|
+
resolved.branch,
|
|
1777
|
+
title,
|
|
1778
|
+
description
|
|
1779
|
+
);
|
|
1780
|
+
if (options.json) {
|
|
1781
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1782
|
+
`);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (result.created) {
|
|
1786
|
+
process.stdout.write(`Created pull request #${result.pullRequest.id}: ${result.pullRequest.title}
|
|
1787
|
+
${result.pullRequest.url}
|
|
1788
|
+
`);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
process.stdout.write(
|
|
1792
|
+
`Active pull request already exists for ${resolved.branch} -> develop: #${result.pullRequest.id}
|
|
1793
|
+
${result.pullRequest.url}
|
|
1794
|
+
`
|
|
1795
|
+
);
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
if (err instanceof Error && err.message.startsWith("AMBIGUOUS_PRS:")) {
|
|
1798
|
+
const ids = err.message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
1799
|
+
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
1800
|
+
}
|
|
1801
|
+
handlePrCommandError(err, context, "write");
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
return command;
|
|
1805
|
+
}
|
|
1806
|
+
function createPrCommentsCommand() {
|
|
1807
|
+
const command = new Command11("comments");
|
|
1808
|
+
command.description("List active pull request comments for the current branch").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output JSON").action(async (options) => {
|
|
1809
|
+
validateOrgProjectPair(options);
|
|
1810
|
+
let context;
|
|
1811
|
+
try {
|
|
1812
|
+
const resolved = await resolvePrCommandContext(options);
|
|
1813
|
+
context = resolved.context;
|
|
1814
|
+
const pullRequests = await listPullRequests(resolved.context, resolved.repo, resolved.pat, resolved.branch, {
|
|
1815
|
+
status: "active"
|
|
1816
|
+
});
|
|
1817
|
+
if (pullRequests.length === 0) {
|
|
1818
|
+
writeError(`No active pull request found for branch ${resolved.branch}.`);
|
|
1819
|
+
}
|
|
1820
|
+
if (pullRequests.length > 1) {
|
|
1821
|
+
const ids = pullRequests.map((pullRequest2) => `#${pullRequest2.id}`).join(", ");
|
|
1822
|
+
writeError(`Multiple active pull requests found for branch ${resolved.branch}: ${ids}. Use pr status to review them.`);
|
|
1823
|
+
}
|
|
1824
|
+
const pullRequest = pullRequests[0];
|
|
1825
|
+
const threads = await getPullRequestThreads(resolved.context, resolved.repo, resolved.pat, pullRequest.id);
|
|
1826
|
+
const result = { branch: resolved.branch, pullRequest, threads };
|
|
1827
|
+
if (options.json) {
|
|
1828
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1829
|
+
`);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (threads.length === 0) {
|
|
1833
|
+
process.stdout.write(`Pull request #${pullRequest.id} has no active comments.
|
|
1834
|
+
`);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
process.stdout.write(`${formatThreads(pullRequest.id, pullRequest.title, threads)}
|
|
1838
|
+
`);
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
handlePrCommandError(err, context, "read");
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
return command;
|
|
1844
|
+
}
|
|
1845
|
+
function createPrCommand() {
|
|
1846
|
+
const command = new Command11("pr");
|
|
1847
|
+
command.description("Manage Azure DevOps pull requests");
|
|
1848
|
+
command.addCommand(createPrStatusCommand());
|
|
1849
|
+
command.addCommand(createPrOpenCommand());
|
|
1850
|
+
command.addCommand(createPrCommentsCommand());
|
|
1851
|
+
return command;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1403
1854
|
// src/index.ts
|
|
1404
|
-
var program = new
|
|
1855
|
+
var program = new Command12();
|
|
1405
1856
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
1406
1857
|
program.addCommand(createGetItemCommand());
|
|
1407
1858
|
program.addCommand(createClearPatCommand());
|
|
@@ -1412,6 +1863,8 @@ program.addCommand(createSetFieldCommand());
|
|
|
1412
1863
|
program.addCommand(createGetMdFieldCommand());
|
|
1413
1864
|
program.addCommand(createSetMdFieldCommand());
|
|
1414
1865
|
program.addCommand(createUpsertCommand());
|
|
1866
|
+
program.addCommand(createListFieldsCommand());
|
|
1867
|
+
program.addCommand(createPrCommand());
|
|
1415
1868
|
program.showHelpAfterError();
|
|
1416
1869
|
program.parse();
|
|
1417
1870
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"lint": "eslint src/",
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"format": "prettier --check src/",
|
|
17
|
-
"test": "npm run build && vitest run"
|
|
17
|
+
"test": "npm run build && vitest run tests/unit",
|
|
18
|
+
"test:integration": "npm run build && vitest run tests/integration"
|
|
18
19
|
},
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|