azdo-cli 0.8.0-develop.176 → 0.9.0-develop.188
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 +128 -11
- package/package.json +3 -2
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 Command14 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -166,17 +166,33 @@ async function getWorkItemFields(context, id, pat) {
|
|
|
166
166
|
const data = await response.json();
|
|
167
167
|
return data.fields;
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
function extractAttachments(relations) {
|
|
170
|
+
if (!relations) return null;
|
|
171
|
+
const attachments = relations.filter((r) => r.rel === "AttachedFile").map((r) => ({
|
|
172
|
+
name: r.attributes.name ?? "unknown",
|
|
173
|
+
size: r.attributes.resourceSize ?? 0,
|
|
174
|
+
url: r.url
|
|
175
|
+
}));
|
|
176
|
+
return attachments.length > 0 ? attachments : null;
|
|
177
|
+
}
|
|
178
|
+
function buildWorkItemUrl(context, id, options = {}) {
|
|
170
179
|
const url = new URL(
|
|
171
180
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
172
181
|
);
|
|
173
182
|
url.searchParams.set("api-version", "7.1");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const allFields = normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields]);
|
|
177
|
-
url.searchParams.set("fields", allFields.join(","));
|
|
183
|
+
if (options.includeRelations) {
|
|
184
|
+
url.searchParams.set("$expand", "relations");
|
|
178
185
|
}
|
|
179
|
-
|
|
186
|
+
if (options.fields && options.fields.length > 0) {
|
|
187
|
+
url.searchParams.set("fields", options.fields.join(","));
|
|
188
|
+
}
|
|
189
|
+
return url;
|
|
190
|
+
}
|
|
191
|
+
async function fetchWorkItemResponse(context, id, pat, options = {}) {
|
|
192
|
+
const response = await fetchWithErrors(
|
|
193
|
+
buildWorkItemUrl(context, id, options).toString(),
|
|
194
|
+
{ headers: authHeaders(pat) }
|
|
195
|
+
);
|
|
180
196
|
if (response.status === 400) {
|
|
181
197
|
const serverMessage = await readResponseMessage(response);
|
|
182
198
|
if (serverMessage) {
|
|
@@ -186,7 +202,14 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
186
202
|
if (!response.ok) {
|
|
187
203
|
throw new Error(`HTTP_${response.status}`);
|
|
188
204
|
}
|
|
189
|
-
|
|
205
|
+
return await response.json();
|
|
206
|
+
}
|
|
207
|
+
async function getWorkItem(context, id, pat, extraFields) {
|
|
208
|
+
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
209
|
+
const data = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, pat, {
|
|
210
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields])
|
|
211
|
+
}) : await fetchWorkItemResponse(context, id, pat, { includeRelations: true });
|
|
212
|
+
const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, pat, { includeRelations: true }) : data;
|
|
190
213
|
const descriptionParts = [];
|
|
191
214
|
if (data.fields["System.Description"]) {
|
|
192
215
|
descriptionParts.push({ label: "Description", value: data.fields["System.Description"] });
|
|
@@ -214,7 +237,8 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
214
237
|
areaPath: data.fields["System.AreaPath"],
|
|
215
238
|
iterationPath: data.fields["System.IterationPath"],
|
|
216
239
|
url: data._links.html.href,
|
|
217
|
-
extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null
|
|
240
|
+
extraFields: normalizedExtraFields.length > 0 ? buildExtraFields(data.fields, normalizedExtraFields) : null,
|
|
241
|
+
attachments: extractAttachments(relationsData.relations)
|
|
218
242
|
};
|
|
219
243
|
}
|
|
220
244
|
async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
@@ -328,6 +352,13 @@ async function applyWorkItemPatch(context, id, pat, operations) {
|
|
|
328
352
|
});
|
|
329
353
|
return readWriteResponse(response, "UPDATE_REJECTED");
|
|
330
354
|
}
|
|
355
|
+
async function downloadAttachment(url, pat) {
|
|
356
|
+
const response = await fetchWithErrors(url, { headers: authHeaders(pat) });
|
|
357
|
+
if (!response.ok) {
|
|
358
|
+
throw new Error(`HTTP_${response.status}`);
|
|
359
|
+
}
|
|
360
|
+
return response.arrayBuffer();
|
|
361
|
+
}
|
|
331
362
|
|
|
332
363
|
// src/services/auth.ts
|
|
333
364
|
import { createInterface } from "readline";
|
|
@@ -816,7 +847,7 @@ function stripHtml(html) {
|
|
|
816
847
|
text = text.replaceAll(/<br\s*\/?>/gi, "\n");
|
|
817
848
|
text = text.replaceAll(/<\/?(p|div)>/gi, "\n");
|
|
818
849
|
text = text.replaceAll(/<li>/gi, "\n");
|
|
819
|
-
text = text
|
|
850
|
+
text = removeHtmlTags(text);
|
|
820
851
|
text = text.replaceAll("&", "&");
|
|
821
852
|
text = text.replaceAll("<", "<");
|
|
822
853
|
text = text.replaceAll(">", ">");
|
|
@@ -826,6 +857,24 @@ function stripHtml(html) {
|
|
|
826
857
|
text = text.replaceAll(/\n{3,}/g, "\n\n");
|
|
827
858
|
return text.trim();
|
|
828
859
|
}
|
|
860
|
+
function removeHtmlTags(value) {
|
|
861
|
+
let result = "";
|
|
862
|
+
let insideTag = false;
|
|
863
|
+
for (const char of value) {
|
|
864
|
+
if (char === "<") {
|
|
865
|
+
insideTag = true;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (char === ">" && insideTag) {
|
|
869
|
+
insideTag = false;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (!insideTag) {
|
|
873
|
+
result += char;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}
|
|
829
878
|
function convertRichText(html, markdown) {
|
|
830
879
|
if (!html) return "";
|
|
831
880
|
return markdown ? toMarkdown(html) : stripHtml(html);
|
|
@@ -857,6 +906,23 @@ function summarizeDescription(text, label, markdown) {
|
|
|
857
906
|
}
|
|
858
907
|
return [`${label("Description:")}${content}`];
|
|
859
908
|
}
|
|
909
|
+
function formatFileSize(bytes) {
|
|
910
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
911
|
+
const kb = bytes / 1024;
|
|
912
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
913
|
+
const mb = kb / 1024;
|
|
914
|
+
return `${mb.toFixed(1)} MB`;
|
|
915
|
+
}
|
|
916
|
+
function formatAttachments(attachments, short) {
|
|
917
|
+
if (short) {
|
|
918
|
+
return [`Attachments: ${attachments.length}`];
|
|
919
|
+
}
|
|
920
|
+
const lines = ["", "Attachments:"];
|
|
921
|
+
for (const att of attachments) {
|
|
922
|
+
lines.push(` ${att.name} (${formatFileSize(att.size)})`);
|
|
923
|
+
}
|
|
924
|
+
return lines;
|
|
925
|
+
}
|
|
860
926
|
function formatWorkItem(workItem, short, markdown = false) {
|
|
861
927
|
const label = (name) => name.padEnd(13);
|
|
862
928
|
const lines = [
|
|
@@ -883,6 +949,9 @@ function formatWorkItem(workItem, short, markdown = false) {
|
|
|
883
949
|
} else {
|
|
884
950
|
lines.push("Description:", descriptionText);
|
|
885
951
|
}
|
|
952
|
+
if (workItem.attachments) {
|
|
953
|
+
lines.push(...formatAttachments(workItem.attachments, short));
|
|
954
|
+
}
|
|
886
955
|
return lines.join("\n");
|
|
887
956
|
}
|
|
888
957
|
function createGetItemCommand() {
|
|
@@ -2171,8 +2240,55 @@ function createCommentsCommand() {
|
|
|
2171
2240
|
return command;
|
|
2172
2241
|
}
|
|
2173
2242
|
|
|
2243
|
+
// src/commands/download-attachment.ts
|
|
2244
|
+
import { Command as Command13 } from "commander";
|
|
2245
|
+
import { writeFile } from "fs/promises";
|
|
2246
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2247
|
+
import { join as join2 } from "path";
|
|
2248
|
+
function createDownloadAttachmentCommand() {
|
|
2249
|
+
const command = new Command13("download-attachment");
|
|
2250
|
+
command.description("Download an attachment from an Azure DevOps work item").argument("<id>", "work item ID").argument("<filename>", "name of the attachment to download").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--output <dir>", "target directory for the downloaded file").action(
|
|
2251
|
+
async (idStr, filename, options) => {
|
|
2252
|
+
const id = parseWorkItemId(idStr);
|
|
2253
|
+
validateOrgProjectPair(options);
|
|
2254
|
+
let context;
|
|
2255
|
+
try {
|
|
2256
|
+
context = resolveContext(options);
|
|
2257
|
+
const credential = await resolvePat();
|
|
2258
|
+
const outputDir = options.output ?? ".";
|
|
2259
|
+
if (!existsSync4(outputDir)) {
|
|
2260
|
+
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
2261
|
+
`);
|
|
2262
|
+
process.exit(1);
|
|
2263
|
+
}
|
|
2264
|
+
const workItem = await getWorkItem(context, id, credential.pat);
|
|
2265
|
+
const attachment = workItem.attachments?.find(
|
|
2266
|
+
(a) => a.name === filename
|
|
2267
|
+
);
|
|
2268
|
+
if (!attachment) {
|
|
2269
|
+
process.stderr.write(
|
|
2270
|
+
`Error: Attachment "${filename}" not found on work item ${id}.
|
|
2271
|
+
`
|
|
2272
|
+
);
|
|
2273
|
+
process.exit(1);
|
|
2274
|
+
}
|
|
2275
|
+
const data = await downloadAttachment(attachment.url, credential.pat);
|
|
2276
|
+
const outputPath = join2(outputDir, filename);
|
|
2277
|
+
await writeFile(outputPath, Buffer.from(data));
|
|
2278
|
+
process.stdout.write(
|
|
2279
|
+
`Downloaded "${filename}" (${formatFileSize(attachment.size)}) to ${outputPath}
|
|
2280
|
+
`
|
|
2281
|
+
);
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
handleCommandError(err, id, context, "read", false);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
);
|
|
2287
|
+
return command;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2174
2290
|
// src/index.ts
|
|
2175
|
-
var program = new
|
|
2291
|
+
var program = new Command14();
|
|
2176
2292
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
2177
2293
|
program.addCommand(createGetItemCommand());
|
|
2178
2294
|
program.addCommand(createClearPatCommand());
|
|
@@ -2186,6 +2302,7 @@ program.addCommand(createUpsertCommand());
|
|
|
2186
2302
|
program.addCommand(createListFieldsCommand());
|
|
2187
2303
|
program.addCommand(createPrCommand());
|
|
2188
2304
|
program.addCommand(createCommentsCommand());
|
|
2305
|
+
program.addCommand(createDownloadAttachmentCommand());
|
|
2189
2306
|
program.showHelpAfterError();
|
|
2190
2307
|
program.parse();
|
|
2191
2308
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-develop.188",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"lint": "eslint src/",
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"format": "prettier --check src/",
|
|
17
|
-
"test": "npm run build && vitest run tests/unit",
|
|
17
|
+
"test": "npm run build && vitest run tests/unit tests/integration",
|
|
18
|
+
"test:unit": "npm run build && vitest run tests/unit",
|
|
18
19
|
"test:integration": "npm run build && vitest run tests/integration",
|
|
19
20
|
"test:integration:full": "bash scripts/setup-keyring.sh && npm run test:integration"
|
|
20
21
|
},
|