azdo-cli 0.5.0-013-comments-markdown.174 → 0.5.0-014-work-item-attachments.182
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 +109 -10
- 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";
|
|
@@ -857,6 +888,23 @@ function summarizeDescription(text, label, markdown) {
|
|
|
857
888
|
}
|
|
858
889
|
return [`${label("Description:")}${content}`];
|
|
859
890
|
}
|
|
891
|
+
function formatFileSize(bytes) {
|
|
892
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
893
|
+
const kb = bytes / 1024;
|
|
894
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
895
|
+
const mb = kb / 1024;
|
|
896
|
+
return `${mb.toFixed(1)} MB`;
|
|
897
|
+
}
|
|
898
|
+
function formatAttachments(attachments, short) {
|
|
899
|
+
if (short) {
|
|
900
|
+
return [`Attachments: ${attachments.length}`];
|
|
901
|
+
}
|
|
902
|
+
const lines = ["", "Attachments:"];
|
|
903
|
+
for (const att of attachments) {
|
|
904
|
+
lines.push(` ${att.name} (${formatFileSize(att.size)})`);
|
|
905
|
+
}
|
|
906
|
+
return lines;
|
|
907
|
+
}
|
|
860
908
|
function formatWorkItem(workItem, short, markdown = false) {
|
|
861
909
|
const label = (name) => name.padEnd(13);
|
|
862
910
|
const lines = [
|
|
@@ -883,6 +931,9 @@ function formatWorkItem(workItem, short, markdown = false) {
|
|
|
883
931
|
} else {
|
|
884
932
|
lines.push("Description:", descriptionText);
|
|
885
933
|
}
|
|
934
|
+
if (workItem.attachments) {
|
|
935
|
+
lines.push(...formatAttachments(workItem.attachments, short));
|
|
936
|
+
}
|
|
886
937
|
return lines.join("\n");
|
|
887
938
|
}
|
|
888
939
|
function createGetItemCommand() {
|
|
@@ -2171,8 +2222,55 @@ function createCommentsCommand() {
|
|
|
2171
2222
|
return command;
|
|
2172
2223
|
}
|
|
2173
2224
|
|
|
2225
|
+
// src/commands/download-attachment.ts
|
|
2226
|
+
import { Command as Command13 } from "commander";
|
|
2227
|
+
import { writeFile } from "fs/promises";
|
|
2228
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2229
|
+
import { join as join2 } from "path";
|
|
2230
|
+
function createDownloadAttachmentCommand() {
|
|
2231
|
+
const command = new Command13("download-attachment");
|
|
2232
|
+
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(
|
|
2233
|
+
async (idStr, filename, options) => {
|
|
2234
|
+
const id = parseWorkItemId(idStr);
|
|
2235
|
+
validateOrgProjectPair(options);
|
|
2236
|
+
let context;
|
|
2237
|
+
try {
|
|
2238
|
+
context = resolveContext(options);
|
|
2239
|
+
const credential = await resolvePat();
|
|
2240
|
+
const outputDir = options.output ?? ".";
|
|
2241
|
+
if (!existsSync4(outputDir)) {
|
|
2242
|
+
process.stderr.write(`Error: Output directory "${outputDir}" does not exist.
|
|
2243
|
+
`);
|
|
2244
|
+
process.exit(1);
|
|
2245
|
+
}
|
|
2246
|
+
const workItem = await getWorkItem(context, id, credential.pat);
|
|
2247
|
+
const attachment = workItem.attachments?.find(
|
|
2248
|
+
(a) => a.name === filename
|
|
2249
|
+
);
|
|
2250
|
+
if (!attachment) {
|
|
2251
|
+
process.stderr.write(
|
|
2252
|
+
`Error: Attachment "${filename}" not found on work item ${id}.
|
|
2253
|
+
`
|
|
2254
|
+
);
|
|
2255
|
+
process.exit(1);
|
|
2256
|
+
}
|
|
2257
|
+
const data = await downloadAttachment(attachment.url, credential.pat);
|
|
2258
|
+
const outputPath = join2(outputDir, filename);
|
|
2259
|
+
await writeFile(outputPath, Buffer.from(data));
|
|
2260
|
+
process.stdout.write(
|
|
2261
|
+
`Downloaded "${filename}" (${formatFileSize(attachment.size)}) to ${outputPath}
|
|
2262
|
+
`
|
|
2263
|
+
);
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
handleCommandError(err, id, context, "read", false);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
);
|
|
2269
|
+
return command;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2174
2272
|
// src/index.ts
|
|
2175
|
-
var program = new
|
|
2273
|
+
var program = new Command14();
|
|
2176
2274
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
2177
2275
|
program.addCommand(createGetItemCommand());
|
|
2178
2276
|
program.addCommand(createClearPatCommand());
|
|
@@ -2186,6 +2284,7 @@ program.addCommand(createUpsertCommand());
|
|
|
2186
2284
|
program.addCommand(createListFieldsCommand());
|
|
2187
2285
|
program.addCommand(createPrCommand());
|
|
2188
2286
|
program.addCommand(createCommentsCommand());
|
|
2287
|
+
program.addCommand(createDownloadAttachmentCommand());
|
|
2189
2288
|
program.showHelpAfterError();
|
|
2190
2289
|
program.parse();
|
|
2191
2290
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.5.0-
|
|
3
|
+
"version": "0.5.0-014-work-item-attachments.182",
|
|
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
|
},
|