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-PHW66RC4.js");
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,