azdo-cli 0.2.0-develop.14 → 0.2.0-develop.21
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 +300 -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 {
|
|
@@ -61,7 +86,8 @@ 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
|
|
65
91
|
};
|
|
66
92
|
}
|
|
67
93
|
|
|
@@ -175,7 +201,11 @@ function parseAzdoRemote(url) {
|
|
|
175
201
|
for (const pattern of patterns) {
|
|
176
202
|
const match = url.match(pattern);
|
|
177
203
|
if (match) {
|
|
178
|
-
|
|
204
|
+
const project = match[2];
|
|
205
|
+
if (/^DefaultCollection$/i.test(project)) {
|
|
206
|
+
return { org: match[1], project: "" };
|
|
207
|
+
}
|
|
208
|
+
return { org: match[1], project };
|
|
179
209
|
}
|
|
180
210
|
}
|
|
181
211
|
return null;
|
|
@@ -188,12 +218,97 @@ function detectAzdoContext() {
|
|
|
188
218
|
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
189
219
|
}
|
|
190
220
|
const context = parseAzdoRemote(remoteUrl);
|
|
191
|
-
if (!context) {
|
|
221
|
+
if (!context || !context.org && !context.project) {
|
|
192
222
|
throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
|
|
193
223
|
}
|
|
194
224
|
return context;
|
|
195
225
|
}
|
|
196
226
|
|
|
227
|
+
// src/services/config-store.ts
|
|
228
|
+
import fs from "fs";
|
|
229
|
+
import path from "path";
|
|
230
|
+
import os from "os";
|
|
231
|
+
var SETTINGS = [
|
|
232
|
+
{
|
|
233
|
+
key: "org",
|
|
234
|
+
description: "Azure DevOps organization name",
|
|
235
|
+
type: "string",
|
|
236
|
+
example: "mycompany",
|
|
237
|
+
required: true
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
key: "project",
|
|
241
|
+
description: "Azure DevOps project name",
|
|
242
|
+
type: "string",
|
|
243
|
+
example: "MyProject",
|
|
244
|
+
required: true
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
key: "fields",
|
|
248
|
+
description: "Extra work item fields to include (comma-separated reference names)",
|
|
249
|
+
type: "string[]",
|
|
250
|
+
example: "System.Tags,Custom.Priority",
|
|
251
|
+
required: false
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
255
|
+
function getConfigPath() {
|
|
256
|
+
return path.join(os.homedir(), ".azdo", "config.json");
|
|
257
|
+
}
|
|
258
|
+
function loadConfig() {
|
|
259
|
+
const configPath = getConfigPath();
|
|
260
|
+
let raw;
|
|
261
|
+
try {
|
|
262
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err.code === "ENOENT") {
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(raw);
|
|
271
|
+
} catch {
|
|
272
|
+
process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
|
|
273
|
+
`);
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function saveConfig(config) {
|
|
278
|
+
const configPath = getConfigPath();
|
|
279
|
+
const dir = path.dirname(configPath);
|
|
280
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
281
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
282
|
+
}
|
|
283
|
+
function validateKey(key) {
|
|
284
|
+
if (!VALID_KEYS.includes(key)) {
|
|
285
|
+
throw new Error(`Unknown setting key "${key}". Valid keys: org, project, fields`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function getConfigValue(key) {
|
|
289
|
+
validateKey(key);
|
|
290
|
+
const config = loadConfig();
|
|
291
|
+
return config[key];
|
|
292
|
+
}
|
|
293
|
+
function setConfigValue(key, value) {
|
|
294
|
+
validateKey(key);
|
|
295
|
+
const config = loadConfig();
|
|
296
|
+
if (value === "") {
|
|
297
|
+
delete config[key];
|
|
298
|
+
} else if (key === "fields") {
|
|
299
|
+
config.fields = value.split(",").map((s) => s.trim());
|
|
300
|
+
} else {
|
|
301
|
+
config[key] = value;
|
|
302
|
+
}
|
|
303
|
+
saveConfig(config);
|
|
304
|
+
}
|
|
305
|
+
function unsetConfigValue(key) {
|
|
306
|
+
validateKey(key);
|
|
307
|
+
const config = loadConfig();
|
|
308
|
+
delete config[key];
|
|
309
|
+
saveConfig(config);
|
|
310
|
+
}
|
|
311
|
+
|
|
197
312
|
// src/commands/get-item.ts
|
|
198
313
|
function stripHtml(html) {
|
|
199
314
|
let text = html;
|
|
@@ -224,6 +339,12 @@ function formatWorkItem(workItem, short) {
|
|
|
224
339
|
lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
|
|
225
340
|
}
|
|
226
341
|
lines.push(`${label("URL:")}${workItem.url}`);
|
|
342
|
+
if (workItem.extraFields) {
|
|
343
|
+
for (const [refName, value] of Object.entries(workItem.extraFields)) {
|
|
344
|
+
const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
|
|
345
|
+
lines.push(`${fieldLabel.padEnd(13)}${value}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
227
348
|
lines.push("");
|
|
228
349
|
const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
|
|
229
350
|
if (short) {
|
|
@@ -240,7 +361,7 @@ function formatWorkItem(workItem, short) {
|
|
|
240
361
|
}
|
|
241
362
|
function createGetItemCommand() {
|
|
242
363
|
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(
|
|
364
|
+
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
365
|
async (idStr, options) => {
|
|
245
366
|
const id = parseInt(idStr, 10);
|
|
246
367
|
if (!Number.isInteger(id) || id <= 0) {
|
|
@@ -263,10 +384,29 @@ function createGetItemCommand() {
|
|
|
263
384
|
if (options.org && options.project) {
|
|
264
385
|
context = { org: options.org, project: options.project };
|
|
265
386
|
} else {
|
|
266
|
-
|
|
387
|
+
const config = loadConfig();
|
|
388
|
+
if (config.org && config.project) {
|
|
389
|
+
context = { org: config.org, project: config.project };
|
|
390
|
+
} else {
|
|
391
|
+
let gitContext = null;
|
|
392
|
+
try {
|
|
393
|
+
gitContext = detectAzdoContext();
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
const org = config.org || gitContext?.org;
|
|
397
|
+
const project = config.project || gitContext?.project;
|
|
398
|
+
if (org && project) {
|
|
399
|
+
context = { org, project };
|
|
400
|
+
} else {
|
|
401
|
+
throw new Error(
|
|
402
|
+
'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
267
406
|
}
|
|
268
407
|
const credential = await resolvePat();
|
|
269
|
-
const
|
|
408
|
+
const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
|
|
409
|
+
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
270
410
|
const output = formatWorkItem(workItem, options.short ?? false);
|
|
271
411
|
process.stdout.write(output + "\n");
|
|
272
412
|
} catch (err) {
|
|
@@ -319,11 +459,161 @@ function createClearPatCommand() {
|
|
|
319
459
|
return command;
|
|
320
460
|
}
|
|
321
461
|
|
|
462
|
+
// src/commands/config.ts
|
|
463
|
+
import { Command as Command3 } from "commander";
|
|
464
|
+
import { createInterface as createInterface2 } from "readline";
|
|
465
|
+
function createConfigCommand() {
|
|
466
|
+
const config = new Command3("config");
|
|
467
|
+
config.description("Manage CLI settings");
|
|
468
|
+
const set = new Command3("set");
|
|
469
|
+
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) => {
|
|
470
|
+
try {
|
|
471
|
+
setConfigValue(key, value);
|
|
472
|
+
if (options.json) {
|
|
473
|
+
const output = { key, value };
|
|
474
|
+
if (key === "fields") {
|
|
475
|
+
output.value = value.split(",").map((s) => s.trim());
|
|
476
|
+
}
|
|
477
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
478
|
+
} else {
|
|
479
|
+
process.stdout.write(`Set "${key}" to "${value}"
|
|
480
|
+
`);
|
|
481
|
+
}
|
|
482
|
+
} catch (err) {
|
|
483
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
484
|
+
process.stderr.write(`Error: ${message}
|
|
485
|
+
`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
const get = new Command3("get");
|
|
490
|
+
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
491
|
+
try {
|
|
492
|
+
const value = getConfigValue(key);
|
|
493
|
+
if (options.json) {
|
|
494
|
+
process.stdout.write(
|
|
495
|
+
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
496
|
+
);
|
|
497
|
+
} else if (value === void 0) {
|
|
498
|
+
process.stdout.write(`Setting "${key}" is not configured.
|
|
499
|
+
`);
|
|
500
|
+
} else if (Array.isArray(value)) {
|
|
501
|
+
process.stdout.write(value.join(",") + "\n");
|
|
502
|
+
} else {
|
|
503
|
+
process.stdout.write(value + "\n");
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
507
|
+
process.stderr.write(`Error: ${message}
|
|
508
|
+
`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
const list = new Command3("list");
|
|
513
|
+
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
514
|
+
const cfg = loadConfig();
|
|
515
|
+
if (options.json) {
|
|
516
|
+
process.stdout.write(JSON.stringify(cfg) + "\n");
|
|
517
|
+
} else {
|
|
518
|
+
const keyWidth = 10;
|
|
519
|
+
const valueWidth = 30;
|
|
520
|
+
for (const setting of SETTINGS) {
|
|
521
|
+
const raw = cfg[setting.key];
|
|
522
|
+
const value = raw === void 0 ? "(not set)" : Array.isArray(raw) ? raw.join(",") : raw;
|
|
523
|
+
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
524
|
+
process.stdout.write(
|
|
525
|
+
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
526
|
+
`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const hasUnset = SETTINGS.some(
|
|
530
|
+
(s) => s.required && cfg[s.key] === void 0
|
|
531
|
+
);
|
|
532
|
+
if (hasUnset) {
|
|
533
|
+
process.stdout.write(
|
|
534
|
+
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
const unset = new Command3("unset");
|
|
540
|
+
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
541
|
+
try {
|
|
542
|
+
unsetConfigValue(key);
|
|
543
|
+
if (options.json) {
|
|
544
|
+
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
545
|
+
} else {
|
|
546
|
+
process.stdout.write(`Unset "${key}"
|
|
547
|
+
`);
|
|
548
|
+
}
|
|
549
|
+
} catch (err) {
|
|
550
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
551
|
+
process.stderr.write(`Error: ${message}
|
|
552
|
+
`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
const wizard = new Command3("wizard");
|
|
557
|
+
wizard.description("Interactive wizard to configure all settings").action(async () => {
|
|
558
|
+
if (!process.stdin.isTTY) {
|
|
559
|
+
process.stderr.write(
|
|
560
|
+
"Error: Wizard requires an interactive terminal.\n"
|
|
561
|
+
);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
const cfg = loadConfig();
|
|
565
|
+
const rl = createInterface2({
|
|
566
|
+
input: process.stdin,
|
|
567
|
+
output: process.stderr
|
|
568
|
+
});
|
|
569
|
+
const ask = (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
570
|
+
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
571
|
+
process.stderr.write("=======================================\n\n");
|
|
572
|
+
for (const setting of SETTINGS) {
|
|
573
|
+
const current = cfg[setting.key];
|
|
574
|
+
const currentDisplay = current === void 0 ? "" : Array.isArray(current) ? current.join(",") : current;
|
|
575
|
+
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
576
|
+
process.stderr.write(`${setting.description}${requiredTag}
|
|
577
|
+
`);
|
|
578
|
+
if (setting.example) {
|
|
579
|
+
process.stderr.write(` Example: ${setting.example}
|
|
580
|
+
`);
|
|
581
|
+
}
|
|
582
|
+
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
583
|
+
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
584
|
+
const trimmed = answer.trim();
|
|
585
|
+
if (trimmed) {
|
|
586
|
+
setConfigValue(setting.key, trimmed);
|
|
587
|
+
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
588
|
+
|
|
589
|
+
`);
|
|
590
|
+
} else if (currentDisplay) {
|
|
591
|
+
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
592
|
+
|
|
593
|
+
`);
|
|
594
|
+
} else {
|
|
595
|
+
process.stderr.write(` -> Skipped "${setting.key}"
|
|
596
|
+
|
|
597
|
+
`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
rl.close();
|
|
601
|
+
process.stderr.write("Configuration complete!\n");
|
|
602
|
+
});
|
|
603
|
+
config.addCommand(set);
|
|
604
|
+
config.addCommand(get);
|
|
605
|
+
config.addCommand(list);
|
|
606
|
+
config.addCommand(unset);
|
|
607
|
+
config.addCommand(wizard);
|
|
608
|
+
return config;
|
|
609
|
+
}
|
|
610
|
+
|
|
322
611
|
// src/index.ts
|
|
323
|
-
var program = new
|
|
612
|
+
var program = new Command4();
|
|
324
613
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
325
614
|
program.addCommand(createGetItemCommand());
|
|
326
615
|
program.addCommand(createClearPatCommand());
|
|
616
|
+
program.addCommand(createConfigCommand());
|
|
327
617
|
program.showHelpAfterError();
|
|
328
618
|
program.parse();
|
|
329
619
|
if (process.argv.length <= 2) {
|