azdo-cli 0.20.0-feature-043-dpapi-credential-store.744 → 0.20.0-feature-043-dpapi-credential-store.746

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
@@ -19,7 +19,7 @@ Azure DevOps CLI focused on work item read/write workflows.
19
19
  - Feed long PR titles, descriptions and comment bodies from a file or a pipe instead of the shell (`--title-file`, `--description-file`, `--file`; `-` reads standard input)
20
20
  - Persist org/project/default fields in local config (`config`)
21
21
  - List all fields of a work item (`list-fields`)
22
- - 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`. Print the credential the CLI itself would use, for API calls the CLI does not yet wrap, with `azdo auth token`. See [docs/authentication.md](docs/authentication.md).
22
+ - 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 — or, on Windows over SSH where Credential Manager is unreachable, in DPAPI-encrypted files under `~/.azdo` (`azdo config set credentialStore dpapi`, which offers to copy existing Credential Manager credentials; `--copy-credentials` / `--no-copy-credentials` skip the prompt). Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. Print the credential the CLI itself would use, for API calls the CLI does not yet wrap, with `azdo auth token`. See [docs/authentication.md](docs/authentication.md).
23
23
  - Trace all HTTP requests to a local file with `--trace <filepath>` (sensitive headers and tokens are automatically redacted).
24
24
 
25
25
  ## Installation
@@ -38,6 +38,10 @@ azdo auth login --org myorg
38
38
  azdo config set org myorg
39
39
  azdo config set project myproject
40
40
 
41
+ # Windows over SSH: keep credentials in DPAPI files instead of Credential Manager
42
+ # (run once from a console to copy the existing credentials across)
43
+ azdo config set credentialStore dpapi --copy-credentials
44
+
41
45
  # Read a work item
42
46
  azdo get-item 12345
43
47
 
@@ -947,8 +947,12 @@ function requestedStore() {
947
947
  let fromConfig;
948
948
  try {
949
949
  fromConfig = loadConfig()?.credentialStore;
950
- } catch {
951
- fromConfig = void 0;
950
+ } catch (err) {
951
+ throw new CredentialStoreUnavailableError(
952
+ "unknown",
953
+ err,
954
+ `Could not read the azdo config file to determine the credential store: ${err.message}. Fix the file, or set AZDO_CREDENTIAL_STORE=<keyring|dpapi> for this session.`
955
+ );
952
956
  }
953
957
  return fromConfig?.trim().toLowerCase() || "keyring";
954
958
  }
@@ -966,7 +970,13 @@ function activeCredentialStore() {
966
970
  }
967
971
  }
968
972
  function probeBackend() {
969
- if (requestedStore() === "dpapi") return "windows-dpapi";
973
+ let requested;
974
+ try {
975
+ requested = requestedStore();
976
+ } catch {
977
+ return "unknown";
978
+ }
979
+ if (requested === "dpapi") return "windows-dpapi";
970
980
  switch (process.platform) {
971
981
  case "win32":
972
982
  return "windows-credential-manager";
@@ -1168,7 +1178,7 @@ async function deletePat(org) {
1168
1178
  }
1169
1179
  try {
1170
1180
  const { unlinkSync: unlinkSync2 } = await import("fs");
1171
- const { lockPath: lockPath2 } = await import("./oauth-token-refresh-M6L5EUZW.js");
1181
+ const { lockPath: lockPath2 } = await import("./oauth-token-refresh-RUT4XIMM.js");
1172
1182
  unlinkSync2(lockPath2(org));
1173
1183
  } catch {
1174
1184
  }
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  tokenResponseToCredential,
40
40
  unsetConfigValue,
41
41
  unsetOrgScopedValue
42
- } from "./chunk-RG45CRKX.js";
42
+ } from "./chunk-W4EVKARR.js";
43
43
 
44
44
  // src/program.ts
45
45
  import { Command as Command19 } from "commander";
@@ -143,6 +143,14 @@ function getActiveTraceWriter() {
143
143
  }
144
144
 
145
145
  // src/services/command-helpers.ts
146
+ var EXIT_CREDENTIAL_STORE_UNAVAILABLE = 4;
147
+ function reportCredentialStoreUnavailable(err) {
148
+ if (!(err instanceof CredentialStoreUnavailableError)) return false;
149
+ process.stderr.write(`Error: ${err.message}
150
+ `);
151
+ process.exitCode = EXIT_CREDENTIAL_STORE_UNAVAILABLE;
152
+ return true;
153
+ }
146
154
  function isSentinel(message, sentinel) {
147
155
  return message === sentinel || message.startsWith(`${sentinel}: `);
148
156
  }
@@ -221,6 +229,10 @@ function formatCreateError(err) {
221
229
  return `Create rejected: ${message}`;
222
230
  }
223
231
  function handleCommandError(err, id, context, scope = "write", exit = true) {
232
+ if (reportCredentialStoreUnavailable(err)) {
233
+ if (exit) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
234
+ return;
235
+ }
224
236
  const error = err instanceof Error ? err : new Error(String(err));
225
237
  const msg = error.message;
226
238
  const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
@@ -2679,7 +2691,8 @@ function buildConfigListEntries(cfg) {
2679
2691
  return entries;
2680
2692
  }
2681
2693
  function writeConfigList(cfg) {
2682
- const keyWidth = 10;
2694
+ const orgKeys = Object.values(cfg.organizations ?? {}).flatMap((scope) => Object.keys(scope ?? {}));
2695
+ const keyWidth = Math.max(10, ...SETTINGS.map((s) => s.key.length + 1), ...orgKeys.map((k) => k.length + 1));
2683
2696
  const valueWidth = 30;
2684
2697
  const scopeWidth = 12;
2685
2698
  process.stdout.write(
@@ -3461,6 +3474,7 @@ function isCreateWriteError(err) {
3461
3474
  return isSentinel(err.message, "AUTH_FAILED") || isSentinel(err.message, "PERMISSION_DENIED") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
3462
3475
  }
3463
3476
  function handleUpsertError(err, id, context) {
3477
+ if (reportCredentialStoreUnavailable(err)) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
3464
3478
  if (!(err instanceof Error)) {
3465
3479
  process.stderr.write(`Error: ${String(err)}
3466
3480
  `);
@@ -4455,6 +4469,7 @@ function writeError(message, exitCode = 1) {
4455
4469
  process.exitCode = exitCode;
4456
4470
  }
4457
4471
  function handlePrCommandError(err, context, mode = "read") {
4472
+ if (reportCredentialStoreUnavailable(err)) return;
4458
4473
  const error = err instanceof Error ? err : new Error(String(err));
4459
4474
  if (isSentinel(error.message, "AUTH_FAILED")) {
4460
4475
  const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
@@ -6165,6 +6180,7 @@ function writeError2(message) {
6165
6180
  process.exitCode = 1;
6166
6181
  }
6167
6182
  function handlePipelineError(err, context) {
6183
+ if (reportCredentialStoreUnavailable(err)) return;
6168
6184
  const error = err instanceof Error ? err : new Error(String(err));
6169
6185
  if (isSentinel(error.message, "AUTH_FAILED")) {
6170
6186
  writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
@@ -7224,6 +7240,7 @@ function formatRelationsList(workItemId, relations) {
7224
7240
  return lines.join("\n");
7225
7241
  }
7226
7242
  function handleRelationError(err, id1) {
7243
+ if (reportCredentialStoreUnavailable(err)) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
7227
7244
  const msg = err instanceof Error ? err.message : String(err);
7228
7245
  if (msg === "SELF_RELATION") {
7229
7246
  process.stderr.write(`Error: cannot relate a work item to itself (#${id1}).
@@ -5,7 +5,7 @@ import {
5
5
  lockPath,
6
6
  locksDir,
7
7
  refreshIfNeeded
8
- } from "./chunk-RG45CRKX.js";
8
+ } from "./chunk-W4EVKARR.js";
9
9
  export {
10
10
  _resetInFlight,
11
11
  classifyRefreshFailure,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.20.0-feature-043-dpapi-credential-store.744",
3
+ "version": "0.20.0-feature-043-dpapi-credential-store.746",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {