azdo-cli 0.11.0 → 0.13.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,10 +13,12 @@ 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`)
19
- - Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. See [docs/authentication.md](docs/authentication.md).
20
+ - Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. See [docs/authentication.md](docs/authentication.md).
21
+ - Trace all HTTP requests to a local file with `--trace <filepath>` (sensitive headers and tokens are automatically redacted).
20
22
 
21
23
  ## Installation
22
24
 
@@ -37,6 +39,11 @@ azdo config set project myproject
37
39
  # Read a work item
38
40
  azdo get-item 12345
39
41
 
42
+ # Download images embedded in a work item's rich-text fields (opt-in)
43
+ azdo get-item 12345 --download-images # saved to the system temp dir
44
+ azdo get-item 12345 --resize-images 1024 --images-path ./img # cap width at 1024px, save as PNG
45
+ azdo get-md-field 12345 System.Description --download-images # same flags on get-md-field
46
+
40
47
  # Update state
41
48
  azdo set-state 12345 "Active"
42
49
 
@@ -48,11 +55,33 @@ azdo comments list 12345
48
55
  azdo comments add 12345 "Investigating the root cause now."
49
56
 
50
57
  # PR comment threads — list, filter, target by number, resolve or reopen
51
- azdo pr comments # active-branch PR
58
+ azdo pr comments # active-branch PR; code-anchored threads show file:line
52
59
  azdo pr comments --pr-number 64 # any PR by number (skips branch lookup)
53
- azdo pr comments --pr-number 64 --hide-resolved
60
+ azdo pr comments --pr-number 64 --hide-resolved # or --exclude-resolved (alias)
61
+ azdo pr comments --code-related-only # only file/line-anchored threads
62
+ azdo pr status # PR checks (status + branch policies + pipeline builds) + code-comment counts
54
63
  azdo pr comment-resolve 17 --pr-number 64 # idempotent: exit 0 even when already resolved
55
64
  azdo pr comment-reopen 17 --pr-number 64
65
+
66
+ # Reply to a PR comment thread
67
+ azdo pr comments reply 148 "Great suggestion, I'll address it." # human-readable output
68
+ azdo pr comments reply 148 "Done." --pr-number 64 --json # JSON: { pullRequestId, threadId, commentId, content }
69
+ azdo pr comment-reply 148 "Done." --pr-number 64 # flat alias, identical behaviour
70
+
71
+ # Pipelines — list, inspect runs, wait (exit code = result), start
72
+ azdo pipeline list --filter ci
73
+ azdo pipeline get-runs 12 --branch develop --limit 1
74
+ azdo pipeline wait 3456 # blocks; exit 0 success / non-zero failure / 124 timeout
75
+ azdo pipeline get-run-detail 3456 # errors, failing tests, per-stage status
76
+ azdo pipeline start 12 --branch develop --parameter env=staging
77
+
78
+ # Work item relations — types, add, remove, list
79
+ azdo relations types # list all relation types (Child, Parent, Related, ...)
80
+ azdo relations types --json # machine-readable JSON array
81
+ azdo relations add child 1000 2000 # make #2000 a child of #1000 (idempotent)
82
+ azdo relations remove child 1000 2000 # remove the child relation
83
+ azdo relations list 1000 # show all relations on work item #1000
84
+ azdo relations list 1000 --json # JSON: { workItemId, relations: [...] }
56
85
  ```
57
86
 
58
87
  ## 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,