azdo-cli 0.11.0 → 0.12.0
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/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
13
13
|
- Create or update work items from markdown documents (`upsert`)
|
|
14
14
|
- Read and post work item comments (`comments`)
|
|
15
15
|
- Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
|
|
16
|
+
- Download images embedded in rich-text fields, optionally resized for LLM use (`get-item`/`get-md-field` `--download-images`, `--resize-images`)
|
|
16
17
|
- Check branch pull request status, open PRs to `develop`, list PR comment threads for any PR (`--pr-number`), and resolve/reopen threads from the CLI (`pr`)
|
|
17
18
|
- Persist org/project/default fields in local config (`config`)
|
|
18
19
|
- List all fields of a work item (`list-fields`)
|
|
@@ -37,6 +38,11 @@ azdo config set project myproject
|
|
|
37
38
|
# Read a work item
|
|
38
39
|
azdo get-item 12345
|
|
39
40
|
|
|
41
|
+
# Download images embedded in a work item's rich-text fields (opt-in)
|
|
42
|
+
azdo get-item 12345 --download-images # saved to the system temp dir
|
|
43
|
+
azdo get-item 12345 --resize-images 1024 --images-path ./img # cap width at 1024px, save as PNG
|
|
44
|
+
azdo get-md-field 12345 System.Description --download-images # same flags on get-md-field
|
|
45
|
+
|
|
40
46
|
# Update state
|
|
41
47
|
azdo set-state 12345 "Active"
|
|
42
48
|
|
|
@@ -48,11 +54,28 @@ azdo comments list 12345
|
|
|
48
54
|
azdo comments add 12345 "Investigating the root cause now."
|
|
49
55
|
|
|
50
56
|
# PR comment threads — list, filter, target by number, resolve or reopen
|
|
51
|
-
azdo pr comments # active-branch PR
|
|
57
|
+
azdo pr comments # active-branch PR; code-anchored threads show file:line
|
|
52
58
|
azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
|
|
53
|
-
azdo pr comments --pr-number 64 --hide-resolved
|
|
59
|
+
azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
|
|
60
|
+
azdo pr comments --code-related-only # only file/line-anchored threads
|
|
61
|
+
azdo pr status # PR checks (status + branch policies + pipeline builds) + code-comment counts
|
|
54
62
|
azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
|
|
55
63
|
azdo pr comment-reopen 17 --pr-number 64
|
|
64
|
+
|
|
65
|
+
# Pipelines — list, inspect runs, wait (exit code = result), start
|
|
66
|
+
azdo pipeline list --filter ci
|
|
67
|
+
azdo pipeline get-runs 12 --branch develop --limit 1
|
|
68
|
+
azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
|
|
69
|
+
azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
|
|
70
|
+
azdo pipeline start 12 --branch develop --parameter env=staging
|
|
71
|
+
|
|
72
|
+
# Work item relations — types, add, remove, list
|
|
73
|
+
azdo relations types # list all relation types (Child, Parent, Related, ...)
|
|
74
|
+
azdo relations types --json # machine-readable JSON array
|
|
75
|
+
azdo relations add child 1000 2000 # make #2000 a child of #1000 (idempotent)
|
|
76
|
+
azdo relations remove child 1000 2000 # remove the child relation
|
|
77
|
+
azdo relations list 1000 # show all relations on work item #1000
|
|
78
|
+
azdo relations list 1000 --json # JSON: { workItemId, relations: [...] }
|
|
56
79
|
```
|
|
57
80
|
|
|
58
81
|
## Documentation
|
|
@@ -65,6 +65,7 @@ var SETTINGS = [
|
|
|
65
65
|
}
|
|
66
66
|
];
|
|
67
67
|
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
68
|
+
var SCOPED_KEYS = ["project", "fields", "markdown"];
|
|
68
69
|
function getConfigPath() {
|
|
69
70
|
return path.join(os.homedir(), ".azdo", "config.json");
|
|
70
71
|
}
|
|
@@ -88,6 +89,20 @@ function loadConfig() {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
function saveConfig(config) {
|
|
92
|
+
if (config.organizations) {
|
|
93
|
+
const normalised = {};
|
|
94
|
+
for (const [k, v] of Object.entries(config.organizations)) {
|
|
95
|
+
if (v && Object.keys(v).length > 0) {
|
|
96
|
+
normalised[k.toLowerCase()] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(normalised).length > 0) {
|
|
100
|
+
config = { ...config, organizations: normalised };
|
|
101
|
+
} else {
|
|
102
|
+
const { organizations: _, ...rest } = config;
|
|
103
|
+
config = rest;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
91
106
|
const configPath = getConfigPath();
|
|
92
107
|
const dir = path.dirname(configPath);
|
|
93
108
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -126,6 +141,115 @@ function unsetConfigValue(key) {
|
|
|
126
141
|
delete config[key];
|
|
127
142
|
saveConfig(config);
|
|
128
143
|
}
|
|
144
|
+
function resolveScopedConfig(org) {
|
|
145
|
+
const config = loadConfig();
|
|
146
|
+
const base = {
|
|
147
|
+
project: config.project,
|
|
148
|
+
fields: config.fields,
|
|
149
|
+
markdown: config.markdown,
|
|
150
|
+
org: config.org
|
|
151
|
+
};
|
|
152
|
+
if (!org) return base;
|
|
153
|
+
const scope = config.organizations?.[org.toLowerCase()];
|
|
154
|
+
if (!scope) return base;
|
|
155
|
+
return {
|
|
156
|
+
org: config.org,
|
|
157
|
+
project: scope.project ?? config.project,
|
|
158
|
+
fields: scope.fields ?? config.fields,
|
|
159
|
+
markdown: scope.markdown ?? config.markdown
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function validateScopedKey(key) {
|
|
163
|
+
if (!SCOPED_KEYS.includes(key)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Invalid scoped key "${key}". Valid scoped keys: ${SCOPED_KEYS.join(", ")}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function applyValueToScope(scope, key, value) {
|
|
170
|
+
if (key === "fields") {
|
|
171
|
+
return { ...scope, fields: value.split(",").map((s) => s.trim()).filter(Boolean) };
|
|
172
|
+
}
|
|
173
|
+
if (key === "markdown") {
|
|
174
|
+
if (value !== "true" && value !== "false") {
|
|
175
|
+
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
176
|
+
}
|
|
177
|
+
return { ...scope, markdown: value === "true" };
|
|
178
|
+
}
|
|
179
|
+
return { ...scope, [key]: value };
|
|
180
|
+
}
|
|
181
|
+
function setOrgScopedValue(org, key, value) {
|
|
182
|
+
validateScopedKey(key);
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const lc = org.toLowerCase();
|
|
185
|
+
const existing = config.organizations?.[lc] ?? {};
|
|
186
|
+
const updated = applyValueToScope(existing, key, value);
|
|
187
|
+
saveConfig({
|
|
188
|
+
...config,
|
|
189
|
+
organizations: { ...config.organizations, [lc]: updated }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function unsetOrgScopedValue(org, key) {
|
|
193
|
+
validateScopedKey(key);
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
const lc = org.toLowerCase();
|
|
196
|
+
const scope = config.organizations?.[lc];
|
|
197
|
+
if (!scope) return;
|
|
198
|
+
const { [key]: _, ...rest } = scope;
|
|
199
|
+
saveConfig({
|
|
200
|
+
...config,
|
|
201
|
+
organizations: { ...config.organizations, [lc]: rest }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function getOrgScopedValue(org, key) {
|
|
205
|
+
validateScopedKey(key);
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
const scope = config.organizations?.[org.toLowerCase()];
|
|
208
|
+
return scope?.[key];
|
|
209
|
+
}
|
|
210
|
+
function readScope(config, name) {
|
|
211
|
+
if (name === "default") {
|
|
212
|
+
const { org: _o, organizations: _orgs, ...defaults } = config;
|
|
213
|
+
return defaults;
|
|
214
|
+
}
|
|
215
|
+
return config.organizations?.[name.toLowerCase()] ?? {};
|
|
216
|
+
}
|
|
217
|
+
function copyOrgScope(from, to, force = false) {
|
|
218
|
+
const config = loadConfig();
|
|
219
|
+
const source = readScope(config, from);
|
|
220
|
+
const toLc = to.toLowerCase();
|
|
221
|
+
const dest = config.organizations?.[toLc] ?? {};
|
|
222
|
+
if (!force) {
|
|
223
|
+
const collisions = Object.keys(source).filter(
|
|
224
|
+
(k) => dest[k] !== void 0
|
|
225
|
+
);
|
|
226
|
+
if (collisions.length > 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Scope "${toLc}" already has values for: ${collisions.join(", ")}. Use --force to overwrite.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
saveConfig({
|
|
233
|
+
...config,
|
|
234
|
+
organizations: {
|
|
235
|
+
...config.organizations,
|
|
236
|
+
[toLc]: { ...dest, ...source }
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function moveOrgScope(from, to, force = false) {
|
|
241
|
+
copyOrgScope(from, to, force);
|
|
242
|
+
if (from !== "default") {
|
|
243
|
+
deleteOrgScope(from);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function deleteOrgScope(name) {
|
|
247
|
+
const config = loadConfig();
|
|
248
|
+
const lc = name.toLowerCase();
|
|
249
|
+
if (!config.organizations?.[lc]) return;
|
|
250
|
+
const { [lc]: _, ...rest } = config.organizations;
|
|
251
|
+
saveConfig({ ...config, organizations: rest });
|
|
252
|
+
}
|
|
129
253
|
|
|
130
254
|
// src/services/oauth-config.ts
|
|
131
255
|
var DEFAULT_OAUTH_CLIENT_ID = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";
|
|
@@ -864,7 +988,7 @@ async function deletePat(org) {
|
|
|
864
988
|
}
|
|
865
989
|
try {
|
|
866
990
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
867
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
991
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-7FQ4VAIS.js");
|
|
868
992
|
unlinkSync2(lockPath2(org));
|
|
869
993
|
} catch {
|
|
870
994
|
}
|
|
@@ -1061,6 +1185,13 @@ export {
|
|
|
1061
1185
|
getConfigValue,
|
|
1062
1186
|
setConfigValue,
|
|
1063
1187
|
unsetConfigValue,
|
|
1188
|
+
resolveScopedConfig,
|
|
1189
|
+
setOrgScopedValue,
|
|
1190
|
+
unsetOrgScopedValue,
|
|
1191
|
+
getOrgScopedValue,
|
|
1192
|
+
copyOrgScope,
|
|
1193
|
+
moveOrgScope,
|
|
1194
|
+
deleteOrgScope,
|
|
1064
1195
|
maskedDisplay,
|
|
1065
1196
|
normalizePat,
|
|
1066
1197
|
AZDO_RESOURCE_ID,
|