azdo-cli 0.2.0-002-get-item-command.10 → 0.2.0-003-cli-settings.17
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 +331 -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 Command4 } 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 {
|
|
@@ -35,6 +60,22 @@ async function getWorkItem(context, id, pat) {
|
|
|
35
60
|
throw new Error(`HTTP_${response.status}`);
|
|
36
61
|
}
|
|
37
62
|
const data = await response.json();
|
|
63
|
+
const descriptionParts = [];
|
|
64
|
+
if (data.fields["System.Description"]) {
|
|
65
|
+
descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
|
|
66
|
+
}
|
|
67
|
+
if (data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"]) {
|
|
68
|
+
descriptionParts.push({ label: "Acceptance Criteria", value: data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] });
|
|
69
|
+
}
|
|
70
|
+
if (data.fields["Microsoft.VSTS.TCM.ReproSteps"]) {
|
|
71
|
+
descriptionParts.push({ label: "Repro Steps", value: data.fields["Microsoft.VSTS.TCM.ReproSteps"] });
|
|
72
|
+
}
|
|
73
|
+
let combinedDescription = null;
|
|
74
|
+
if (descriptionParts.length === 1) {
|
|
75
|
+
combinedDescription = descriptionParts[0].value;
|
|
76
|
+
} else if (descriptionParts.length > 1) {
|
|
77
|
+
combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
|
|
78
|
+
}
|
|
38
79
|
return {
|
|
39
80
|
id: data.id,
|
|
40
81
|
rev: data.rev,
|
|
@@ -42,10 +83,11 @@ async function getWorkItem(context, id, pat) {
|
|
|
42
83
|
state: data.fields["System.State"],
|
|
43
84
|
type: data.fields["System.WorkItemType"],
|
|
44
85
|
assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
|
|
45
|
-
description:
|
|
86
|
+
description: combinedDescription,
|
|
46
87
|
areaPath: data.fields["System.AreaPath"],
|
|
47
88
|
iterationPath: data.fields["System.IterationPath"],
|
|
48
|
-
url: data._links.html.href
|
|
89
|
+
url: data._links.html.href,
|
|
90
|
+
extraFields: extraFields && extraFields.length > 0 ? buildExtraFields(data.fields, extraFields) : null
|
|
49
91
|
};
|
|
50
92
|
}
|
|
51
93
|
|
|
@@ -71,6 +113,15 @@ async function storePat(pat) {
|
|
|
71
113
|
} catch {
|
|
72
114
|
}
|
|
73
115
|
}
|
|
116
|
+
async function deletePat() {
|
|
117
|
+
try {
|
|
118
|
+
const entry = new Entry(SERVICE, ACCOUNT);
|
|
119
|
+
entry.deletePassword();
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
74
125
|
|
|
75
126
|
// src/services/auth.ts
|
|
76
127
|
async function promptForPat() {
|
|
@@ -107,7 +158,7 @@ async function promptForPat() {
|
|
|
107
158
|
}
|
|
108
159
|
} else {
|
|
109
160
|
pat += ch;
|
|
110
|
-
process.stderr.write("*");
|
|
161
|
+
process.stderr.write("*".repeat(ch.length));
|
|
111
162
|
}
|
|
112
163
|
};
|
|
113
164
|
process.stdin.on("data", onData);
|
|
@@ -169,9 +220,95 @@ function detectAzdoContext() {
|
|
|
169
220
|
return context;
|
|
170
221
|
}
|
|
171
222
|
|
|
223
|
+
// src/services/config-store.ts
|
|
224
|
+
import fs from "fs";
|
|
225
|
+
import path from "path";
|
|
226
|
+
import os from "os";
|
|
227
|
+
var SETTINGS = [
|
|
228
|
+
{
|
|
229
|
+
key: "org",
|
|
230
|
+
description: "Azure DevOps organization name",
|
|
231
|
+
type: "string",
|
|
232
|
+
example: "mycompany",
|
|
233
|
+
required: true
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
key: "project",
|
|
237
|
+
description: "Azure DevOps project name",
|
|
238
|
+
type: "string",
|
|
239
|
+
example: "MyProject",
|
|
240
|
+
required: true
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: "fields",
|
|
244
|
+
description: "Extra work item fields to include (comma-separated reference names)",
|
|
245
|
+
type: "string[]",
|
|
246
|
+
example: "System.Tags,Custom.Priority",
|
|
247
|
+
required: false
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
251
|
+
function getConfigPath() {
|
|
252
|
+
return path.join(os.homedir(), ".azdo", "config.json");
|
|
253
|
+
}
|
|
254
|
+
function loadConfig() {
|
|
255
|
+
const configPath = getConfigPath();
|
|
256
|
+
let raw;
|
|
257
|
+
try {
|
|
258
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.code === "ENOENT") {
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(raw);
|
|
267
|
+
} catch {
|
|
268
|
+
process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
|
|
269
|
+
`);
|
|
270
|
+
return {};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function saveConfig(config) {
|
|
274
|
+
const configPath = getConfigPath();
|
|
275
|
+
const dir = path.dirname(configPath);
|
|
276
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
277
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
278
|
+
}
|
|
279
|
+
function validateKey(key) {
|
|
280
|
+
if (!VALID_KEYS.includes(key)) {
|
|
281
|
+
throw new Error(`Unknown setting key "${key}". Valid keys: org, project, fields`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function getConfigValue(key) {
|
|
285
|
+
validateKey(key);
|
|
286
|
+
const config = loadConfig();
|
|
287
|
+
return config[key];
|
|
288
|
+
}
|
|
289
|
+
function setConfigValue(key, value) {
|
|
290
|
+
validateKey(key);
|
|
291
|
+
const config = loadConfig();
|
|
292
|
+
if (value === "") {
|
|
293
|
+
delete config[key];
|
|
294
|
+
} else if (key === "fields") {
|
|
295
|
+
config.fields = value.split(",").map((s) => s.trim());
|
|
296
|
+
} else {
|
|
297
|
+
config[key] = value;
|
|
298
|
+
}
|
|
299
|
+
saveConfig(config);
|
|
300
|
+
}
|
|
301
|
+
function unsetConfigValue(key) {
|
|
302
|
+
validateKey(key);
|
|
303
|
+
const config = loadConfig();
|
|
304
|
+
delete config[key];
|
|
305
|
+
saveConfig(config);
|
|
306
|
+
}
|
|
307
|
+
|
|
172
308
|
// src/commands/get-item.ts
|
|
173
309
|
function stripHtml(html) {
|
|
174
310
|
let text = html;
|
|
311
|
+
text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
|
|
175
312
|
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
176
313
|
text = text.replace(/<\/?(p|div)>/gi, "\n");
|
|
177
314
|
text = text.replace(/<li>/gi, "\n");
|
|
@@ -198,6 +335,12 @@ function formatWorkItem(workItem, short) {
|
|
|
198
335
|
lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
|
|
199
336
|
}
|
|
200
337
|
lines.push(`${label("URL:")}${workItem.url}`);
|
|
338
|
+
if (workItem.extraFields) {
|
|
339
|
+
for (const [refName, value] of Object.entries(workItem.extraFields)) {
|
|
340
|
+
const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
|
|
341
|
+
lines.push(`${fieldLabel.padEnd(13)}${value}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
201
344
|
lines.push("");
|
|
202
345
|
const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
|
|
203
346
|
if (short) {
|
|
@@ -214,7 +357,7 @@ function formatWorkItem(workItem, short) {
|
|
|
214
357
|
}
|
|
215
358
|
function createGetItemCommand() {
|
|
216
359
|
const command = new Command("get-item");
|
|
217
|
-
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(
|
|
360
|
+
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(
|
|
218
361
|
async (idStr, options) => {
|
|
219
362
|
const id = parseInt(idStr, 10);
|
|
220
363
|
if (!Number.isInteger(id) || id <= 0) {
|
|
@@ -237,10 +380,22 @@ function createGetItemCommand() {
|
|
|
237
380
|
if (options.org && options.project) {
|
|
238
381
|
context = { org: options.org, project: options.project };
|
|
239
382
|
} else {
|
|
240
|
-
|
|
383
|
+
try {
|
|
384
|
+
context = detectAzdoContext();
|
|
385
|
+
} catch {
|
|
386
|
+
const config = loadConfig();
|
|
387
|
+
if (config.org && config.project) {
|
|
388
|
+
context = { org: config.org, project: config.project };
|
|
389
|
+
} else {
|
|
390
|
+
throw new Error(
|
|
391
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
241
395
|
}
|
|
242
396
|
const credential = await resolvePat();
|
|
243
|
-
const
|
|
397
|
+
const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
|
|
398
|
+
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
244
399
|
const output = formatWorkItem(workItem, options.short ?? false);
|
|
245
400
|
process.stdout.write(output + "\n");
|
|
246
401
|
} catch (err) {
|
|
@@ -278,10 +433,176 @@ function createGetItemCommand() {
|
|
|
278
433
|
return command;
|
|
279
434
|
}
|
|
280
435
|
|
|
436
|
+
// src/commands/clear-pat.ts
|
|
437
|
+
import { Command as Command2 } from "commander";
|
|
438
|
+
function createClearPatCommand() {
|
|
439
|
+
const command = new Command2("clear-pat");
|
|
440
|
+
command.description("Remove the stored Azure DevOps PAT from the credential store").action(async () => {
|
|
441
|
+
const deleted = await deletePat();
|
|
442
|
+
if (deleted) {
|
|
443
|
+
process.stdout.write("PAT removed from credential store.\n");
|
|
444
|
+
} else {
|
|
445
|
+
process.stdout.write("No stored PAT found.\n");
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
return command;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/commands/config.ts
|
|
452
|
+
import { Command as Command3 } from "commander";
|
|
453
|
+
import { createInterface as createInterface2 } from "readline";
|
|
454
|
+
function createConfigCommand() {
|
|
455
|
+
const config = new Command3("config");
|
|
456
|
+
config.description("Manage CLI settings");
|
|
457
|
+
const set = new Command3("set");
|
|
458
|
+
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) => {
|
|
459
|
+
try {
|
|
460
|
+
setConfigValue(key, value);
|
|
461
|
+
if (options.json) {
|
|
462
|
+
const output = { key, value };
|
|
463
|
+
if (key === "fields") {
|
|
464
|
+
output.value = value.split(",").map((s) => s.trim());
|
|
465
|
+
}
|
|
466
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
467
|
+
} else {
|
|
468
|
+
process.stdout.write(`Set "${key}" to "${value}"
|
|
469
|
+
`);
|
|
470
|
+
}
|
|
471
|
+
} catch (err) {
|
|
472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
473
|
+
process.stderr.write(`Error: ${message}
|
|
474
|
+
`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
const get = new Command3("get");
|
|
479
|
+
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
480
|
+
try {
|
|
481
|
+
const value = getConfigValue(key);
|
|
482
|
+
if (options.json) {
|
|
483
|
+
process.stdout.write(
|
|
484
|
+
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
485
|
+
);
|
|
486
|
+
} else if (value === void 0) {
|
|
487
|
+
process.stdout.write(`Setting "${key}" is not configured.
|
|
488
|
+
`);
|
|
489
|
+
} else if (Array.isArray(value)) {
|
|
490
|
+
process.stdout.write(value.join(",") + "\n");
|
|
491
|
+
} else {
|
|
492
|
+
process.stdout.write(value + "\n");
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {
|
|
495
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
496
|
+
process.stderr.write(`Error: ${message}
|
|
497
|
+
`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
const list = new Command3("list");
|
|
502
|
+
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
503
|
+
const cfg = loadConfig();
|
|
504
|
+
if (options.json) {
|
|
505
|
+
process.stdout.write(JSON.stringify(cfg) + "\n");
|
|
506
|
+
} else {
|
|
507
|
+
const keyWidth = 10;
|
|
508
|
+
const valueWidth = 30;
|
|
509
|
+
for (const setting of SETTINGS) {
|
|
510
|
+
const raw = cfg[setting.key];
|
|
511
|
+
const value = raw === void 0 ? "(not set)" : Array.isArray(raw) ? raw.join(",") : raw;
|
|
512
|
+
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
513
|
+
process.stdout.write(
|
|
514
|
+
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
515
|
+
`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const hasUnset = SETTINGS.some(
|
|
519
|
+
(s) => s.required && cfg[s.key] === void 0
|
|
520
|
+
);
|
|
521
|
+
if (hasUnset) {
|
|
522
|
+
process.stdout.write(
|
|
523
|
+
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
const unset = new Command3("unset");
|
|
529
|
+
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
530
|
+
try {
|
|
531
|
+
unsetConfigValue(key);
|
|
532
|
+
if (options.json) {
|
|
533
|
+
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
534
|
+
} else {
|
|
535
|
+
process.stdout.write(`Unset "${key}"
|
|
536
|
+
`);
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
540
|
+
process.stderr.write(`Error: ${message}
|
|
541
|
+
`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
const wizard = new Command3("wizard");
|
|
546
|
+
wizard.description("Interactive wizard to configure all settings").action(async () => {
|
|
547
|
+
if (!process.stdin.isTTY) {
|
|
548
|
+
process.stderr.write(
|
|
549
|
+
"Error: Wizard requires an interactive terminal.\n"
|
|
550
|
+
);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
const cfg = loadConfig();
|
|
554
|
+
const rl = createInterface2({
|
|
555
|
+
input: process.stdin,
|
|
556
|
+
output: process.stderr
|
|
557
|
+
});
|
|
558
|
+
const ask = (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
559
|
+
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
560
|
+
process.stderr.write("=======================================\n\n");
|
|
561
|
+
for (const setting of SETTINGS) {
|
|
562
|
+
const current = cfg[setting.key];
|
|
563
|
+
const currentDisplay = current === void 0 ? "" : Array.isArray(current) ? current.join(",") : current;
|
|
564
|
+
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
565
|
+
process.stderr.write(`${setting.description}${requiredTag}
|
|
566
|
+
`);
|
|
567
|
+
if (setting.example) {
|
|
568
|
+
process.stderr.write(` Example: ${setting.example}
|
|
569
|
+
`);
|
|
570
|
+
}
|
|
571
|
+
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
572
|
+
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
573
|
+
const trimmed = answer.trim();
|
|
574
|
+
if (trimmed) {
|
|
575
|
+
setConfigValue(setting.key, trimmed);
|
|
576
|
+
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
577
|
+
|
|
578
|
+
`);
|
|
579
|
+
} else if (currentDisplay) {
|
|
580
|
+
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
581
|
+
|
|
582
|
+
`);
|
|
583
|
+
} else {
|
|
584
|
+
process.stderr.write(` -> Skipped "${setting.key}"
|
|
585
|
+
|
|
586
|
+
`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
rl.close();
|
|
590
|
+
process.stderr.write("Configuration complete!\n");
|
|
591
|
+
});
|
|
592
|
+
config.addCommand(set);
|
|
593
|
+
config.addCommand(get);
|
|
594
|
+
config.addCommand(list);
|
|
595
|
+
config.addCommand(unset);
|
|
596
|
+
config.addCommand(wizard);
|
|
597
|
+
return config;
|
|
598
|
+
}
|
|
599
|
+
|
|
281
600
|
// src/index.ts
|
|
282
|
-
var program = new
|
|
601
|
+
var program = new Command4();
|
|
283
602
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
284
603
|
program.addCommand(createGetItemCommand());
|
|
604
|
+
program.addCommand(createClearPatCommand());
|
|
605
|
+
program.addCommand(createConfigCommand());
|
|
285
606
|
program.showHelpAfterError();
|
|
286
607
|
program.parse();
|
|
287
608
|
if (process.argv.length <= 2) {
|