azdo-cli 0.2.0-002-get-item-command.9 → 0.2.0-003-cli-settings.15

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.
Files changed (2) hide show
  1. package/dist/index.js +253 -10
  2. 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 Command2 } from "commander";
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
- async function getWorkItem(context, id, pat) {
19
- const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
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: data.fields["System.Description"] ?? null,
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,72 @@ 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 VALID_KEYS = ["org", "project", "fields"];
228
+ function getConfigPath() {
229
+ return path.join(os.homedir(), ".azdo", "config.json");
230
+ }
231
+ function loadConfig() {
232
+ const configPath = getConfigPath();
233
+ let raw;
234
+ try {
235
+ raw = fs.readFileSync(configPath, "utf-8");
236
+ } catch (err) {
237
+ if (err.code === "ENOENT") {
238
+ return {};
239
+ }
240
+ throw err;
241
+ }
242
+ try {
243
+ return JSON.parse(raw);
244
+ } catch {
245
+ process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
246
+ `);
247
+ return {};
248
+ }
249
+ }
250
+ function saveConfig(config) {
251
+ const configPath = getConfigPath();
252
+ const dir = path.dirname(configPath);
253
+ fs.mkdirSync(dir, { recursive: true });
254
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
255
+ }
256
+ function validateKey(key) {
257
+ if (!VALID_KEYS.includes(key)) {
258
+ throw new Error(`Unknown setting key "${key}". Valid keys: org, project, fields`);
259
+ }
260
+ }
261
+ function getConfigValue(key) {
262
+ validateKey(key);
263
+ const config = loadConfig();
264
+ return config[key];
265
+ }
266
+ function setConfigValue(key, value) {
267
+ validateKey(key);
268
+ const config = loadConfig();
269
+ if (value === "") {
270
+ delete config[key];
271
+ } else if (key === "fields") {
272
+ config.fields = value.split(",").map((s) => s.trim());
273
+ } else {
274
+ config[key] = value;
275
+ }
276
+ saveConfig(config);
277
+ }
278
+ function unsetConfigValue(key) {
279
+ validateKey(key);
280
+ const config = loadConfig();
281
+ delete config[key];
282
+ saveConfig(config);
283
+ }
284
+
172
285
  // src/commands/get-item.ts
173
286
  function stripHtml(html) {
174
287
  let text = html;
288
+ text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
175
289
  text = text.replace(/<br\s*\/?>/gi, "\n");
176
290
  text = text.replace(/<\/?(p|div)>/gi, "\n");
177
291
  text = text.replace(/<li>/gi, "\n");
@@ -198,6 +312,12 @@ function formatWorkItem(workItem, short) {
198
312
  lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
199
313
  }
200
314
  lines.push(`${label("URL:")}${workItem.url}`);
315
+ if (workItem.extraFields) {
316
+ for (const [refName, value] of Object.entries(workItem.extraFields)) {
317
+ const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
318
+ lines.push(`${fieldLabel.padEnd(13)}${value}`);
319
+ }
320
+ }
201
321
  lines.push("");
202
322
  const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
203
323
  if (short) {
@@ -214,7 +334,7 @@ function formatWorkItem(workItem, short) {
214
334
  }
215
335
  function createGetItemCommand() {
216
336
  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(
337
+ 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
338
  async (idStr, options) => {
219
339
  const id = parseInt(idStr, 10);
220
340
  if (!Number.isInteger(id) || id <= 0) {
@@ -237,10 +357,22 @@ function createGetItemCommand() {
237
357
  if (options.org && options.project) {
238
358
  context = { org: options.org, project: options.project };
239
359
  } else {
240
- context = detectAzdoContext();
360
+ try {
361
+ context = detectAzdoContext();
362
+ } catch {
363
+ const config = loadConfig();
364
+ if (config.org && config.project) {
365
+ context = { org: config.org, project: config.project };
366
+ } else {
367
+ throw new Error(
368
+ 'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
369
+ );
370
+ }
371
+ }
241
372
  }
242
373
  const credential = await resolvePat();
243
- const workItem = await getWorkItem(context, id, credential.pat);
374
+ const fieldsList = options.fields ? options.fields.split(",").map((f) => f.trim()) : loadConfig().fields;
375
+ const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
244
376
  const output = formatWorkItem(workItem, options.short ?? false);
245
377
  process.stdout.write(output + "\n");
246
378
  } catch (err) {
@@ -278,10 +410,121 @@ function createGetItemCommand() {
278
410
  return command;
279
411
  }
280
412
 
413
+ // src/commands/clear-pat.ts
414
+ import { Command as Command2 } from "commander";
415
+ function createClearPatCommand() {
416
+ const command = new Command2("clear-pat");
417
+ command.description("Remove the stored Azure DevOps PAT from the credential store").action(async () => {
418
+ const deleted = await deletePat();
419
+ if (deleted) {
420
+ process.stdout.write("PAT removed from credential store.\n");
421
+ } else {
422
+ process.stdout.write("No stored PAT found.\n");
423
+ }
424
+ });
425
+ return command;
426
+ }
427
+
428
+ // src/commands/config.ts
429
+ import { Command as Command3 } from "commander";
430
+ function createConfigCommand() {
431
+ const config = new Command3("config");
432
+ config.description("Manage CLI settings");
433
+ const set = new Command3("set");
434
+ 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) => {
435
+ try {
436
+ setConfigValue(key, value);
437
+ if (options.json) {
438
+ const output = { key, value };
439
+ if (key === "fields") {
440
+ output.value = value.split(",").map((s) => s.trim());
441
+ }
442
+ process.stdout.write(JSON.stringify(output) + "\n");
443
+ } else {
444
+ process.stdout.write(`Set "${key}" to "${value}"
445
+ `);
446
+ }
447
+ } catch (err) {
448
+ const message = err instanceof Error ? err.message : String(err);
449
+ process.stderr.write(`Error: ${message}
450
+ `);
451
+ process.exit(1);
452
+ }
453
+ });
454
+ const get = new Command3("get");
455
+ get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
456
+ try {
457
+ const value = getConfigValue(key);
458
+ if (options.json) {
459
+ process.stdout.write(
460
+ JSON.stringify({ key, value: value ?? null }) + "\n"
461
+ );
462
+ } else if (value === void 0) {
463
+ process.stdout.write(`Setting "${key}" is not configured.
464
+ `);
465
+ } else if (Array.isArray(value)) {
466
+ process.stdout.write(value.join(",") + "\n");
467
+ } else {
468
+ process.stdout.write(value + "\n");
469
+ }
470
+ } catch (err) {
471
+ const message = err instanceof Error ? err.message : String(err);
472
+ process.stderr.write(`Error: ${message}
473
+ `);
474
+ process.exit(1);
475
+ }
476
+ });
477
+ const list = new Command3("list");
478
+ list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
479
+ const cfg = loadConfig();
480
+ if (options.json) {
481
+ process.stdout.write(JSON.stringify(cfg) + "\n");
482
+ } else {
483
+ const entries = [];
484
+ if (cfg.org) entries.push(["org", cfg.org]);
485
+ if (cfg.project) entries.push(["project", cfg.project]);
486
+ if (cfg.fields && cfg.fields.length > 0)
487
+ entries.push(["fields", cfg.fields.join(",")]);
488
+ if (entries.length === 0) {
489
+ process.stdout.write("No settings configured.\n");
490
+ } else {
491
+ for (const [k, v] of entries) {
492
+ process.stdout.write(`${k.padEnd(10)}${v}
493
+ `);
494
+ }
495
+ }
496
+ }
497
+ });
498
+ const unset = new Command3("unset");
499
+ unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
500
+ try {
501
+ unsetConfigValue(key);
502
+ if (options.json) {
503
+ process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
504
+ } else {
505
+ process.stdout.write(`Unset "${key}"
506
+ `);
507
+ }
508
+ } catch (err) {
509
+ const message = err instanceof Error ? err.message : String(err);
510
+ process.stderr.write(`Error: ${message}
511
+ `);
512
+ process.exit(1);
513
+ }
514
+ });
515
+ config.addCommand(set);
516
+ config.addCommand(get);
517
+ config.addCommand(list);
518
+ config.addCommand(unset);
519
+ return config;
520
+ }
521
+
281
522
  // src/index.ts
282
- var program = new Command2();
523
+ var program = new Command4();
283
524
  program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
284
525
  program.addCommand(createGetItemCommand());
526
+ program.addCommand(createClearPatCommand());
527
+ program.addCommand(createConfigCommand());
285
528
  program.showHelpAfterError();
286
529
  program.parse();
287
530
  if (process.argv.length <= 2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.2.0-002-get-item-command.9",
3
+ "version": "0.2.0-003-cli-settings.15",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {