azdo-cli 0.20.0-feature-043-dpapi-credential-store.742 → 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
|
-
|
|
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
|
-
|
|
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";
|
|
@@ -978,14 +988,17 @@ function probeBackend() {
|
|
|
978
988
|
return "unknown";
|
|
979
989
|
}
|
|
980
990
|
}
|
|
981
|
-
function
|
|
991
|
+
function wrapUnavailableAs(backend, fn) {
|
|
982
992
|
try {
|
|
983
993
|
return fn();
|
|
984
994
|
} catch (err) {
|
|
985
995
|
if (err instanceof CredentialStoreUnavailableError) throw err;
|
|
986
|
-
throw new CredentialStoreUnavailableError(
|
|
996
|
+
throw new CredentialStoreUnavailableError(backend, err);
|
|
987
997
|
}
|
|
988
998
|
}
|
|
999
|
+
function wrapUnavailable(fn) {
|
|
1000
|
+
return wrapUnavailableAs(probeBackend(), fn);
|
|
1001
|
+
}
|
|
989
1002
|
function entryFor(account) {
|
|
990
1003
|
if (activeCredentialStore() === "dpapi") return new DpapiEntry(account);
|
|
991
1004
|
return wrapUnavailable(() => new Entry(SERVICE, account));
|
|
@@ -1165,7 +1178,7 @@ async function deletePat(org) {
|
|
|
1165
1178
|
}
|
|
1166
1179
|
try {
|
|
1167
1180
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
1168
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
1181
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-RUT4XIMM.js");
|
|
1169
1182
|
unlinkSync2(lockPath2(org));
|
|
1170
1183
|
} catch {
|
|
1171
1184
|
}
|
|
@@ -1191,6 +1204,62 @@ async function listOrgsWithStoredPat() {
|
|
|
1191
1204
|
present.sort((a, b) => a.localeCompare(b));
|
|
1192
1205
|
return present;
|
|
1193
1206
|
}
|
|
1207
|
+
function keyringEntry(account) {
|
|
1208
|
+
return new Entry(SERVICE, account);
|
|
1209
|
+
}
|
|
1210
|
+
function knownOrgs() {
|
|
1211
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
1212
|
+
for (const ev of readAuditEvents()) {
|
|
1213
|
+
if (ev.event === "auth.store" || ev.event === "oauth-login-success") orgs.add(ev.org);
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const config = loadConfig();
|
|
1217
|
+
if (config.org) orgs.add(config.org);
|
|
1218
|
+
for (const name of Object.keys(config.organizations ?? {})) orgs.add(name);
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
return [...orgs].sort((a, b) => a.localeCompare(b));
|
|
1222
|
+
}
|
|
1223
|
+
function listKeyringCredentials() {
|
|
1224
|
+
const present = [];
|
|
1225
|
+
for (const org of knownOrgs()) {
|
|
1226
|
+
const value = wrapUnavailableAs(
|
|
1227
|
+
"windows-credential-manager",
|
|
1228
|
+
() => keyringEntry(accountFor(org)).getPassword()
|
|
1229
|
+
);
|
|
1230
|
+
if (value !== null) present.push(org);
|
|
1231
|
+
}
|
|
1232
|
+
return present;
|
|
1233
|
+
}
|
|
1234
|
+
function copyKeyringCredentialsToDpapi(orgs) {
|
|
1235
|
+
const result = { copied: [], skipped: [], failed: [] };
|
|
1236
|
+
for (const org of orgs) {
|
|
1237
|
+
try {
|
|
1238
|
+
const raw = wrapUnavailableAs(
|
|
1239
|
+
"windows-credential-manager",
|
|
1240
|
+
() => keyringEntry(accountFor(org)).getPassword()
|
|
1241
|
+
);
|
|
1242
|
+
if (raw === null) continue;
|
|
1243
|
+
const target = new DpapiEntry(accountFor(org));
|
|
1244
|
+
if (target.getPassword() !== null) {
|
|
1245
|
+
result.skipped.push(org);
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const cred = parseStoredValue(raw);
|
|
1249
|
+
target.setPassword(raw);
|
|
1250
|
+
appendAuthAuditEvent({
|
|
1251
|
+
event: "auth.store",
|
|
1252
|
+
org,
|
|
1253
|
+
backend: "windows-dpapi",
|
|
1254
|
+
...cred.kind === "pat" ? { masked_pat: maskedDisplay(cred.token) } : { accountId: cred.accountId }
|
|
1255
|
+
});
|
|
1256
|
+
result.copied.push(org);
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
result.failed.push({ org, message: err.message });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
1194
1263
|
|
|
1195
1264
|
// src/services/oauth-token-refresh.ts
|
|
1196
1265
|
var DEFAULT_LOCK_WAIT_MS = 5e3;
|
|
@@ -1393,5 +1462,7 @@ export {
|
|
|
1393
1462
|
storePat,
|
|
1394
1463
|
storeOAuthCredential,
|
|
1395
1464
|
deletePat,
|
|
1396
|
-
listOrgsWithStoredPat
|
|
1465
|
+
listOrgsWithStoredPat,
|
|
1466
|
+
listKeyringCredentials,
|
|
1467
|
+
copyKeyringCredentialsToDpapi
|
|
1397
1468
|
};
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SETTINGS,
|
|
8
8
|
appendAuthAuditEvent,
|
|
9
9
|
buildScopeString,
|
|
10
|
+
copyKeyringCredentialsToDpapi,
|
|
10
11
|
copyOrgScope,
|
|
11
12
|
defaultScopes,
|
|
12
13
|
deleteOrgScope,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
getOrgScopedValue,
|
|
17
18
|
getPat,
|
|
18
19
|
getStoredCredential,
|
|
20
|
+
listKeyringCredentials,
|
|
19
21
|
listOrgsWithStoredPat,
|
|
20
22
|
loadConfig,
|
|
21
23
|
maskedDisplay,
|
|
@@ -37,7 +39,7 @@ import {
|
|
|
37
39
|
tokenResponseToCredential,
|
|
38
40
|
unsetConfigValue,
|
|
39
41
|
unsetOrgScopedValue
|
|
40
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-W4EVKARR.js";
|
|
41
43
|
|
|
42
44
|
// src/program.ts
|
|
43
45
|
import { Command as Command19 } from "commander";
|
|
@@ -141,6 +143,14 @@ function getActiveTraceWriter() {
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
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
|
+
}
|
|
144
154
|
function isSentinel(message, sentinel) {
|
|
145
155
|
return message === sentinel || message.startsWith(`${sentinel}: `);
|
|
146
156
|
}
|
|
@@ -219,6 +229,10 @@ function formatCreateError(err) {
|
|
|
219
229
|
return `Create rejected: ${message}`;
|
|
220
230
|
}
|
|
221
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
|
+
}
|
|
222
236
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
223
237
|
const msg = error.message;
|
|
224
238
|
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
@@ -2607,6 +2621,56 @@ not be resolved, 4 OS credential store unavailable. stdout stays empty on all.
|
|
|
2607
2621
|
// src/commands/config.ts
|
|
2608
2622
|
import { Command as Command4 } from "commander";
|
|
2609
2623
|
import { createInterface as createInterface2 } from "readline";
|
|
2624
|
+
|
|
2625
|
+
// src/services/dpapi-copy.ts
|
|
2626
|
+
var RERUN = "azdo config set credentialStore dpapi --copy-credentials";
|
|
2627
|
+
function isYes(answer) {
|
|
2628
|
+
const a = answer.trim().toLowerCase();
|
|
2629
|
+
return a === "" || a === "y" || a === "yes";
|
|
2630
|
+
}
|
|
2631
|
+
function empty(found, keyringUnavailable) {
|
|
2632
|
+
return { found, keyringUnavailable, copied: [], skipped: [], failed: [] };
|
|
2633
|
+
}
|
|
2634
|
+
async function offerKeyringToDpapiCopy(choice, io) {
|
|
2635
|
+
if (choice === "no") return empty([], false);
|
|
2636
|
+
let found;
|
|
2637
|
+
try {
|
|
2638
|
+
found = listKeyringCredentials();
|
|
2639
|
+
} catch {
|
|
2640
|
+
io.notice(
|
|
2641
|
+
`Credential Manager is not reachable from this session (typical over SSH), so no existing credentials were copied. To bring them over, run \`${RERUN}\` once from a console session.
|
|
2642
|
+
`
|
|
2643
|
+
);
|
|
2644
|
+
return empty([], true);
|
|
2645
|
+
}
|
|
2646
|
+
if (found.length === 0) return empty(found, false);
|
|
2647
|
+
const list = found.join(", ");
|
|
2648
|
+
if (choice === "ask") {
|
|
2649
|
+
if (!io.interactive) {
|
|
2650
|
+
io.notice(`Credential Manager holds credentials for ${list}. Run \`${RERUN}\` to copy them to the DPAPI store.
|
|
2651
|
+
`);
|
|
2652
|
+
return empty(found, false);
|
|
2653
|
+
}
|
|
2654
|
+
const answer = await io.ask(`Copy the credentials for ${list} from Credential Manager to the DPAPI store? [Y/n] `);
|
|
2655
|
+
if (!isYes(answer)) {
|
|
2656
|
+
io.notice("Nothing copied. Run `azdo auth login` for each org, or re-run with --copy-credentials.\n");
|
|
2657
|
+
return empty(found, false);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
const result = copyKeyringCredentialsToDpapi(found);
|
|
2661
|
+
for (const org of result.copied) io.notice(`Copied the credential for ${org} to the DPAPI store.
|
|
2662
|
+
`);
|
|
2663
|
+
for (const org of result.skipped) io.notice(`Kept the existing DPAPI credential for ${org} (not overwritten).
|
|
2664
|
+
`);
|
|
2665
|
+
for (const { org, message } of result.failed) io.notice(`Could not copy the credential for ${org}: ${message}
|
|
2666
|
+
`);
|
|
2667
|
+
if (result.copied.length > 0) {
|
|
2668
|
+
io.notice("The Credential Manager entries were left in place; `credentialStore keyring` still finds them.\n");
|
|
2669
|
+
}
|
|
2670
|
+
return { found, keyringUnavailable: false, ...result };
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// src/commands/config.ts
|
|
2610
2674
|
function formatConfigValue(value, unsetFallback = "") {
|
|
2611
2675
|
if (value === void 0) {
|
|
2612
2676
|
return unsetFallback;
|
|
@@ -2627,7 +2691,8 @@ function buildConfigListEntries(cfg) {
|
|
|
2627
2691
|
return entries;
|
|
2628
2692
|
}
|
|
2629
2693
|
function writeConfigList(cfg) {
|
|
2630
|
-
const
|
|
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));
|
|
2631
2696
|
const valueWidth = 30;
|
|
2632
2697
|
const scopeWidth = 12;
|
|
2633
2698
|
process.stdout.write(
|
|
@@ -2661,6 +2726,25 @@ function writeConfigList(cfg) {
|
|
|
2661
2726
|
);
|
|
2662
2727
|
}
|
|
2663
2728
|
}
|
|
2729
|
+
async function offerCopy(options) {
|
|
2730
|
+
let choice = "ask";
|
|
2731
|
+
if (options.copyCredentials === true) choice = "yes";
|
|
2732
|
+
else if (options.copyCredentials === false) choice = "no";
|
|
2733
|
+
const interactive = !options.json && Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY);
|
|
2734
|
+
let rl;
|
|
2735
|
+
try {
|
|
2736
|
+
return await offerKeyringToDpapiCopy(choice, {
|
|
2737
|
+
interactive,
|
|
2738
|
+
ask: (prompt) => {
|
|
2739
|
+
rl ??= createInterface2({ input: process.stdin, output: process.stderr });
|
|
2740
|
+
return createAsk(rl)(prompt);
|
|
2741
|
+
},
|
|
2742
|
+
notice: (message) => process.stderr.write(message)
|
|
2743
|
+
});
|
|
2744
|
+
} finally {
|
|
2745
|
+
rl?.close();
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2664
2748
|
function createAsk(rl) {
|
|
2665
2749
|
return (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
2666
2750
|
}
|
|
@@ -2713,30 +2797,38 @@ function createConfigCommand() {
|
|
|
2713
2797
|
].join("\n")
|
|
2714
2798
|
);
|
|
2715
2799
|
const set = new Command4("set");
|
|
2716
|
-
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").action((key, value, options) => {
|
|
2800
|
+
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").option("--copy-credentials", "with credentialStore dpapi: copy Credential Manager credentials without asking").option("--no-copy-credentials", "with credentialStore dpapi: do not offer to copy Credential Manager credentials").action((key, value, options) => {
|
|
2717
2801
|
try {
|
|
2718
2802
|
if (options.org) {
|
|
2719
2803
|
setOrgScopedValue(options.org, key, value);
|
|
2720
2804
|
} else {
|
|
2721
2805
|
setConfigValue(key, value);
|
|
2722
2806
|
}
|
|
2723
|
-
if (options.json) {
|
|
2724
|
-
const output = { key, value, scope: options.org ?? "default" };
|
|
2725
|
-
if (key === "fields") {
|
|
2726
|
-
output.value = value.split(",").map((s) => s.trim());
|
|
2727
|
-
}
|
|
2728
|
-
process.stdout.write(JSON.stringify(output) + "\n");
|
|
2729
|
-
} else {
|
|
2730
|
-
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2731
|
-
process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
|
|
2732
|
-
`);
|
|
2733
|
-
}
|
|
2734
2807
|
} catch (err) {
|
|
2735
2808
|
const message = err instanceof Error ? err.message : String(err);
|
|
2736
2809
|
process.stderr.write(`Error: ${message}
|
|
2737
2810
|
`);
|
|
2738
2811
|
process.exit(1);
|
|
2739
2812
|
}
|
|
2813
|
+
const output = { key, value, scope: options.org ?? "default" };
|
|
2814
|
+
if (key === "fields") {
|
|
2815
|
+
output.value = value.split(",").map((s) => s.trim());
|
|
2816
|
+
}
|
|
2817
|
+
if (!options.json) {
|
|
2818
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2819
|
+
process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
|
|
2820
|
+
`);
|
|
2821
|
+
}
|
|
2822
|
+
const writeJson = () => {
|
|
2823
|
+
if (options.json) process.stdout.write(JSON.stringify(output) + "\n");
|
|
2824
|
+
};
|
|
2825
|
+
if (!options.org && key === "credentialStore" && loadConfig().credentialStore === "dpapi") {
|
|
2826
|
+
return offerCopy(options).then((outcome) => {
|
|
2827
|
+
output.credentialsCopied = outcome.copied;
|
|
2828
|
+
writeJson();
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
writeJson();
|
|
2740
2832
|
});
|
|
2741
2833
|
const get = new Command4("get");
|
|
2742
2834
|
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields, markdown, credentialStore)").option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
@@ -3382,6 +3474,7 @@ function isCreateWriteError(err) {
|
|
|
3382
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_");
|
|
3383
3475
|
}
|
|
3384
3476
|
function handleUpsertError(err, id, context) {
|
|
3477
|
+
if (reportCredentialStoreUnavailable(err)) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
|
|
3385
3478
|
if (!(err instanceof Error)) {
|
|
3386
3479
|
process.stderr.write(`Error: ${String(err)}
|
|
3387
3480
|
`);
|
|
@@ -4376,6 +4469,7 @@ function writeError(message, exitCode = 1) {
|
|
|
4376
4469
|
process.exitCode = exitCode;
|
|
4377
4470
|
}
|
|
4378
4471
|
function handlePrCommandError(err, context, mode = "read") {
|
|
4472
|
+
if (reportCredentialStoreUnavailable(err)) return;
|
|
4379
4473
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4380
4474
|
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
4381
4475
|
const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
|
|
@@ -6086,6 +6180,7 @@ function writeError2(message) {
|
|
|
6086
6180
|
process.exitCode = 1;
|
|
6087
6181
|
}
|
|
6088
6182
|
function handlePipelineError(err, context) {
|
|
6183
|
+
if (reportCredentialStoreUnavailable(err)) return;
|
|
6089
6184
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
6090
6185
|
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
6091
6186
|
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
@@ -7145,6 +7240,7 @@ function formatRelationsList(workItemId, relations) {
|
|
|
7145
7240
|
return lines.join("\n");
|
|
7146
7241
|
}
|
|
7147
7242
|
function handleRelationError(err, id1) {
|
|
7243
|
+
if (reportCredentialStoreUnavailable(err)) process.exit(EXIT_CREDENTIAL_STORE_UNAVAILABLE);
|
|
7148
7244
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7149
7245
|
if (msg === "SELF_RELATION") {
|
|
7150
7246
|
process.stderr.write(`Error: cannot relate a work item to itself (#${id1}).
|