azdo-cli 0.2.0-develop.14 → 0.2.0-develop.24
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/dist/index.js +665 -10
- package/package.json +1 -1
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 Command7 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -15,8 +15,33 @@ var version = pkg.version;
|
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
|
|
17
17
|
// src/services/azdo-client.ts
|
|
18
|
-
|
|
19
|
-
|
|
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 buildExtraFields(fields, requested) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const name of requested) {
|
|
32
|
+
const val = fields[name];
|
|
33
|
+
if (val !== void 0 && val !== null) {
|
|
34
|
+
result[name] = String(val);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
38
|
+
}
|
|
39
|
+
async function getWorkItem(context, id, pat, extraFields) {
|
|
40
|
+
let url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
|
|
41
|
+
if (extraFields && extraFields.length > 0) {
|
|
42
|
+
const allFields = [...DEFAULT_FIELDS, ...extraFields];
|
|
43
|
+
url += `&fields=${allFields.join(",")}`;
|
|
44
|
+
}
|
|
20
45
|
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
21
46
|
let response;
|
|
22
47
|
try {
|
|
@@ -61,7 +86,50 @@ async function getWorkItem(context, id, pat) {
|
|
|
61
86
|
description: combinedDescription,
|
|
62
87
|
areaPath: data.fields["System.AreaPath"],
|
|
63
88
|
iterationPath: data.fields["System.IterationPath"],
|
|
64
|
-
url: data._links.html.href
|
|
89
|
+
url: data._links.html.href,
|
|
90
|
+
extraFields: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
94
|
+
const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
|
|
95
|
+
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetch(url, {
|
|
99
|
+
method: "PATCH",
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Basic ${token}`,
|
|
102
|
+
"Content-Type": "application/json-patch+json"
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify(operations)
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("NETWORK_ERROR");
|
|
108
|
+
}
|
|
109
|
+
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
110
|
+
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
111
|
+
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
112
|
+
if (response.status === 400) {
|
|
113
|
+
let serverMessage = "Unknown error";
|
|
114
|
+
try {
|
|
115
|
+
const body = await response.json();
|
|
116
|
+
if (body.message) serverMessage = body.message;
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
|
|
120
|
+
}
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`HTTP_${response.status}`);
|
|
123
|
+
}
|
|
124
|
+
const data = await response.json();
|
|
125
|
+
const lastOp = operations[operations.length - 1];
|
|
126
|
+
const fieldValue = lastOp.value ?? null;
|
|
127
|
+
return {
|
|
128
|
+
id: data.id,
|
|
129
|
+
rev: data.rev,
|
|
130
|
+
title: data.fields["System.Title"],
|
|
131
|
+
fieldName,
|
|
132
|
+
fieldValue
|
|
65
133
|
};
|
|
66
134
|
}
|
|
67
135
|
|
|
@@ -175,7 +243,11 @@ function parseAzdoRemote(url) {
|
|
|
175
243
|
for (const pattern of patterns) {
|
|
176
244
|
const match = url.match(pattern);
|
|
177
245
|
if (match) {
|
|
178
|
-
|
|
246
|
+
const project = match[2];
|
|
247
|
+
if (/^DefaultCollection$/i.test(project)) {
|
|
248
|
+
return { org: match[1], project: "" };
|
|
249
|
+
}
|
|
250
|
+
return { org: match[1], project };
|
|
179
251
|
}
|
|
180
252
|
}
|
|
181
253
|
return null;
|
|
@@ -188,12 +260,97 @@ function detectAzdoContext() {
|
|
|
188
260
|
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
189
261
|
}
|
|
190
262
|
const context = parseAzdoRemote(remoteUrl);
|
|
191
|
-
if (!context) {
|
|
263
|
+
if (!context || !context.org && !context.project) {
|
|
192
264
|
throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
|
|
193
265
|
}
|
|
194
266
|
return context;
|
|
195
267
|
}
|
|
196
268
|
|
|
269
|
+
// src/services/config-store.ts
|
|
270
|
+
import fs from "fs";
|
|
271
|
+
import path from "path";
|
|
272
|
+
import os from "os";
|
|
273
|
+
var SETTINGS = [
|
|
274
|
+
{
|
|
275
|
+
key: "org",
|
|
276
|
+
description: "Azure DevOps organization name",
|
|
277
|
+
type: "string",
|
|
278
|
+
example: "mycompany",
|
|
279
|
+
required: true
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
key: "project",
|
|
283
|
+
description: "Azure DevOps project name",
|
|
284
|
+
type: "string",
|
|
285
|
+
example: "MyProject",
|
|
286
|
+
required: true
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
key: "fields",
|
|
290
|
+
description: "Extra work item fields to include (comma-separated reference names)",
|
|
291
|
+
type: "string[]",
|
|
292
|
+
example: "System.Tags,Custom.Priority",
|
|
293
|
+
required: false
|
|
294
|
+
}
|
|
295
|
+
];
|
|
296
|
+
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
297
|
+
function getConfigPath() {
|
|
298
|
+
return path.join(os.homedir(), ".azdo", "config.json");
|
|
299
|
+
}
|
|
300
|
+
function loadConfig() {
|
|
301
|
+
const configPath = getConfigPath();
|
|
302
|
+
let raw;
|
|
303
|
+
try {
|
|
304
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err.code === "ENOENT") {
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(raw);
|
|
313
|
+
} catch {
|
|
314
|
+
process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
|
|
315
|
+
`);
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function saveConfig(config) {
|
|
320
|
+
const configPath = getConfigPath();
|
|
321
|
+
const dir = path.dirname(configPath);
|
|
322
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
323
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
324
|
+
}
|
|
325
|
+
function validateKey(key) {
|
|
326
|
+
if (!VALID_KEYS.includes(key)) {
|
|
327
|
+
throw new Error(`Unknown setting key "${key}". Valid keys: org, project, fields`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function getConfigValue(key) {
|
|
331
|
+
validateKey(key);
|
|
332
|
+
const config = loadConfig();
|
|
333
|
+
return config[key];
|
|
334
|
+
}
|
|
335
|
+
function setConfigValue(key, value) {
|
|
336
|
+
validateKey(key);
|
|
337
|
+
const config = loadConfig();
|
|
338
|
+
if (value === "") {
|
|
339
|
+
delete config[key];
|
|
340
|
+
} else if (key === "fields") {
|
|
341
|
+
config.fields = value.split(",").map((s) => s.trim());
|
|
342
|
+
} else {
|
|
343
|
+
config[key] = value;
|
|
344
|
+
}
|
|
345
|
+
saveConfig(config);
|
|
346
|
+
}
|
|
347
|
+
function unsetConfigValue(key) {
|
|
348
|
+
validateKey(key);
|
|
349
|
+
const config = loadConfig();
|
|
350
|
+
delete config[key];
|
|
351
|
+
saveConfig(config);
|
|
352
|
+
}
|
|
353
|
+
|
|
197
354
|
// src/commands/get-item.ts
|
|
198
355
|
function stripHtml(html) {
|
|
199
356
|
let text = html;
|
|
@@ -224,6 +381,12 @@ function formatWorkItem(workItem, short) {
|
|
|
224
381
|
lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
|
|
225
382
|
}
|
|
226
383
|
lines.push(`${label("URL:")}${workItem.url}`);
|
|
384
|
+
if (workItem.extraFields) {
|
|
385
|
+
for (const [refName, value] of Object.entries(workItem.extraFields)) {
|
|
386
|
+
const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
|
|
387
|
+
lines.push(`${fieldLabel.padEnd(13)}${value}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
227
390
|
lines.push("");
|
|
228
391
|
const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
|
|
229
392
|
if (short) {
|
|
@@ -240,7 +403,7 @@ function formatWorkItem(workItem, short) {
|
|
|
240
403
|
}
|
|
241
404
|
function createGetItemCommand() {
|
|
242
405
|
const command = new Command("get-item");
|
|
243
|
-
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").action(
|
|
406
|
+
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(
|
|
244
407
|
async (idStr, options) => {
|
|
245
408
|
const id = parseInt(idStr, 10);
|
|
246
409
|
if (!Number.isInteger(id) || id <= 0) {
|
|
@@ -263,10 +426,29 @@ function createGetItemCommand() {
|
|
|
263
426
|
if (options.org && options.project) {
|
|
264
427
|
context = { org: options.org, project: options.project };
|
|
265
428
|
} else {
|
|
266
|
-
|
|
429
|
+
const config = loadConfig();
|
|
430
|
+
if (config.org && config.project) {
|
|
431
|
+
context = { org: config.org, project: config.project };
|
|
432
|
+
} else {
|
|
433
|
+
let gitContext = null;
|
|
434
|
+
try {
|
|
435
|
+
gitContext = detectAzdoContext();
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
const org = config.org || gitContext?.org;
|
|
439
|
+
const project = config.project || gitContext?.project;
|
|
440
|
+
if (org && project) {
|
|
441
|
+
context = { org, project };
|
|
442
|
+
} else {
|
|
443
|
+
throw new Error(
|
|
444
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
267
448
|
}
|
|
268
449
|
const credential = await resolvePat();
|
|
269
|
-
const
|
|
450
|
+
const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
|
|
451
|
+
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
270
452
|
const output = formatWorkItem(workItem, options.short ?? false);
|
|
271
453
|
process.stdout.write(output + "\n");
|
|
272
454
|
} catch (err) {
|
|
@@ -319,11 +501,484 @@ function createClearPatCommand() {
|
|
|
319
501
|
return command;
|
|
320
502
|
}
|
|
321
503
|
|
|
504
|
+
// src/commands/config.ts
|
|
505
|
+
import { Command as Command3 } from "commander";
|
|
506
|
+
import { createInterface as createInterface2 } from "readline";
|
|
507
|
+
function createConfigCommand() {
|
|
508
|
+
const config = new Command3("config");
|
|
509
|
+
config.description("Manage CLI settings");
|
|
510
|
+
const set = new Command3("set");
|
|
511
|
+
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) => {
|
|
512
|
+
try {
|
|
513
|
+
setConfigValue(key, value);
|
|
514
|
+
if (options.json) {
|
|
515
|
+
const output = { key, value };
|
|
516
|
+
if (key === "fields") {
|
|
517
|
+
output.value = value.split(",").map((s) => s.trim());
|
|
518
|
+
}
|
|
519
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
520
|
+
} else {
|
|
521
|
+
process.stdout.write(`Set "${key}" to "${value}"
|
|
522
|
+
`);
|
|
523
|
+
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
526
|
+
process.stderr.write(`Error: ${message}
|
|
527
|
+
`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
const get = new Command3("get");
|
|
532
|
+
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
533
|
+
try {
|
|
534
|
+
const value = getConfigValue(key);
|
|
535
|
+
if (options.json) {
|
|
536
|
+
process.stdout.write(
|
|
537
|
+
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
538
|
+
);
|
|
539
|
+
} else if (value === void 0) {
|
|
540
|
+
process.stdout.write(`Setting "${key}" is not configured.
|
|
541
|
+
`);
|
|
542
|
+
} else if (Array.isArray(value)) {
|
|
543
|
+
process.stdout.write(value.join(",") + "\n");
|
|
544
|
+
} else {
|
|
545
|
+
process.stdout.write(value + "\n");
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
549
|
+
process.stderr.write(`Error: ${message}
|
|
550
|
+
`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
const list = new Command3("list");
|
|
555
|
+
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
556
|
+
const cfg = loadConfig();
|
|
557
|
+
if (options.json) {
|
|
558
|
+
process.stdout.write(JSON.stringify(cfg) + "\n");
|
|
559
|
+
} else {
|
|
560
|
+
const keyWidth = 10;
|
|
561
|
+
const valueWidth = 30;
|
|
562
|
+
for (const setting of SETTINGS) {
|
|
563
|
+
const raw = cfg[setting.key];
|
|
564
|
+
const value = raw === void 0 ? "(not set)" : Array.isArray(raw) ? raw.join(",") : raw;
|
|
565
|
+
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
566
|
+
process.stdout.write(
|
|
567
|
+
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
568
|
+
`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const hasUnset = SETTINGS.some(
|
|
572
|
+
(s) => s.required && cfg[s.key] === void 0
|
|
573
|
+
);
|
|
574
|
+
if (hasUnset) {
|
|
575
|
+
process.stdout.write(
|
|
576
|
+
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
const unset = new Command3("unset");
|
|
582
|
+
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
583
|
+
try {
|
|
584
|
+
unsetConfigValue(key);
|
|
585
|
+
if (options.json) {
|
|
586
|
+
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
587
|
+
} else {
|
|
588
|
+
process.stdout.write(`Unset "${key}"
|
|
589
|
+
`);
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
593
|
+
process.stderr.write(`Error: ${message}
|
|
594
|
+
`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
const wizard = new Command3("wizard");
|
|
599
|
+
wizard.description("Interactive wizard to configure all settings").action(async () => {
|
|
600
|
+
if (!process.stdin.isTTY) {
|
|
601
|
+
process.stderr.write(
|
|
602
|
+
"Error: Wizard requires an interactive terminal.\n"
|
|
603
|
+
);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
const cfg = loadConfig();
|
|
607
|
+
const rl = createInterface2({
|
|
608
|
+
input: process.stdin,
|
|
609
|
+
output: process.stderr
|
|
610
|
+
});
|
|
611
|
+
const ask = (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
612
|
+
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
613
|
+
process.stderr.write("=======================================\n\n");
|
|
614
|
+
for (const setting of SETTINGS) {
|
|
615
|
+
const current = cfg[setting.key];
|
|
616
|
+
const currentDisplay = current === void 0 ? "" : Array.isArray(current) ? current.join(",") : current;
|
|
617
|
+
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
618
|
+
process.stderr.write(`${setting.description}${requiredTag}
|
|
619
|
+
`);
|
|
620
|
+
if (setting.example) {
|
|
621
|
+
process.stderr.write(` Example: ${setting.example}
|
|
622
|
+
`);
|
|
623
|
+
}
|
|
624
|
+
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
625
|
+
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
626
|
+
const trimmed = answer.trim();
|
|
627
|
+
if (trimmed) {
|
|
628
|
+
setConfigValue(setting.key, trimmed);
|
|
629
|
+
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
630
|
+
|
|
631
|
+
`);
|
|
632
|
+
} else if (currentDisplay) {
|
|
633
|
+
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
634
|
+
|
|
635
|
+
`);
|
|
636
|
+
} else {
|
|
637
|
+
process.stderr.write(` -> Skipped "${setting.key}"
|
|
638
|
+
|
|
639
|
+
`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
rl.close();
|
|
643
|
+
process.stderr.write("Configuration complete!\n");
|
|
644
|
+
});
|
|
645
|
+
config.addCommand(set);
|
|
646
|
+
config.addCommand(get);
|
|
647
|
+
config.addCommand(list);
|
|
648
|
+
config.addCommand(unset);
|
|
649
|
+
config.addCommand(wizard);
|
|
650
|
+
return config;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/commands/set-state.ts
|
|
654
|
+
import { Command as Command4 } from "commander";
|
|
655
|
+
function resolveContext(options) {
|
|
656
|
+
if (options.org && options.project) {
|
|
657
|
+
return { org: options.org, project: options.project };
|
|
658
|
+
}
|
|
659
|
+
const config = loadConfig();
|
|
660
|
+
if (config.org && config.project) {
|
|
661
|
+
return { org: config.org, project: config.project };
|
|
662
|
+
}
|
|
663
|
+
let gitContext = null;
|
|
664
|
+
try {
|
|
665
|
+
gitContext = detectAzdoContext();
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
const org = config.org || gitContext?.org;
|
|
669
|
+
const project = config.project || gitContext?.project;
|
|
670
|
+
if (org && project) {
|
|
671
|
+
return { org, project };
|
|
672
|
+
}
|
|
673
|
+
throw new Error(
|
|
674
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
function createSetStateCommand() {
|
|
678
|
+
const command = new Command4("set-state");
|
|
679
|
+
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(
|
|
680
|
+
async (idStr, state, options) => {
|
|
681
|
+
const id = parseInt(idStr, 10);
|
|
682
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
683
|
+
process.stderr.write(
|
|
684
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
685
|
+
`
|
|
686
|
+
);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
const hasOrg = options.org !== void 0;
|
|
690
|
+
const hasProject = options.project !== void 0;
|
|
691
|
+
if (hasOrg !== hasProject) {
|
|
692
|
+
process.stderr.write(
|
|
693
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
694
|
+
);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
let context;
|
|
698
|
+
try {
|
|
699
|
+
context = resolveContext(options);
|
|
700
|
+
const credential = await resolvePat();
|
|
701
|
+
const operations = [
|
|
702
|
+
{ op: "add", path: "/fields/System.State", value: state }
|
|
703
|
+
];
|
|
704
|
+
const result = await updateWorkItem(context, id, credential.pat, "System.State", operations);
|
|
705
|
+
if (options.json) {
|
|
706
|
+
process.stdout.write(
|
|
707
|
+
JSON.stringify({
|
|
708
|
+
id: result.id,
|
|
709
|
+
rev: result.rev,
|
|
710
|
+
title: result.title,
|
|
711
|
+
field: result.fieldName,
|
|
712
|
+
value: result.fieldValue
|
|
713
|
+
}) + "\n"
|
|
714
|
+
);
|
|
715
|
+
} else {
|
|
716
|
+
process.stdout.write(`Updated work item ${result.id}: State -> ${state}
|
|
717
|
+
`);
|
|
718
|
+
}
|
|
719
|
+
} catch (err) {
|
|
720
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
721
|
+
const msg = error.message;
|
|
722
|
+
if (msg === "AUTH_FAILED") {
|
|
723
|
+
process.stderr.write(
|
|
724
|
+
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
725
|
+
);
|
|
726
|
+
} else if (msg === "PERMISSION_DENIED") {
|
|
727
|
+
process.stderr.write(
|
|
728
|
+
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
729
|
+
`
|
|
730
|
+
);
|
|
731
|
+
} else if (msg === "NOT_FOUND") {
|
|
732
|
+
process.stderr.write(
|
|
733
|
+
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
734
|
+
`
|
|
735
|
+
);
|
|
736
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
737
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
738
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
739
|
+
`);
|
|
740
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
741
|
+
process.stderr.write(
|
|
742
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
process.stderr.write(`Error: ${msg}
|
|
746
|
+
`);
|
|
747
|
+
}
|
|
748
|
+
process.exit(1);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
return command;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/commands/assign.ts
|
|
756
|
+
import { Command as Command5 } from "commander";
|
|
757
|
+
function resolveContext2(options) {
|
|
758
|
+
if (options.org && options.project) {
|
|
759
|
+
return { org: options.org, project: options.project };
|
|
760
|
+
}
|
|
761
|
+
const config = loadConfig();
|
|
762
|
+
if (config.org && config.project) {
|
|
763
|
+
return { org: config.org, project: config.project };
|
|
764
|
+
}
|
|
765
|
+
let gitContext = null;
|
|
766
|
+
try {
|
|
767
|
+
gitContext = detectAzdoContext();
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
const org = config.org || gitContext?.org;
|
|
771
|
+
const project = config.project || gitContext?.project;
|
|
772
|
+
if (org && project) {
|
|
773
|
+
return { org, project };
|
|
774
|
+
}
|
|
775
|
+
throw new Error(
|
|
776
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function createAssignCommand() {
|
|
780
|
+
const command = new Command5("assign");
|
|
781
|
+
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(
|
|
782
|
+
async (idStr, name, options) => {
|
|
783
|
+
const id = parseInt(idStr, 10);
|
|
784
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
785
|
+
process.stderr.write(
|
|
786
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
787
|
+
`
|
|
788
|
+
);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
if (!name && !options.unassign) {
|
|
792
|
+
process.stderr.write(
|
|
793
|
+
"Error: Either provide a user name or use --unassign.\n"
|
|
794
|
+
);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
if (name && options.unassign) {
|
|
798
|
+
process.stderr.write(
|
|
799
|
+
"Error: Cannot provide both a user name and --unassign.\n"
|
|
800
|
+
);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
const hasOrg = options.org !== void 0;
|
|
804
|
+
const hasProject = options.project !== void 0;
|
|
805
|
+
if (hasOrg !== hasProject) {
|
|
806
|
+
process.stderr.write(
|
|
807
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
808
|
+
);
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
let context;
|
|
812
|
+
try {
|
|
813
|
+
context = resolveContext2(options);
|
|
814
|
+
const credential = await resolvePat();
|
|
815
|
+
const value = options.unassign ? "" : name;
|
|
816
|
+
const operations = [
|
|
817
|
+
{ op: "add", path: "/fields/System.AssignedTo", value }
|
|
818
|
+
];
|
|
819
|
+
const result = await updateWorkItem(context, id, credential.pat, "System.AssignedTo", operations);
|
|
820
|
+
if (options.json) {
|
|
821
|
+
process.stdout.write(
|
|
822
|
+
JSON.stringify({
|
|
823
|
+
id: result.id,
|
|
824
|
+
rev: result.rev,
|
|
825
|
+
title: result.title,
|
|
826
|
+
field: result.fieldName,
|
|
827
|
+
value: result.fieldValue
|
|
828
|
+
}) + "\n"
|
|
829
|
+
);
|
|
830
|
+
} else {
|
|
831
|
+
const displayValue = options.unassign ? "(unassigned)" : name;
|
|
832
|
+
process.stdout.write(`Updated work item ${result.id}: Assigned To -> ${displayValue}
|
|
833
|
+
`);
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
837
|
+
const msg = error.message;
|
|
838
|
+
if (msg === "AUTH_FAILED") {
|
|
839
|
+
process.stderr.write(
|
|
840
|
+
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
841
|
+
);
|
|
842
|
+
} else if (msg === "PERMISSION_DENIED") {
|
|
843
|
+
process.stderr.write(
|
|
844
|
+
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
845
|
+
`
|
|
846
|
+
);
|
|
847
|
+
} else if (msg === "NOT_FOUND") {
|
|
848
|
+
process.stderr.write(
|
|
849
|
+
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
850
|
+
`
|
|
851
|
+
);
|
|
852
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
853
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
854
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
855
|
+
`);
|
|
856
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
857
|
+
process.stderr.write(
|
|
858
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
859
|
+
);
|
|
860
|
+
} else {
|
|
861
|
+
process.stderr.write(`Error: ${msg}
|
|
862
|
+
`);
|
|
863
|
+
}
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
return command;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/commands/set-field.ts
|
|
872
|
+
import { Command as Command6 } from "commander";
|
|
873
|
+
function resolveContext3(options) {
|
|
874
|
+
if (options.org && options.project) {
|
|
875
|
+
return { org: options.org, project: options.project };
|
|
876
|
+
}
|
|
877
|
+
const config = loadConfig();
|
|
878
|
+
if (config.org && config.project) {
|
|
879
|
+
return { org: config.org, project: config.project };
|
|
880
|
+
}
|
|
881
|
+
let gitContext = null;
|
|
882
|
+
try {
|
|
883
|
+
gitContext = detectAzdoContext();
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
const org = config.org || gitContext?.org;
|
|
887
|
+
const project = config.project || gitContext?.project;
|
|
888
|
+
if (org && project) {
|
|
889
|
+
return { org, project };
|
|
890
|
+
}
|
|
891
|
+
throw new Error(
|
|
892
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
function createSetFieldCommand() {
|
|
896
|
+
const command = new Command6("set-field");
|
|
897
|
+
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(
|
|
898
|
+
async (idStr, field, value, options) => {
|
|
899
|
+
const id = parseInt(idStr, 10);
|
|
900
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
901
|
+
process.stderr.write(
|
|
902
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
903
|
+
`
|
|
904
|
+
);
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
const hasOrg = options.org !== void 0;
|
|
908
|
+
const hasProject = options.project !== void 0;
|
|
909
|
+
if (hasOrg !== hasProject) {
|
|
910
|
+
process.stderr.write(
|
|
911
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
912
|
+
);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
let context;
|
|
916
|
+
try {
|
|
917
|
+
context = resolveContext3(options);
|
|
918
|
+
const credential = await resolvePat();
|
|
919
|
+
const operations = [
|
|
920
|
+
{ op: "add", path: `/fields/${field}`, value }
|
|
921
|
+
];
|
|
922
|
+
const result = await updateWorkItem(context, id, credential.pat, field, operations);
|
|
923
|
+
if (options.json) {
|
|
924
|
+
process.stdout.write(
|
|
925
|
+
JSON.stringify({
|
|
926
|
+
id: result.id,
|
|
927
|
+
rev: result.rev,
|
|
928
|
+
title: result.title,
|
|
929
|
+
field: result.fieldName,
|
|
930
|
+
value: result.fieldValue
|
|
931
|
+
}) + "\n"
|
|
932
|
+
);
|
|
933
|
+
} else {
|
|
934
|
+
process.stdout.write(`Updated work item ${result.id}: ${field} -> ${value}
|
|
935
|
+
`);
|
|
936
|
+
}
|
|
937
|
+
} catch (err) {
|
|
938
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
939
|
+
const msg = error.message;
|
|
940
|
+
if (msg === "AUTH_FAILED") {
|
|
941
|
+
process.stderr.write(
|
|
942
|
+
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (Read & Write)" scope.\n'
|
|
943
|
+
);
|
|
944
|
+
} else if (msg === "PERMISSION_DENIED") {
|
|
945
|
+
process.stderr.write(
|
|
946
|
+
`Error: Access denied. Your PAT may lack write permissions for project "${context.project}".
|
|
947
|
+
`
|
|
948
|
+
);
|
|
949
|
+
} else if (msg === "NOT_FOUND") {
|
|
950
|
+
process.stderr.write(
|
|
951
|
+
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
952
|
+
`
|
|
953
|
+
);
|
|
954
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
955
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
956
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
957
|
+
`);
|
|
958
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
959
|
+
process.stderr.write(
|
|
960
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
961
|
+
);
|
|
962
|
+
} else {
|
|
963
|
+
process.stderr.write(`Error: ${msg}
|
|
964
|
+
`);
|
|
965
|
+
}
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
);
|
|
970
|
+
return command;
|
|
971
|
+
}
|
|
972
|
+
|
|
322
973
|
// src/index.ts
|
|
323
|
-
var program = new
|
|
974
|
+
var program = new Command7();
|
|
324
975
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
325
976
|
program.addCommand(createGetItemCommand());
|
|
326
977
|
program.addCommand(createClearPatCommand());
|
|
978
|
+
program.addCommand(createConfigCommand());
|
|
979
|
+
program.addCommand(createSetStateCommand());
|
|
980
|
+
program.addCommand(createAssignCommand());
|
|
981
|
+
program.addCommand(createSetFieldCommand());
|
|
327
982
|
program.showHelpAfterError();
|
|
328
983
|
program.parse();
|
|
329
984
|
if (process.argv.length <= 2) {
|