azdo-cli 0.2.0-develop.8 → 0.2.0-develop.88

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 (3) hide show
  1. package/README.md +240 -51
  2. package/dist/index.js +1400 -2
  3. package/package.json +5 -3
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 } from "commander";
4
+ import { Command as Command10 } from "commander";
5
5
 
6
6
  // src/version.ts
7
7
  import { readFileSync } from "fs";
@@ -11,9 +11,1407 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  var pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf-8"));
12
12
  var version = pkg.version;
13
13
 
14
+ // src/commands/get-item.ts
15
+ import { Command } from "commander";
16
+
17
+ // src/services/azdo-client.ts
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 authHeaders(pat) {
30
+ const token = Buffer.from(`:${pat}`).toString("base64");
31
+ return { Authorization: `Basic ${token}` };
32
+ }
33
+ async function fetchWithErrors(url, init) {
34
+ let response;
35
+ try {
36
+ response = await fetch(url, init);
37
+ } catch {
38
+ throw new Error("NETWORK_ERROR");
39
+ }
40
+ if (response.status === 401) throw new Error("AUTH_FAILED");
41
+ if (response.status === 403) throw new Error("PERMISSION_DENIED");
42
+ if (response.status === 404) throw new Error("NOT_FOUND");
43
+ return response;
44
+ }
45
+ async function readResponseMessage(response) {
46
+ try {
47
+ const body = await response.json();
48
+ if (typeof body.message === "string" && body.message.trim() !== "") {
49
+ return body.message.trim();
50
+ }
51
+ } catch {
52
+ }
53
+ return null;
54
+ }
55
+ function normalizeFieldList(fields) {
56
+ return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
57
+ }
58
+ function buildExtraFields(fields, requested) {
59
+ const result = {};
60
+ for (const name of requested) {
61
+ const val = fields[name];
62
+ if (val !== void 0 && val !== null) {
63
+ result[name] = String(val);
64
+ }
65
+ }
66
+ return Object.keys(result).length > 0 ? result : null;
67
+ }
68
+ function writeHeaders(pat) {
69
+ return {
70
+ ...authHeaders(pat),
71
+ "Content-Type": "application/json-patch+json"
72
+ };
73
+ }
74
+ async function readWriteResponse(response, errorCode) {
75
+ if (response.status === 400) {
76
+ const serverMessage = await readResponseMessage(response) ?? "Unknown error";
77
+ throw new Error(`${errorCode}: ${serverMessage}`);
78
+ }
79
+ if (!response.ok) {
80
+ throw new Error(`HTTP_${response.status}`);
81
+ }
82
+ const data = await response.json();
83
+ return {
84
+ id: data.id,
85
+ rev: data.rev,
86
+ fields: data.fields
87
+ };
88
+ }
89
+ async function getWorkItem(context, id, pat, extraFields) {
90
+ const url = new URL(
91
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
92
+ );
93
+ url.searchParams.set("api-version", "7.1");
94
+ const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
95
+ if (normalizedExtraFields.length > 0) {
96
+ const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
97
+ url.searchParams.set("fields", allFields.join(","));
98
+ }
99
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
100
+ if (response.status === 400) {
101
+ const serverMessage = await readResponseMessage(response);
102
+ if (serverMessage) {
103
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
104
+ }
105
+ }
106
+ if (!response.ok) {
107
+ throw new Error(`HTTP_${response.status}`);
108
+ }
109
+ const data = await response.json();
110
+ const descriptionParts = [];
111
+ if (data.fields["System.Description"]) {
112
+ descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
113
+ }
114
+ if (data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"]) {
115
+ descriptionParts.push({ label: "Acceptance Criteria", value: data.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] });
116
+ }
117
+ if (data.fields["Microsoft.VSTS.TCM.ReproSteps"]) {
118
+ descriptionParts.push({ label: "Repro Steps", value: data.fields["Microsoft.VSTS.TCM.ReproSteps"] });
119
+ }
120
+ let combinedDescription = null;
121
+ if (descriptionParts.length === 1) {
122
+ combinedDescription = descriptionParts[0].value;
123
+ } else if (descriptionParts.length > 1) {
124
+ combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
125
+ }
126
+ return {
127
+ id: data.id,
128
+ rev: data.rev,
129
+ title: data.fields["System.Title"],
130
+ state: data.fields["System.State"],
131
+ type: data.fields["System.WorkItemType"],
132
+ assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
133
+ description: combinedDescription,
134
+ areaPath: data.fields["System.AreaPath"],
135
+ iterationPath: data.fields["System.IterationPath"],
136
+ url: data._links.html.href,
137
+ extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
138
+ };
139
+ }
140
+ async function getWorkItemFieldValue(context, id, pat, fieldName) {
141
+ const url = new URL(
142
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
143
+ );
144
+ url.searchParams.set("api-version", "7.1");
145
+ url.searchParams.set("fields", fieldName);
146
+ const response = await fetchWithErrors(url.toString(), { headers: authHeaders(pat) });
147
+ if (response.status === 400) {
148
+ const serverMessage = await readResponseMessage(response);
149
+ if (serverMessage) {
150
+ throw new Error(`BAD_REQUEST: ${serverMessage}`);
151
+ }
152
+ }
153
+ if (!response.ok) {
154
+ throw new Error(`HTTP_${response.status}`);
155
+ }
156
+ const data = await response.json();
157
+ const value = data.fields[fieldName];
158
+ if (value === void 0 || value === null || value === "") {
159
+ return null;
160
+ }
161
+ return typeof value === "object" ? JSON.stringify(value) : `${value}`;
162
+ }
163
+ async function updateWorkItem(context, id, pat, fieldName, operations) {
164
+ const result = await applyWorkItemPatch(context, id, pat, operations);
165
+ const title = result.fields["System.Title"];
166
+ const lastOp = operations[operations.length - 1];
167
+ const fieldValue = lastOp.value ?? null;
168
+ return {
169
+ id: result.id,
170
+ rev: result.rev,
171
+ title: typeof title === "string" ? title : "",
172
+ fieldName,
173
+ fieldValue
174
+ };
175
+ }
176
+ async function createWorkItem(context, workItemType, pat, operations) {
177
+ const url = new URL(
178
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}`
179
+ );
180
+ url.searchParams.set("api-version", "7.1");
181
+ const response = await fetchWithErrors(url.toString(), {
182
+ method: "POST",
183
+ headers: writeHeaders(pat),
184
+ body: JSON.stringify(operations)
185
+ });
186
+ return readWriteResponse(response, "CREATE_REJECTED");
187
+ }
188
+ async function applyWorkItemPatch(context, id, pat, operations) {
189
+ const url = new URL(
190
+ `https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
191
+ );
192
+ url.searchParams.set("api-version", "7.1");
193
+ const response = await fetchWithErrors(url.toString(), {
194
+ method: "PATCH",
195
+ headers: writeHeaders(pat),
196
+ body: JSON.stringify(operations)
197
+ });
198
+ return readWriteResponse(response, "UPDATE_REJECTED");
199
+ }
200
+
201
+ // src/services/auth.ts
202
+ import { createInterface } from "readline";
203
+
204
+ // src/services/credential-store.ts
205
+ import { Entry } from "@napi-rs/keyring";
206
+ var SERVICE = "azdo-cli";
207
+ var ACCOUNT = "pat";
208
+ async function getPat() {
209
+ try {
210
+ const entry = new Entry(SERVICE, ACCOUNT);
211
+ return entry.getPassword();
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+ async function storePat(pat) {
217
+ try {
218
+ const entry = new Entry(SERVICE, ACCOUNT);
219
+ entry.setPassword(pat);
220
+ } catch {
221
+ }
222
+ }
223
+ async function deletePat() {
224
+ try {
225
+ const entry = new Entry(SERVICE, ACCOUNT);
226
+ entry.deletePassword();
227
+ return true;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ // src/services/auth.ts
234
+ function normalizePat(rawPat) {
235
+ const trimmedPat = rawPat.trim();
236
+ return trimmedPat.length > 0 ? trimmedPat : null;
237
+ }
238
+ async function promptForPat() {
239
+ if (!process.stdin.isTTY) {
240
+ return null;
241
+ }
242
+ return new Promise((resolve2) => {
243
+ const rl = createInterface({
244
+ input: process.stdin,
245
+ output: process.stderr
246
+ });
247
+ process.stderr.write("Enter your Azure DevOps PAT: ");
248
+ process.stdin.setRawMode(true);
249
+ process.stdin.resume();
250
+ let pat = "";
251
+ const onData = (key) => {
252
+ const ch = key.toString("utf8");
253
+ if (ch === "") {
254
+ process.stdin.setRawMode(false);
255
+ process.stdin.removeListener("data", onData);
256
+ rl.close();
257
+ process.stderr.write("\n");
258
+ resolve2(null);
259
+ } else if (ch === "\r" || ch === "\n") {
260
+ process.stdin.setRawMode(false);
261
+ process.stdin.removeListener("data", onData);
262
+ rl.close();
263
+ process.stderr.write("\n");
264
+ resolve2(pat);
265
+ } else if (ch === "\x7F" || ch === "\b") {
266
+ if (pat.length > 0) {
267
+ pat = pat.slice(0, -1);
268
+ process.stderr.write("\b \b");
269
+ }
270
+ } else {
271
+ pat += ch;
272
+ process.stderr.write("*".repeat(ch.length));
273
+ }
274
+ };
275
+ process.stdin.on("data", onData);
276
+ });
277
+ }
278
+ async function resolvePat() {
279
+ const envPat = process.env.AZDO_PAT;
280
+ if (envPat) {
281
+ return { pat: envPat, source: "env" };
282
+ }
283
+ const storedPat = await getPat();
284
+ if (storedPat !== null) {
285
+ return { pat: storedPat, source: "credential-store" };
286
+ }
287
+ const promptedPat = await promptForPat();
288
+ if (promptedPat !== null) {
289
+ const normalizedPat = normalizePat(promptedPat);
290
+ if (normalizedPat !== null) {
291
+ await storePat(normalizedPat);
292
+ return { pat: normalizedPat, source: "prompt" };
293
+ }
294
+ }
295
+ throw new Error(
296
+ "Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
297
+ );
298
+ }
299
+
300
+ // src/services/git-remote.ts
301
+ import { execSync } from "child_process";
302
+ var patterns = [
303
+ // HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
304
+ /^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/.+$/,
305
+ // HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
306
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/.+$/,
307
+ // HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
308
+ /^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/.+$/,
309
+ // SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
310
+ /^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/.+$/,
311
+ // SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
312
+ /^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/.+$/
313
+ ];
314
+ function parseAzdoRemote(url) {
315
+ for (const pattern of patterns) {
316
+ const match = url.match(pattern);
317
+ if (match) {
318
+ const project = match[2];
319
+ if (/^DefaultCollection$/i.test(project)) {
320
+ return { org: match[1], project: "" };
321
+ }
322
+ return { org: match[1], project };
323
+ }
324
+ }
325
+ return null;
326
+ }
327
+ function detectAzdoContext() {
328
+ let remoteUrl;
329
+ try {
330
+ remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
331
+ } catch {
332
+ throw new Error("Not in a git repository. Provide --org and --project explicitly.");
333
+ }
334
+ const context = parseAzdoRemote(remoteUrl);
335
+ if (!context || !context.org && !context.project) {
336
+ throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
337
+ }
338
+ return context;
339
+ }
340
+
341
+ // src/services/config-store.ts
342
+ import fs from "fs";
343
+ import path from "path";
344
+ import os from "os";
345
+ var SETTINGS = [
346
+ {
347
+ key: "org",
348
+ description: "Azure DevOps organization name",
349
+ type: "string",
350
+ example: "mycompany",
351
+ required: true
352
+ },
353
+ {
354
+ key: "project",
355
+ description: "Azure DevOps project name",
356
+ type: "string",
357
+ example: "MyProject",
358
+ required: true
359
+ },
360
+ {
361
+ key: "fields",
362
+ description: "Extra work item fields to include (comma-separated reference names)",
363
+ type: "string[]",
364
+ example: "System.Tags,Custom.Priority",
365
+ required: false
366
+ },
367
+ {
368
+ key: "markdown",
369
+ description: "Convert rich text fields to markdown on display",
370
+ type: "boolean",
371
+ example: "true",
372
+ required: false
373
+ }
374
+ ];
375
+ var VALID_KEYS = SETTINGS.map((s) => s.key);
376
+ function getConfigPath() {
377
+ return path.join(os.homedir(), ".azdo", "config.json");
378
+ }
379
+ function loadConfig() {
380
+ const configPath = getConfigPath();
381
+ let raw;
382
+ try {
383
+ raw = fs.readFileSync(configPath, "utf-8");
384
+ } catch (err) {
385
+ if (err.code === "ENOENT") {
386
+ return {};
387
+ }
388
+ throw err;
389
+ }
390
+ try {
391
+ return JSON.parse(raw);
392
+ } catch {
393
+ process.stderr.write(`Warning: Config file ${configPath} contains invalid JSON. Using defaults.
394
+ `);
395
+ return {};
396
+ }
397
+ }
398
+ function saveConfig(config) {
399
+ const configPath = getConfigPath();
400
+ const dir = path.dirname(configPath);
401
+ fs.mkdirSync(dir, { recursive: true });
402
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
403
+ }
404
+ function validateKey(key) {
405
+ if (!VALID_KEYS.includes(key)) {
406
+ throw new Error(`Unknown setting key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`);
407
+ }
408
+ }
409
+ function getConfigValue(key) {
410
+ validateKey(key);
411
+ const config = loadConfig();
412
+ return config[key];
413
+ }
414
+ function setConfigValue(key, value) {
415
+ validateKey(key);
416
+ const config = loadConfig();
417
+ if (value === "") {
418
+ delete config[key];
419
+ } else if (key === "markdown") {
420
+ if (value !== "true" && value !== "false") {
421
+ throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
422
+ }
423
+ config.markdown = value === "true";
424
+ } else if (key === "fields") {
425
+ config.fields = value.split(",").map((s) => s.trim());
426
+ } else {
427
+ config[key] = value;
428
+ }
429
+ saveConfig(config);
430
+ }
431
+ function unsetConfigValue(key) {
432
+ validateKey(key);
433
+ const config = loadConfig();
434
+ delete config[key];
435
+ saveConfig(config);
436
+ }
437
+
438
+ // src/services/context.ts
439
+ function resolveContext(options) {
440
+ if (options.org && options.project) {
441
+ return { org: options.org, project: options.project };
442
+ }
443
+ const config = loadConfig();
444
+ if (config.org && config.project) {
445
+ return { org: config.org, project: config.project };
446
+ }
447
+ let gitContext = null;
448
+ try {
449
+ gitContext = detectAzdoContext();
450
+ } catch {
451
+ }
452
+ const org = config.org || gitContext?.org;
453
+ const project = config.project || gitContext?.project;
454
+ if (org && project) {
455
+ return { org, project };
456
+ }
457
+ throw new Error(
458
+ 'Could not determine org/project. Use --org and --project flags, work from an Azure DevOps git repo, or run "azdo config set org/project".'
459
+ );
460
+ }
461
+
462
+ // src/services/md-convert.ts
463
+ import { NodeHtmlMarkdown } from "node-html-markdown";
464
+
465
+ // src/services/html-detect.ts
466
+ var HTML_TAG_REGEX = /<\/?([a-z][a-z0-9]*)\b/gi;
467
+ var HTML_TAGS = /* @__PURE__ */ new Set([
468
+ "p",
469
+ "br",
470
+ "div",
471
+ "span",
472
+ "strong",
473
+ "em",
474
+ "b",
475
+ "i",
476
+ "u",
477
+ "a",
478
+ "ul",
479
+ "ol",
480
+ "li",
481
+ "h1",
482
+ "h2",
483
+ "h3",
484
+ "h4",
485
+ "h5",
486
+ "h6",
487
+ "table",
488
+ "tr",
489
+ "td",
490
+ "th",
491
+ "img",
492
+ "pre",
493
+ "code"
494
+ ]);
495
+ function isHtml(content) {
496
+ let match;
497
+ HTML_TAG_REGEX.lastIndex = 0;
498
+ while ((match = HTML_TAG_REGEX.exec(content)) !== null) {
499
+ if (HTML_TAGS.has(match[1].toLowerCase())) {
500
+ return true;
501
+ }
502
+ }
503
+ return false;
504
+ }
505
+
506
+ // src/services/md-convert.ts
507
+ function htmlToMarkdown(html) {
508
+ return NodeHtmlMarkdown.translate(html);
509
+ }
510
+ function toMarkdown(content) {
511
+ if (isHtml(content)) {
512
+ return htmlToMarkdown(content);
513
+ }
514
+ return content;
515
+ }
516
+
517
+ // src/services/command-helpers.ts
518
+ function parseWorkItemId(idStr) {
519
+ const id = Number.parseInt(idStr, 10);
520
+ if (!Number.isInteger(id) || id <= 0) {
521
+ process.stderr.write(
522
+ `Error: Work item ID must be a positive integer. Got: "${idStr}"
523
+ `
524
+ );
525
+ process.exit(1);
526
+ }
527
+ return id;
528
+ }
529
+ function validateOrgProjectPair(options) {
530
+ const hasOrg = options.org !== void 0;
531
+ const hasProject = options.project !== void 0;
532
+ if (hasOrg !== hasProject) {
533
+ process.stderr.write(
534
+ "Error: --org and --project must both be provided, or both omitted.\n"
535
+ );
536
+ process.exit(1);
537
+ }
538
+ }
539
+ function validateSource(options) {
540
+ const hasContent = options.content !== void 0;
541
+ const hasFile = options.file !== void 0;
542
+ if (hasContent === hasFile) {
543
+ process.stderr.write("Error: provide exactly one of --content or --file\n");
544
+ process.exit(1);
545
+ }
546
+ }
547
+ function formatCreateError(err) {
548
+ const error = err instanceof Error ? err : new Error(String(err));
549
+ const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
550
+ const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
551
+ if (requiredMatches.length > 0) {
552
+ const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
553
+ return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
554
+ }
555
+ return `Create rejected: ${message}`;
556
+ }
557
+ function handleCommandError(err, id, context, scope = "write", exit = true) {
558
+ const error = err instanceof Error ? err : new Error(String(err));
559
+ const msg = error.message;
560
+ const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
561
+ if (msg === "AUTH_FAILED") {
562
+ process.stderr.write(
563
+ `Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
564
+ `
565
+ );
566
+ } else if (msg === "PERMISSION_DENIED") {
567
+ process.stderr.write(
568
+ `Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
569
+ `
570
+ );
571
+ } else if (msg === "NOT_FOUND") {
572
+ process.stderr.write(
573
+ `Error: Work item ${id} not found in ${context?.org}/${context?.project}.
574
+ `
575
+ );
576
+ } else if (msg === "NETWORK_ERROR") {
577
+ process.stderr.write(
578
+ "Error: Could not connect to Azure DevOps. Check your network connection.\n"
579
+ );
580
+ } else if (msg.startsWith("BAD_REQUEST:")) {
581
+ const serverMsg = msg.replace("BAD_REQUEST: ", "");
582
+ process.stderr.write(`Error: Request rejected: ${serverMsg}
583
+ `);
584
+ } else if (msg.startsWith("CREATE_REJECTED:")) {
585
+ process.stderr.write(`Error: ${formatCreateError(error)}
586
+ `);
587
+ } else if (msg.startsWith("UPDATE_REJECTED:")) {
588
+ const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
589
+ process.stderr.write(`Error: Update rejected: ${serverMsg}
590
+ `);
591
+ } else {
592
+ process.stderr.write(`Error: ${msg}
593
+ `);
594
+ }
595
+ if (exit) {
596
+ process.exit(1);
597
+ } else {
598
+ process.exitCode = 1;
599
+ }
600
+ }
601
+
602
+ // src/commands/get-item.ts
603
+ function parseRequestedFields(raw) {
604
+ if (raw === void 0) return void 0;
605
+ const source = Array.isArray(raw) ? raw : [raw];
606
+ const tokens = source.flatMap((entry) => entry.split(/[,\s]+/)).map((field) => field.trim()).filter((field) => field.length > 0);
607
+ if (tokens.length === 0) return void 0;
608
+ return Array.from(new Set(tokens));
609
+ }
610
+ function stripHtml(html) {
611
+ let text = html;
612
+ text = text.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
613
+ text = text.replace(/<br\s*\/?>/gi, "\n");
614
+ text = text.replace(/<\/?(p|div)>/gi, "\n");
615
+ text = text.replace(/<li>/gi, "\n");
616
+ text = text.replace(/<[^>]*>/g, "");
617
+ text = text.replace(/&amp;/g, "&");
618
+ text = text.replace(/&lt;/g, "<");
619
+ text = text.replace(/&gt;/g, ">");
620
+ text = text.replace(/&quot;/g, '"');
621
+ text = text.replace(/&#39;/g, "'");
622
+ text = text.replace(/&nbsp;/g, " ");
623
+ text = text.replace(/\n{3,}/g, "\n\n");
624
+ return text.trim();
625
+ }
626
+ function convertRichText(html, markdown) {
627
+ if (!html) return "";
628
+ return markdown ? toMarkdown(html) : stripHtml(html);
629
+ }
630
+ function formatExtraFields(extraFields, markdown) {
631
+ return Object.entries(extraFields).map(([refName, value]) => {
632
+ const fieldLabel = refName.includes(".") ? refName.split(".").pop() : refName;
633
+ const displayValue = markdown ? toMarkdown(value) : value;
634
+ return `${fieldLabel.padEnd(13)}${displayValue}`;
635
+ });
636
+ }
637
+ function summarizeDescription(text, label) {
638
+ const descLines = text.split("\n").filter((l) => l.trim() !== "");
639
+ const firstThree = descLines.slice(0, 3);
640
+ const suffix = descLines.length > 3 ? "\n..." : "";
641
+ return [`${label("Description:")}${firstThree.join("\n")}${suffix}`];
642
+ }
643
+ function formatWorkItem(workItem, short, markdown = false) {
644
+ const lines = [];
645
+ const label = (name) => name.padEnd(13);
646
+ lines.push(`${label("ID:")}${workItem.id}`);
647
+ lines.push(`${label("Type:")}${workItem.type}`);
648
+ lines.push(`${label("Title:")}${workItem.title}`);
649
+ lines.push(`${label("State:")}${workItem.state}`);
650
+ lines.push(`${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`);
651
+ if (!short) {
652
+ lines.push(`${label("Area:")}${workItem.areaPath}`);
653
+ lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
654
+ }
655
+ lines.push(`${label("URL:")}${workItem.url}`);
656
+ if (workItem.extraFields) {
657
+ lines.push(...formatExtraFields(workItem.extraFields, markdown));
658
+ }
659
+ lines.push("");
660
+ const descriptionText = convertRichText(workItem.description, markdown);
661
+ if (short) {
662
+ lines.push(...summarizeDescription(descriptionText, label));
663
+ } else {
664
+ lines.push("Description:");
665
+ lines.push(descriptionText);
666
+ }
667
+ return lines.join("\n");
668
+ }
669
+ function createGetItemCommand() {
670
+ const command = new Command("get-item");
671
+ 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").option("--markdown", "convert rich text fields to markdown").action(
672
+ async (idStr, options) => {
673
+ const id = parseWorkItemId(idStr);
674
+ validateOrgProjectPair(options);
675
+ let context;
676
+ try {
677
+ context = resolveContext(options);
678
+ const credential = await resolvePat();
679
+ const fieldsList = options.fields !== void 0 ? parseRequestedFields(options.fields) : parseRequestedFields(loadConfig().fields);
680
+ const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
681
+ const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
682
+ const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
683
+ process.stdout.write(output + "\n");
684
+ } catch (err) {
685
+ handleCommandError(err, id, context, "read", false);
686
+ }
687
+ }
688
+ );
689
+ return command;
690
+ }
691
+
692
+ // src/commands/clear-pat.ts
693
+ import { Command as Command2 } from "commander";
694
+ function createClearPatCommand() {
695
+ const command = new Command2("clear-pat");
696
+ command.description("Remove the stored Azure DevOps PAT from the credential store").action(async () => {
697
+ const deleted = await deletePat();
698
+ if (deleted) {
699
+ process.stdout.write("PAT removed from credential store.\n");
700
+ } else {
701
+ process.stdout.write("No stored PAT found.\n");
702
+ }
703
+ });
704
+ return command;
705
+ }
706
+
707
+ // src/commands/config.ts
708
+ import { Command as Command3 } from "commander";
709
+ import { createInterface as createInterface2 } from "readline";
710
+ function createConfigCommand() {
711
+ const config = new Command3("config");
712
+ config.description("Manage CLI settings");
713
+ const set = new Command3("set");
714
+ 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) => {
715
+ try {
716
+ setConfigValue(key, value);
717
+ if (options.json) {
718
+ const output = { key, value };
719
+ if (key === "fields") {
720
+ output.value = value.split(",").map((s) => s.trim());
721
+ }
722
+ process.stdout.write(JSON.stringify(output) + "\n");
723
+ } else {
724
+ process.stdout.write(`Set "${key}" to "${value}"
725
+ `);
726
+ }
727
+ } catch (err) {
728
+ const message = err instanceof Error ? err.message : String(err);
729
+ process.stderr.write(`Error: ${message}
730
+ `);
731
+ process.exit(1);
732
+ }
733
+ });
734
+ const get = new Command3("get");
735
+ get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
736
+ try {
737
+ const value = getConfigValue(key);
738
+ if (options.json) {
739
+ process.stdout.write(
740
+ JSON.stringify({ key, value: value ?? null }) + "\n"
741
+ );
742
+ } else if (value === void 0) {
743
+ process.stdout.write(`Setting "${key}" is not configured.
744
+ `);
745
+ } else if (Array.isArray(value)) {
746
+ process.stdout.write(value.join(",") + "\n");
747
+ } else {
748
+ process.stdout.write(value + "\n");
749
+ }
750
+ } catch (err) {
751
+ const message = err instanceof Error ? err.message : String(err);
752
+ process.stderr.write(`Error: ${message}
753
+ `);
754
+ process.exit(1);
755
+ }
756
+ });
757
+ const list = new Command3("list");
758
+ list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
759
+ const cfg = loadConfig();
760
+ if (options.json) {
761
+ process.stdout.write(JSON.stringify(cfg) + "\n");
762
+ } else {
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
+ }
782
+ }
783
+ });
784
+ const unset = new Command3("unset");
785
+ unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
786
+ try {
787
+ unsetConfigValue(key);
788
+ if (options.json) {
789
+ process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
790
+ } else {
791
+ process.stdout.write(`Unset "${key}"
792
+ `);
793
+ }
794
+ } catch (err) {
795
+ const message = err instanceof Error ? err.message : String(err);
796
+ process.stderr.write(`Error: ${message}
797
+ `);
798
+ process.exit(1);
799
+ }
800
+ });
801
+ const wizard = new Command3("wizard");
802
+ wizard.description("Interactive wizard to configure all settings").action(async () => {
803
+ if (!process.stdin.isTTY) {
804
+ process.stderr.write(
805
+ "Error: Wizard requires an interactive terminal.\n"
806
+ );
807
+ process.exit(1);
808
+ }
809
+ const cfg = loadConfig();
810
+ const rl = createInterface2({
811
+ input: process.stdin,
812
+ output: process.stderr
813
+ });
814
+ const ask = (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
815
+ process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
816
+ process.stderr.write("=======================================\n\n");
817
+ for (const setting of SETTINGS) {
818
+ const current = cfg[setting.key];
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
+ }
844
+ }
845
+ rl.close();
846
+ process.stderr.write("Configuration complete!\n");
847
+ });
848
+ config.addCommand(set);
849
+ config.addCommand(get);
850
+ config.addCommand(list);
851
+ config.addCommand(unset);
852
+ config.addCommand(wizard);
853
+ return config;
854
+ }
855
+
856
+ // src/commands/set-state.ts
857
+ import { Command as Command4 } from "commander";
858
+ function createSetStateCommand() {
859
+ const command = new Command4("set-state");
860
+ 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(
861
+ async (idStr, state, options) => {
862
+ const id = parseWorkItemId(idStr);
863
+ validateOrgProjectPair(options);
864
+ let context;
865
+ try {
866
+ context = resolveContext(options);
867
+ const credential = await resolvePat();
868
+ const operations = [
869
+ { op: "add", path: "/fields/System.State", value: state }
870
+ ];
871
+ const result = await updateWorkItem(context, id, credential.pat, "System.State", operations);
872
+ if (options.json) {
873
+ process.stdout.write(
874
+ JSON.stringify({
875
+ id: result.id,
876
+ rev: result.rev,
877
+ title: result.title,
878
+ field: result.fieldName,
879
+ value: result.fieldValue
880
+ }) + "\n"
881
+ );
882
+ } else {
883
+ process.stdout.write(`Updated work item ${result.id}: State -> ${state}
884
+ `);
885
+ }
886
+ } catch (err) {
887
+ handleCommandError(err, id, context, "write");
888
+ }
889
+ }
890
+ );
891
+ return command;
892
+ }
893
+
894
+ // src/commands/assign.ts
895
+ import { Command as Command5 } from "commander";
896
+ function createAssignCommand() {
897
+ const command = new Command5("assign");
898
+ 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(
899
+ async (idStr, name, options) => {
900
+ const id = parseWorkItemId(idStr);
901
+ if (!name && !options.unassign) {
902
+ process.stderr.write(
903
+ "Error: Either provide a user name or use --unassign.\n"
904
+ );
905
+ process.exit(1);
906
+ }
907
+ if (name && options.unassign) {
908
+ process.stderr.write(
909
+ "Error: Cannot provide both a user name and --unassign.\n"
910
+ );
911
+ process.exit(1);
912
+ }
913
+ validateOrgProjectPair(options);
914
+ let context;
915
+ try {
916
+ context = resolveContext(options);
917
+ const credential = await resolvePat();
918
+ const value = options.unassign ? "" : name;
919
+ const operations = [
920
+ { op: "add", path: "/fields/System.AssignedTo", value }
921
+ ];
922
+ const result = await updateWorkItem(context, id, credential.pat, "System.AssignedTo", 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
+ const displayValue = options.unassign ? "(unassigned)" : name;
935
+ process.stdout.write(`Updated work item ${result.id}: Assigned To -> ${displayValue}
936
+ `);
937
+ }
938
+ } catch (err) {
939
+ handleCommandError(err, id, context, "write");
940
+ }
941
+ }
942
+ );
943
+ return command;
944
+ }
945
+
946
+ // src/commands/set-field.ts
947
+ import { Command as Command6 } from "commander";
948
+ function createSetFieldCommand() {
949
+ const command = new Command6("set-field");
950
+ 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(
951
+ async (idStr, field, value, options) => {
952
+ const id = parseWorkItemId(idStr);
953
+ validateOrgProjectPair(options);
954
+ let context;
955
+ try {
956
+ context = resolveContext(options);
957
+ const credential = await resolvePat();
958
+ const operations = [
959
+ { op: "add", path: `/fields/${field}`, value }
960
+ ];
961
+ const result = await updateWorkItem(context, id, credential.pat, field, operations);
962
+ if (options.json) {
963
+ process.stdout.write(
964
+ JSON.stringify({
965
+ id: result.id,
966
+ rev: result.rev,
967
+ title: result.title,
968
+ field: result.fieldName,
969
+ value: result.fieldValue
970
+ }) + "\n"
971
+ );
972
+ } else {
973
+ process.stdout.write(`Updated work item ${result.id}: ${field} -> ${value}
974
+ `);
975
+ }
976
+ } catch (err) {
977
+ handleCommandError(err, id, context, "write");
978
+ }
979
+ }
980
+ );
981
+ return command;
982
+ }
983
+
984
+ // src/commands/get-md-field.ts
985
+ import { Command as Command7 } from "commander";
986
+ function createGetMdFieldCommand() {
987
+ const command = new Command7("get-md-field");
988
+ command.description("Get a work item field value, converting HTML to markdown").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(
989
+ async (idStr, field, options) => {
990
+ const id = parseWorkItemId(idStr);
991
+ validateOrgProjectPair(options);
992
+ let context;
993
+ try {
994
+ context = resolveContext(options);
995
+ const credential = await resolvePat();
996
+ const value = await getWorkItemFieldValue(context, id, credential.pat, field);
997
+ if (value === null) {
998
+ process.stdout.write("\n");
999
+ } else {
1000
+ process.stdout.write(toMarkdown(value) + "\n");
1001
+ }
1002
+ } catch (err) {
1003
+ handleCommandError(err, id, context, "read");
1004
+ }
1005
+ }
1006
+ );
1007
+ return command;
1008
+ }
1009
+
1010
+ // src/commands/set-md-field.ts
1011
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1012
+ import { Command as Command8 } from "commander";
1013
+ function fail(message) {
1014
+ process.stderr.write(`Error: ${message}
1015
+ `);
1016
+ process.exit(1);
1017
+ }
1018
+ function resolveContent(inlineContent, options) {
1019
+ if (inlineContent && options.file) {
1020
+ fail("Cannot specify both inline content and --file.");
1021
+ }
1022
+ if (options.file) {
1023
+ return readFileContent(options.file);
1024
+ }
1025
+ if (inlineContent) {
1026
+ return inlineContent;
1027
+ }
1028
+ return null;
1029
+ }
1030
+ function readFileContent(filePath) {
1031
+ if (!existsSync(filePath)) {
1032
+ fail(`File not found: ${filePath}`);
1033
+ }
1034
+ try {
1035
+ return readFileSync2(filePath, "utf-8");
1036
+ } catch {
1037
+ fail(`Cannot read file: ${filePath}`);
1038
+ }
1039
+ }
1040
+ async function readStdinContent() {
1041
+ if (process.stdin.isTTY) {
1042
+ fail(
1043
+ "No content provided. Pass markdown content as the third argument, use --file, or pipe via stdin."
1044
+ );
1045
+ }
1046
+ const chunks = [];
1047
+ for await (const chunk of process.stdin) {
1048
+ chunks.push(chunk);
1049
+ }
1050
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trimEnd();
1051
+ if (!stdinContent) {
1052
+ fail(
1053
+ "No content provided via stdin. Pipe markdown content or use inline content or --file."
1054
+ );
1055
+ }
1056
+ return stdinContent;
1057
+ }
1058
+ function formatOutput(result, options, field) {
1059
+ if (options.json) {
1060
+ process.stdout.write(
1061
+ JSON.stringify({
1062
+ id: result.id,
1063
+ rev: result.rev,
1064
+ field: result.fieldName,
1065
+ value: result.fieldValue
1066
+ }) + "\n"
1067
+ );
1068
+ } else {
1069
+ process.stdout.write(`Updated work item ${result.id}: ${field} set with markdown content
1070
+ `);
1071
+ }
1072
+ }
1073
+ function createSetMdFieldCommand() {
1074
+ const command = new Command8("set-md-field");
1075
+ command.description("Set a work item field with markdown content").argument("<id>", "work item ID").argument("<field>", "field reference name (e.g., System.Description)").argument("[content]", "markdown content to set").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--json", "output result as JSON").option("--file <path>", "read markdown content from file").action(
1076
+ async (idStr, field, inlineContent, options) => {
1077
+ const id = parseWorkItemId(idStr);
1078
+ validateOrgProjectPair(options);
1079
+ const content = resolveContent(inlineContent, options) ?? await readStdinContent();
1080
+ let context;
1081
+ try {
1082
+ context = resolveContext(options);
1083
+ const credential = await resolvePat();
1084
+ const operations = [
1085
+ { op: "add", path: `/fields/${field}`, value: content },
1086
+ { op: "add", path: `/multilineFieldsFormat/${field}`, value: "Markdown" }
1087
+ ];
1088
+ const result = await updateWorkItem(context, id, credential.pat, field, operations);
1089
+ formatOutput(result, options, field);
1090
+ } catch (err) {
1091
+ handleCommandError(err, id, context, "write");
1092
+ }
1093
+ }
1094
+ );
1095
+ return command;
1096
+ }
1097
+
1098
+ // src/commands/upsert.ts
1099
+ import { existsSync as existsSync2, readFileSync as readFileSync3, unlinkSync } from "fs";
1100
+ import { Command as Command9 } from "commander";
1101
+
1102
+ // src/services/task-document.ts
1103
+ var FIELD_ALIASES = /* @__PURE__ */ new Map([
1104
+ ["title", "System.Title"],
1105
+ ["assignedto", "System.AssignedTo"],
1106
+ ["assigned to", "System.AssignedTo"],
1107
+ ["state", "System.State"],
1108
+ ["description", "System.Description"],
1109
+ ["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
1110
+ ["acceptance criteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
1111
+ ["tags", "System.Tags"],
1112
+ ["priority", "Microsoft.VSTS.Common.Priority"]
1113
+ ]);
1114
+ var RICH_TEXT_FIELDS = /* @__PURE__ */ new Set([
1115
+ "System.Description",
1116
+ "Microsoft.VSTS.Common.AcceptanceCriteria"
1117
+ ]);
1118
+ var REFERENCE_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$/;
1119
+ function normalizeAlias(name) {
1120
+ return name.trim().replaceAll(/\s+/g, " ").toLowerCase();
1121
+ }
1122
+ function parseScalarValue(rawValue, fieldName) {
1123
+ if (rawValue === void 0) {
1124
+ throw new Error(`Malformed YAML front matter: missing value for "${fieldName}"`);
1125
+ }
1126
+ const trimmed = rawValue.trim();
1127
+ if (trimmed === "" || trimmed === "null" || trimmed === "~") {
1128
+ return null;
1129
+ }
1130
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1131
+ return trimmed.slice(1, -1);
1132
+ }
1133
+ if (/^[[{]|^[>|]-?$/.test(trimmed)) {
1134
+ throw new Error(`Malformed YAML front matter: unsupported value for "${fieldName}"`);
1135
+ }
1136
+ return trimmed;
1137
+ }
1138
+ function parseFrontMatter(content) {
1139
+ if (!content.startsWith("---")) {
1140
+ return { frontMatter: "", remainder: content };
1141
+ }
1142
+ const frontMatterPattern = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
1143
+ const match = frontMatterPattern.exec(content);
1144
+ if (!match) {
1145
+ throw new Error('Malformed YAML front matter: missing closing "---"');
1146
+ }
1147
+ return {
1148
+ frontMatter: match[1],
1149
+ remainder: content.slice(match[0].length)
1150
+ };
1151
+ }
1152
+ function assertKnownField(name, kind) {
1153
+ const resolved = resolveFieldName(name);
1154
+ if (!resolved) {
1155
+ const prefix = kind === "rich-text" ? "Unknown rich-text field" : "Unknown field";
1156
+ throw new Error(`${prefix}: ${name}`);
1157
+ }
1158
+ if (kind === "rich-text" && !RICH_TEXT_FIELDS.has(resolved)) {
1159
+ throw new Error(`Unknown rich-text field: ${name}`);
1160
+ }
1161
+ return resolved;
1162
+ }
1163
+ function pushField(fields, seen, refName, value, kind) {
1164
+ if (seen.has(refName)) {
1165
+ throw new Error(`Duplicate field: ${refName}`);
1166
+ }
1167
+ seen.add(refName);
1168
+ fields.push({
1169
+ refName,
1170
+ value,
1171
+ op: value === null ? "clear" : "set",
1172
+ kind
1173
+ });
1174
+ }
1175
+ function parseScalarFields(frontMatter, fields, seen) {
1176
+ if (frontMatter.trim() === "") {
1177
+ return;
1178
+ }
1179
+ for (const rawLine of frontMatter.split(/\r?\n/)) {
1180
+ const line = rawLine.trim();
1181
+ if (line === "") {
1182
+ continue;
1183
+ }
1184
+ const separatorIndex = rawLine.indexOf(":");
1185
+ if (separatorIndex <= 0) {
1186
+ throw new Error(`Malformed YAML front matter: ${rawLine.trim()}`);
1187
+ }
1188
+ const rawName = rawLine.slice(0, separatorIndex).trim();
1189
+ const rawValue = rawLine.slice(separatorIndex + 1);
1190
+ const refName = assertKnownField(rawName, "scalar");
1191
+ const value = parseScalarValue(rawValue, rawName);
1192
+ pushField(fields, seen, refName, value, "scalar");
1193
+ }
1194
+ }
1195
+ function parseRichTextSections(content, fields, seen) {
1196
+ const normalizedContent = content.replaceAll("\r\n", "\n");
1197
+ const lines = normalizedContent.split("\n");
1198
+ const headings = [];
1199
+ for (let index = 0; index < lines.length; index += 1) {
1200
+ const line = lines[index];
1201
+ if (!line.startsWith("##")) {
1202
+ continue;
1203
+ }
1204
+ const headingBody = line.slice(2);
1205
+ if (headingBody.trim() === "" || !headingBody.startsWith(" ") && !headingBody.startsWith(" ")) {
1206
+ continue;
1207
+ }
1208
+ headings.push({
1209
+ lineIndex: index,
1210
+ rawName: headingBody.trim()
1211
+ });
1212
+ }
1213
+ if (headings.length === 0) {
1214
+ return;
1215
+ }
1216
+ for (let index = 0; index < headings[0].lineIndex; index += 1) {
1217
+ if (lines[index].trim() !== "") {
1218
+ throw new Error("Unexpected content before the first markdown heading section");
1219
+ }
1220
+ }
1221
+ for (let index = 0; index < headings.length; index += 1) {
1222
+ const { lineIndex, rawName } = headings[index];
1223
+ const refName = assertKnownField(rawName, "rich-text");
1224
+ const bodyStart = lineIndex + 1;
1225
+ const bodyEnd = index + 1 < headings.length ? headings[index + 1].lineIndex : lines.length;
1226
+ const rawBody = lines.slice(bodyStart, bodyEnd).join("\n");
1227
+ const value = rawBody.trim() === "" ? null : rawBody.trimEnd();
1228
+ pushField(fields, seen, refName, value, "rich-text");
1229
+ }
1230
+ }
1231
+ function resolveFieldName(name) {
1232
+ const trimmed = name.trim();
1233
+ if (trimmed === "") {
1234
+ return null;
1235
+ }
1236
+ const alias = FIELD_ALIASES.get(normalizeAlias(trimmed));
1237
+ if (alias) {
1238
+ return alias;
1239
+ }
1240
+ return REFERENCE_NAME_PATTERN.test(trimmed) ? trimmed : null;
1241
+ }
1242
+ function parseTaskDocument(content) {
1243
+ const { frontMatter, remainder } = parseFrontMatter(content);
1244
+ const fields = [];
1245
+ const seen = /* @__PURE__ */ new Set();
1246
+ parseScalarFields(frontMatter, fields, seen);
1247
+ parseRichTextSections(remainder, fields, seen);
1248
+ return { fields };
1249
+ }
1250
+
1251
+ // src/commands/upsert.ts
1252
+ function fail2(message) {
1253
+ process.stderr.write(`Error: ${message}
1254
+ `);
1255
+ process.exit(1);
1256
+ }
1257
+ function loadSourceContent(options) {
1258
+ validateSource(options);
1259
+ if (options.content !== void 0) {
1260
+ return { content: options.content };
1261
+ }
1262
+ const filePath = options.file;
1263
+ if (!existsSync2(filePath)) {
1264
+ fail2(`File not found: ${filePath}`);
1265
+ }
1266
+ try {
1267
+ return {
1268
+ content: readFileSync3(filePath, "utf-8"),
1269
+ sourceFile: filePath
1270
+ };
1271
+ } catch {
1272
+ fail2(`Cannot read file: ${filePath}`);
1273
+ }
1274
+ }
1275
+ function toPatchOperations(fields, action) {
1276
+ const operations = [];
1277
+ for (const field of fields) {
1278
+ if (field.op === "clear") {
1279
+ if (action === "updated") {
1280
+ operations.push({ op: "remove", path: `/fields/${field.refName}` });
1281
+ }
1282
+ continue;
1283
+ }
1284
+ operations.push({ op: "add", path: `/fields/${field.refName}`, value: field.value ?? "" });
1285
+ if (field.kind === "rich-text") {
1286
+ operations.push({
1287
+ op: "add",
1288
+ path: `/multilineFieldsFormat/${field.refName}`,
1289
+ value: "Markdown"
1290
+ });
1291
+ }
1292
+ }
1293
+ return operations;
1294
+ }
1295
+ function buildAppliedFields(fields) {
1296
+ const applied = {};
1297
+ for (const field of fields) {
1298
+ applied[field.refName] = field.value;
1299
+ }
1300
+ return applied;
1301
+ }
1302
+ function ensureTitleForCreate(fields) {
1303
+ const titleField = fields.find((field) => field.refName === "System.Title");
1304
+ if (!titleField || titleField.op === "clear" || titleField.value === null || titleField.value.trim() === "") {
1305
+ fail2("Title is required when creating a task.");
1306
+ }
1307
+ }
1308
+ function writeSuccess(result, options) {
1309
+ if (options.json) {
1310
+ process.stdout.write(`${JSON.stringify(result)}
1311
+ `);
1312
+ return;
1313
+ }
1314
+ const verb = result.action === "created" ? "Created" : "Updated";
1315
+ const fields = Object.keys(result.fields).join(", ");
1316
+ const suffix = fields ? ` (${fields})` : "";
1317
+ process.stdout.write(`${verb} task #${result.id}${suffix}
1318
+ `);
1319
+ }
1320
+ function cleanupSourceFile(sourceFile) {
1321
+ if (!sourceFile) {
1322
+ return;
1323
+ }
1324
+ try {
1325
+ unlinkSync(sourceFile);
1326
+ } catch {
1327
+ process.stderr.write(`Warning: upsert succeeded but could not delete source file: ${sourceFile}
1328
+ `);
1329
+ }
1330
+ }
1331
+ function buildUpsertResult(action, writeResult, fields) {
1332
+ const appliedFields = buildAppliedFields(fields);
1333
+ return {
1334
+ action,
1335
+ id: writeResult.id,
1336
+ fields: appliedFields
1337
+ };
1338
+ }
1339
+ function isUpdateWriteError(err) {
1340
+ return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NOT_FOUND" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
1341
+ }
1342
+ function isCreateWriteError(err) {
1343
+ return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
1344
+ }
1345
+ function handleUpsertError(err, id, context) {
1346
+ if (!(err instanceof Error)) {
1347
+ process.stderr.write(`Error: ${String(err)}
1348
+ `);
1349
+ process.exit(1);
1350
+ }
1351
+ if (id === void 0 && err.message.startsWith("CREATE_REJECTED:")) {
1352
+ process.stderr.write(`Error: ${formatCreateError(err)}
1353
+ `);
1354
+ process.exit(1);
1355
+ }
1356
+ if (id !== void 0 && isUpdateWriteError(err)) {
1357
+ handleCommandError(err, id, context, "write");
1358
+ return;
1359
+ }
1360
+ if (id === void 0 && isCreateWriteError(err)) {
1361
+ handleCommandError(err, 0, context, "write");
1362
+ return;
1363
+ }
1364
+ process.stderr.write(`Error: ${err.message}
1365
+ `);
1366
+ process.exit(1);
1367
+ }
1368
+ function createUpsertCommand() {
1369
+ const command = new Command9("upsert");
1370
+ command.description("Create or update a Task from a markdown document").argument("[id]", "work item ID to update; omit to create a new Task").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
1371
+ validateOrgProjectPair(options);
1372
+ const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
1373
+ const { content, sourceFile } = loadSourceContent(options);
1374
+ let context;
1375
+ try {
1376
+ context = resolveContext(options);
1377
+ const document = parseTaskDocument(content);
1378
+ const action = id === void 0 ? "created" : "updated";
1379
+ if (action === "created") {
1380
+ ensureTitleForCreate(document.fields);
1381
+ }
1382
+ const operations = toPatchOperations(document.fields, action);
1383
+ const credential = await resolvePat();
1384
+ let writeResult;
1385
+ if (action === "created") {
1386
+ writeResult = await createWorkItem(context, "Task", credential.pat, operations);
1387
+ } else {
1388
+ if (id === void 0) {
1389
+ fail2("Work item ID is required for updates.");
1390
+ }
1391
+ writeResult = await applyWorkItemPatch(context, id, credential.pat, operations);
1392
+ }
1393
+ const result = buildUpsertResult(action, writeResult, document.fields);
1394
+ writeSuccess(result, options);
1395
+ cleanupSourceFile(sourceFile);
1396
+ } catch (err) {
1397
+ handleUpsertError(err, id, context);
1398
+ }
1399
+ });
1400
+ return command;
1401
+ }
1402
+
14
1403
  // src/index.ts
15
- var program = new Command();
1404
+ var program = new Command10();
16
1405
  program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
1406
+ program.addCommand(createGetItemCommand());
1407
+ program.addCommand(createClearPatCommand());
1408
+ program.addCommand(createConfigCommand());
1409
+ program.addCommand(createSetStateCommand());
1410
+ program.addCommand(createAssignCommand());
1411
+ program.addCommand(createSetFieldCommand());
1412
+ program.addCommand(createGetMdFieldCommand());
1413
+ program.addCommand(createSetMdFieldCommand());
1414
+ program.addCommand(createUpsertCommand());
17
1415
  program.showHelpAfterError();
18
1416
  program.parse();
19
1417
  if (process.argv.length <= 2) {