@kolatts/pncli 5.3.0 → 6.0.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 +2 -1
- package/dist/{chunk-2J2HAFKO.js → chunk-4FR4WGNR.js} +100 -13
- package/dist/chunk-4FR4WGNR.js.map +1 -0
- package/dist/chunk-WDWAPTVC.js +1001 -0
- package/dist/chunk-WDWAPTVC.js.map +1 -0
- package/dist/cli.js +3000 -493
- package/dist/cli.js.map +1 -1
- package/dist/{config-Y6IE6GY7.js → config-5LC7ZTEP.js} +4 -2
- package/dist/{http-V7PZD7LE.js → http-DJSGEGD6.js} +2 -2
- package/package.json +1 -1
- package/skills/pncli/SKILL.md +25 -6
- package/skills/pncli/ado.md +4 -0
- package/skills/pncli/bitbucket.md +4 -0
- package/skills/pncli/github.md +37 -0
- package/skills/pncli/jira.md +45 -1
- package/skills/pncli/marketplace.md +53 -1
- package/skills/pncli/saucelabs.md +175 -0
- package/skills/pncli/skills-guide.md +169 -0
- package/dist/chunk-2J2HAFKO.js.map +0 -1
- package/dist/chunk-QHJ2HM5J.js +0 -466
- package/dist/chunk-QHJ2HM5J.js.map +0 -1
- /package/dist/{config-Y6IE6GY7.js.map → config-5LC7ZTEP.js.map} +0 -0
- /package/dist/{http-V7PZD7LE.js.map → http-DJSGEGD6.js.map} +0 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PncliError
|
|
4
|
+
} from "./chunk-HRNOAQDN.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/config.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
|
|
12
|
+
// src/types/jira.ts
|
|
13
|
+
var CUSTOM_FIELD_TYPES = [
|
|
14
|
+
"string",
|
|
15
|
+
"number",
|
|
16
|
+
"date",
|
|
17
|
+
"datetime",
|
|
18
|
+
"select",
|
|
19
|
+
"multi-select",
|
|
20
|
+
"option-id",
|
|
21
|
+
"cascading-select",
|
|
22
|
+
"user",
|
|
23
|
+
"labels",
|
|
24
|
+
"url",
|
|
25
|
+
"textarea"
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// src/lib/validate-custom-fields.ts
|
|
29
|
+
var EXAMPLE = '[{"id":"customfield_10032","name":"Epic Link","type":"select"}]';
|
|
30
|
+
function assertValidCustomFields(fields, opts = {}) {
|
|
31
|
+
const where = opts.source ? `in ${opts.source}` : "config";
|
|
32
|
+
const fix = (value) => `Fix with: pncli config set jira.customFields '${value}'${opts.repo ? " --repo" : ""}`;
|
|
33
|
+
if (!Array.isArray(fields)) {
|
|
34
|
+
throw new PncliError(
|
|
35
|
+
`Invalid jira.customFields ${where}: expected an array, got ${JSON.stringify(fields)}. ${fix("[]")}`,
|
|
36
|
+
1
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
for (const f of fields) {
|
|
40
|
+
const entry = f;
|
|
41
|
+
if (typeof entry !== "object" || entry === null || typeof entry.id !== "string" || typeof entry.name !== "string") {
|
|
42
|
+
throw new PncliError(
|
|
43
|
+
`Invalid jira.customFields ${where}: each entry must be an object with string "id" and "name" fields. Got: ${JSON.stringify(f)}. ${fix(EXAMPLE)}`,
|
|
44
|
+
1
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (entry.type !== void 0 && (typeof entry.type !== "string" || !CUSTOM_FIELD_TYPES.includes(entry.type))) {
|
|
48
|
+
throw new PncliError(
|
|
49
|
+
`Invalid jira.customFields ${where}: entry "${entry.name}" has unknown "type" ${JSON.stringify(entry.type)}; expected one of ${CUSTOM_FIELD_TYPES.join(", ")}. ${fix(EXAMPLE)}`,
|
|
50
|
+
1
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/lib/keychain.ts
|
|
57
|
+
import { spawnSync } from "child_process";
|
|
58
|
+
var KEYCHAIN_PREFIX = "keychain:";
|
|
59
|
+
var KEYCHAIN_SERVICE = "pncli";
|
|
60
|
+
var ACCOUNT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
61
|
+
var defaultRunner = (command, args, input) => {
|
|
62
|
+
const r = spawnSync(command, args, { input, encoding: "utf8", windowsHide: true, timeout: 3e4 });
|
|
63
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", error: r.error };
|
|
64
|
+
};
|
|
65
|
+
var KeychainError = class extends Error {
|
|
66
|
+
};
|
|
67
|
+
var KeychainBatchError = class extends KeychainError {
|
|
68
|
+
constructor(partial, errors) {
|
|
69
|
+
super(Object.entries(errors).map(([a, e]) => `${a}: ${e}`).join("; "));
|
|
70
|
+
this.partial = partial;
|
|
71
|
+
this.errors = errors;
|
|
72
|
+
}
|
|
73
|
+
partial;
|
|
74
|
+
errors;
|
|
75
|
+
};
|
|
76
|
+
function lookupEach(accounts, lookup) {
|
|
77
|
+
const out = {};
|
|
78
|
+
const errors = {};
|
|
79
|
+
for (const account of accounts) {
|
|
80
|
+
try {
|
|
81
|
+
out[account] = lookup(account);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
errors[account] = err instanceof Error ? err.message : String(err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (Object.keys(errors).length > 0) throw new KeychainBatchError(out, errors);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function isKeychainRef(value) {
|
|
90
|
+
return typeof value === "string" && value.startsWith(KEYCHAIN_PREFIX);
|
|
91
|
+
}
|
|
92
|
+
function keychainRef(account) {
|
|
93
|
+
assertValidAccount(account);
|
|
94
|
+
return `${KEYCHAIN_PREFIX}${account}`;
|
|
95
|
+
}
|
|
96
|
+
function accountFromRef(ref) {
|
|
97
|
+
const account = ref.slice(KEYCHAIN_PREFIX.length);
|
|
98
|
+
assertValidAccount(account);
|
|
99
|
+
return account;
|
|
100
|
+
}
|
|
101
|
+
function assertValidAccount(account) {
|
|
102
|
+
if (!ACCOUNT_PATTERN.test(account)) {
|
|
103
|
+
throw new KeychainError(`Invalid keychain account name "${account}" \u2014 use letters, digits, '.', '_' or '-' (max 128 chars).`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function assertStorableSecret(secret) {
|
|
107
|
+
if (!secret) throw new KeychainError("Refusing to store an empty secret.");
|
|
108
|
+
if (/[\r\n]/.test(secret)) throw new KeychainError("Secrets containing newlines cannot be stored in the keychain.");
|
|
109
|
+
}
|
|
110
|
+
function failure(backend, action, r) {
|
|
111
|
+
const detail = r.error?.message ?? (r.stderr.trim() || `exit code ${r.status}`);
|
|
112
|
+
return new KeychainError(`${backend}: could not ${action}: ${detail}`);
|
|
113
|
+
}
|
|
114
|
+
function macosBackend(run = defaultRunner) {
|
|
115
|
+
return {
|
|
116
|
+
name: "macos",
|
|
117
|
+
store: "macOS login Keychain",
|
|
118
|
+
available() {
|
|
119
|
+
const r = run("security", ["help"]);
|
|
120
|
+
return !r.error;
|
|
121
|
+
},
|
|
122
|
+
getMany(accounts) {
|
|
123
|
+
return lookupEach(accounts, (account) => {
|
|
124
|
+
assertValidAccount(account);
|
|
125
|
+
const r = run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"]);
|
|
126
|
+
if (r.status === 44) return null;
|
|
127
|
+
if (r.status !== 0) throw failure("macOS Keychain", `read "${account}"`, r);
|
|
128
|
+
return r.stdout.replace(/\r?\n$/, "");
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
set(account, secret) {
|
|
132
|
+
assertValidAccount(account);
|
|
133
|
+
assertStorableSecret(secret);
|
|
134
|
+
const hex = Buffer.from(secret, "utf8").toString("hex");
|
|
135
|
+
const r = run("security", ["-i"], `add-generic-password -U -s ${KEYCHAIN_SERVICE} -a ${account} -l "pncli ${account}" -X ${hex}
|
|
136
|
+
`);
|
|
137
|
+
if (r.status !== 0 || /error/i.test(r.stderr)) throw failure("macOS Keychain", `store "${account}"`, r);
|
|
138
|
+
},
|
|
139
|
+
delete(account) {
|
|
140
|
+
assertValidAccount(account);
|
|
141
|
+
const r = run("security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account]);
|
|
142
|
+
if (r.status === 44) return false;
|
|
143
|
+
if (r.status !== 0) throw failure("macOS Keychain", `delete "${account}"`, r);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
var WINDOWS_CREDMAN_SCRIPT = String.raw`
|
|
149
|
+
$ErrorActionPreference = 'Stop'
|
|
150
|
+
# Redirected console streams default to the OEM code page; secrets are UTF-8 JSON both ways.
|
|
151
|
+
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding $false
|
|
152
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
|
153
|
+
Add-Type -TypeDefinition @'
|
|
154
|
+
using System;
|
|
155
|
+
using System.Runtime.InteropServices;
|
|
156
|
+
using System.Text;
|
|
157
|
+
public static class PncliCredMan {
|
|
158
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
159
|
+
private struct CREDENTIAL {
|
|
160
|
+
public int Flags; public int Type; public string TargetName; public string Comment;
|
|
161
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
162
|
+
public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist;
|
|
163
|
+
public int AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName;
|
|
164
|
+
}
|
|
165
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
166
|
+
private static extern bool CredReadW(string target, int type, int flags, out IntPtr cred);
|
|
167
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
168
|
+
private static extern bool CredWriteW(ref CREDENTIAL cred, int flags);
|
|
169
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
170
|
+
private static extern bool CredDeleteW(string target, int type, int flags);
|
|
171
|
+
[DllImport("advapi32.dll")]
|
|
172
|
+
private static extern void CredFree(IntPtr cred);
|
|
173
|
+
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
174
|
+
private static extern bool CredEnumerateW(string filter, int flags, out int count, out IntPtr creds);
|
|
175
|
+
private const int NotFound = 1168;
|
|
176
|
+
public static string Read(string target) {
|
|
177
|
+
IntPtr p;
|
|
178
|
+
if (!CredReadW(target, 1, 0, out p)) {
|
|
179
|
+
int err = Marshal.GetLastWin32Error();
|
|
180
|
+
if (err == NotFound) return null;
|
|
181
|
+
throw new System.ComponentModel.Win32Exception(err);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));
|
|
185
|
+
if (c.CredentialBlobSize == 0) return "";
|
|
186
|
+
byte[] bytes = new byte[c.CredentialBlobSize];
|
|
187
|
+
Marshal.Copy(c.CredentialBlob, bytes, 0, c.CredentialBlobSize);
|
|
188
|
+
return Encoding.UTF8.GetString(bytes);
|
|
189
|
+
} finally { CredFree(p); }
|
|
190
|
+
}
|
|
191
|
+
public static void Write(string target, string user, string secret) {
|
|
192
|
+
byte[] bytes = Encoding.UTF8.GetBytes(secret);
|
|
193
|
+
CREDENTIAL c = new CREDENTIAL();
|
|
194
|
+
c.Type = 1; c.TargetName = target; c.UserName = user; c.Persist = 2;
|
|
195
|
+
c.CredentialBlobSize = bytes.Length;
|
|
196
|
+
c.CredentialBlob = Marshal.AllocHGlobal(bytes.Length);
|
|
197
|
+
try {
|
|
198
|
+
Marshal.Copy(bytes, 0, c.CredentialBlob, bytes.Length);
|
|
199
|
+
if (!CredWriteW(ref c, 0)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
200
|
+
} finally { Marshal.FreeHGlobal(c.CredentialBlob); }
|
|
201
|
+
}
|
|
202
|
+
// Git Credential Manager writes UTF-16LE blobs; pncli and git-credential-wincred write UTF-8.
|
|
203
|
+
// Tokens are ASCII, so "every odd byte is zero" reliably identifies UTF-16.
|
|
204
|
+
private static string Decode(byte[] bytes) {
|
|
205
|
+
bool utf16 = bytes.Length % 2 == 0 && bytes.Length > 0;
|
|
206
|
+
for (int i = 1; utf16 && i < bytes.Length; i += 2) if (bytes[i] != 0) utf16 = false;
|
|
207
|
+
return utf16 ? Encoding.Unicode.GetString(bytes) : Encoding.UTF8.GetString(bytes);
|
|
208
|
+
}
|
|
209
|
+
// Flattened [target, user, secret] triples for every credential matching the filter (e.g. "git:*").
|
|
210
|
+
public static string[] List(string filter) {
|
|
211
|
+
int count; IntPtr list;
|
|
212
|
+
if (!CredEnumerateW(filter, 0, out count, out list)) {
|
|
213
|
+
int err = Marshal.GetLastWin32Error();
|
|
214
|
+
if (err == NotFound) return new string[0];
|
|
215
|
+
throw new System.ComponentModel.Win32Exception(err);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
string[] outp = new string[count * 3];
|
|
219
|
+
for (int i = 0; i < count; i++) {
|
|
220
|
+
IntPtr item = Marshal.ReadIntPtr(list, i * IntPtr.Size);
|
|
221
|
+
CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(item, typeof(CREDENTIAL));
|
|
222
|
+
string secret = "";
|
|
223
|
+
if (c.CredentialBlobSize > 0) {
|
|
224
|
+
byte[] bytes = new byte[c.CredentialBlobSize];
|
|
225
|
+
Marshal.Copy(c.CredentialBlob, bytes, 0, c.CredentialBlobSize);
|
|
226
|
+
secret = Decode(bytes);
|
|
227
|
+
}
|
|
228
|
+
outp[i * 3] = c.TargetName; outp[i * 3 + 1] = c.UserName; outp[i * 3 + 2] = secret;
|
|
229
|
+
}
|
|
230
|
+
return outp;
|
|
231
|
+
} finally { CredFree(list); }
|
|
232
|
+
}
|
|
233
|
+
public static bool Delete(string target) {
|
|
234
|
+
if (CredDeleteW(target, 1, 0)) return true;
|
|
235
|
+
int err = Marshal.GetLastWin32Error();
|
|
236
|
+
if (err == NotFound) return false;
|
|
237
|
+
throw new System.ComponentModel.Win32Exception(err);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
'@
|
|
241
|
+
$req = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
242
|
+
switch ($req.op) {
|
|
243
|
+
'get' {
|
|
244
|
+
$out = @{}
|
|
245
|
+
foreach ($t in $req.targets) { $out[$t] = [PncliCredMan]::Read($t) }
|
|
246
|
+
[Console]::Out.Write((ConvertTo-Json -InputObject $out -Compress))
|
|
247
|
+
}
|
|
248
|
+
'set' { [PncliCredMan]::Write($req.target, $req.user, $req.secret); [Console]::Out.Write('{"ok":true}') }
|
|
249
|
+
'delete' { $d = [PncliCredMan]::Delete($req.target); [Console]::Out.Write((ConvertTo-Json -InputObject @{ deleted = $d } -Compress)) }
|
|
250
|
+
'list' {
|
|
251
|
+
$flat = [PncliCredMan]::List($req.filter)
|
|
252
|
+
$items = @()
|
|
253
|
+
for ($i = 0; $i -lt $flat.Length; $i += 3) { $items += @{ target = $flat[$i]; user = $flat[$i + 1]; secret = $flat[$i + 2] } }
|
|
254
|
+
[Console]::Out.Write((ConvertTo-Json -InputObject @($items) -Compress -Depth 3))
|
|
255
|
+
}
|
|
256
|
+
'ping' { [Console]::Out.Write('{"ok":true}') }
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
function windowsListCredentials(filter, run = defaultRunner) {
|
|
260
|
+
const encoded = Buffer.from(WINDOWS_CREDMAN_SCRIPT, "utf16le").toString("base64");
|
|
261
|
+
const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], JSON.stringify({ op: "list", filter }));
|
|
262
|
+
if (r.status !== 0) throw failure("Windows Credential Manager", "list credentials", r);
|
|
263
|
+
const parsed = JSON.parse(r.stdout.trim() || "[]");
|
|
264
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
265
|
+
return items.filter((x) => !!x && typeof x === "object").map((x) => ({ target: String(x.target ?? ""), user: String(x.user ?? ""), secret: String(x.secret ?? "") }));
|
|
266
|
+
}
|
|
267
|
+
function windowsTarget(account) {
|
|
268
|
+
return `${KEYCHAIN_SERVICE}:${account}`;
|
|
269
|
+
}
|
|
270
|
+
function windowsBackend(run = defaultRunner) {
|
|
271
|
+
const encoded = Buffer.from(WINDOWS_CREDMAN_SCRIPT, "utf16le").toString("base64");
|
|
272
|
+
const invoke = (payload, action) => {
|
|
273
|
+
const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], JSON.stringify(payload));
|
|
274
|
+
if (r.status !== 0) throw failure("Windows Credential Manager", action, r);
|
|
275
|
+
try {
|
|
276
|
+
return JSON.parse(r.stdout.trim() || "{}");
|
|
277
|
+
} catch {
|
|
278
|
+
throw new KeychainError(`Windows Credential Manager: unexpected output while trying to ${action}.`);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
name: "windows",
|
|
283
|
+
store: "Windows Credential Manager",
|
|
284
|
+
available() {
|
|
285
|
+
try {
|
|
286
|
+
invoke({ op: "ping" }, "start PowerShell");
|
|
287
|
+
return true;
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
getMany(accounts) {
|
|
293
|
+
accounts.forEach(assertValidAccount);
|
|
294
|
+
if (accounts.length === 0) return {};
|
|
295
|
+
const raw = invoke({ op: "get", targets: accounts.map(windowsTarget) }, "read credentials");
|
|
296
|
+
return Object.fromEntries(accounts.map((a) => [a, raw[windowsTarget(a)] ?? null]));
|
|
297
|
+
},
|
|
298
|
+
set(account, secret) {
|
|
299
|
+
assertValidAccount(account);
|
|
300
|
+
assertStorableSecret(secret);
|
|
301
|
+
invoke({ op: "set", target: windowsTarget(account), user: account, secret }, `store "${account}"`);
|
|
302
|
+
},
|
|
303
|
+
delete(account) {
|
|
304
|
+
assertValidAccount(account);
|
|
305
|
+
const r = invoke({ op: "delete", target: windowsTarget(account) }, `delete "${account}"`);
|
|
306
|
+
return !!r.deleted;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function linuxBackend(run = defaultRunner) {
|
|
311
|
+
return {
|
|
312
|
+
name: "linux",
|
|
313
|
+
store: "Secret Service (secret-tool)",
|
|
314
|
+
available() {
|
|
315
|
+
const r = run("secret-tool", ["--version"]);
|
|
316
|
+
return !r.error;
|
|
317
|
+
},
|
|
318
|
+
getMany(accounts) {
|
|
319
|
+
return lookupEach(accounts, (account) => {
|
|
320
|
+
assertValidAccount(account);
|
|
321
|
+
const r = run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "account", account]);
|
|
322
|
+
if (r.error) throw failure("Secret Service", `read "${account}"`, r);
|
|
323
|
+
if (r.status !== 0) return null;
|
|
324
|
+
return r.stdout.replace(/\r?\n$/, "");
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
set(account, secret) {
|
|
328
|
+
assertValidAccount(account);
|
|
329
|
+
assertStorableSecret(secret);
|
|
330
|
+
const r = run("secret-tool", ["store", `--label=pncli ${account}`, "service", KEYCHAIN_SERVICE, "account", account], secret);
|
|
331
|
+
if (r.status !== 0) throw failure("Secret Service", `store "${account}"`, r);
|
|
332
|
+
},
|
|
333
|
+
delete(account) {
|
|
334
|
+
assertValidAccount(account);
|
|
335
|
+
const existing = this.getMany([account])[account];
|
|
336
|
+
if (existing === null) return false;
|
|
337
|
+
const r = run("secret-tool", ["clear", "service", KEYCHAIN_SERVICE, "account", account]);
|
|
338
|
+
if (r.status !== 0) throw failure("Secret Service", `delete "${account}"`, r);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function noneBackend() {
|
|
344
|
+
const unsupported = () => {
|
|
345
|
+
throw new KeychainError(`No OS keychain backend is available on ${process.platform}. Set PNCLI_KEYCHAIN_BACKEND to macos, windows, or linux if one should be.`);
|
|
346
|
+
};
|
|
347
|
+
return { name: "none", store: "none", available: () => false, getMany: unsupported, set: unsupported, delete: unsupported };
|
|
348
|
+
}
|
|
349
|
+
function getKeychainBackend(run = defaultRunner, platform = process.platform) {
|
|
350
|
+
const override = process.env.PNCLI_KEYCHAIN_BACKEND || void 0;
|
|
351
|
+
const name = override ?? (platform === "darwin" ? "macos" : platform === "win32" ? "windows" : platform === "linux" ? "linux" : "none");
|
|
352
|
+
switch (name) {
|
|
353
|
+
case "macos":
|
|
354
|
+
return macosBackend(run);
|
|
355
|
+
case "windows":
|
|
356
|
+
return windowsBackend(run);
|
|
357
|
+
case "linux":
|
|
358
|
+
return linuxBackend(run);
|
|
359
|
+
case "none":
|
|
360
|
+
return noneBackend();
|
|
361
|
+
default:
|
|
362
|
+
throw new KeychainError(`Unknown PNCLI_KEYCHAIN_BACKEND "${String(override)}" \u2014 use macos, windows, linux, or none.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
var SECRET_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
366
|
+
"token",
|
|
367
|
+
"apiToken",
|
|
368
|
+
"pat",
|
|
369
|
+
"apiKey",
|
|
370
|
+
"clientSecret",
|
|
371
|
+
"serviceKey",
|
|
372
|
+
"passcode",
|
|
373
|
+
"refreshToken",
|
|
374
|
+
"adminApiKey",
|
|
375
|
+
"platformToken",
|
|
376
|
+
"password",
|
|
377
|
+
"connection",
|
|
378
|
+
"accessKey"
|
|
379
|
+
]);
|
|
380
|
+
function findConfigStrings(root, predicate, prefix = []) {
|
|
381
|
+
const found = [];
|
|
382
|
+
if (Array.isArray(root)) {
|
|
383
|
+
root.forEach((item, i) => found.push(...findConfigStrings(item, predicate, [...prefix, i])));
|
|
384
|
+
} else if (root && typeof root === "object") {
|
|
385
|
+
for (const [key, value] of Object.entries(root)) {
|
|
386
|
+
if (typeof value === "string") {
|
|
387
|
+
if (predicate(key, value)) found.push({ path: [...prefix, key], value });
|
|
388
|
+
} else {
|
|
389
|
+
found.push(...findConfigStrings(value, predicate, [...prefix, key]));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return found;
|
|
394
|
+
}
|
|
395
|
+
function purgeEntries(config, backend, accounts) {
|
|
396
|
+
const stillReferenced = new Set(findKeychainRefs(config).map((r) => r.value.slice(KEYCHAIN_PREFIX.length)));
|
|
397
|
+
const purged = [];
|
|
398
|
+
for (const account of new Set(accounts)) {
|
|
399
|
+
if (stillReferenced.has(account)) continue;
|
|
400
|
+
if (backend.delete(account)) purged.push(account);
|
|
401
|
+
}
|
|
402
|
+
return purged;
|
|
403
|
+
}
|
|
404
|
+
function findKeychainRefs(root) {
|
|
405
|
+
return findConfigStrings(root, (_k, v) => isKeychainRef(v));
|
|
406
|
+
}
|
|
407
|
+
function findPlaintextSecrets(root) {
|
|
408
|
+
return findConfigStrings(root, (k, v) => typeof k === "string" && SECRET_FIELD_NAMES.has(k) && v.length > 0 && !isKeychainRef(v));
|
|
409
|
+
}
|
|
410
|
+
function accountForPath(root, segments) {
|
|
411
|
+
const parts = [];
|
|
412
|
+
let node = root;
|
|
413
|
+
for (const seg of segments) {
|
|
414
|
+
if (typeof seg === "number") {
|
|
415
|
+
const entry = Array.isArray(node) ? node[seg] : void 0;
|
|
416
|
+
const name = entry && typeof entry === "object" ? entry.name : void 0;
|
|
417
|
+
parts.push(typeof name === "string" && /^[A-Za-z0-9._-]+$/.test(name) ? name : String(seg));
|
|
418
|
+
} else {
|
|
419
|
+
parts.push(seg);
|
|
420
|
+
}
|
|
421
|
+
node = node && typeof node === "object" ? node[seg] : void 0;
|
|
422
|
+
}
|
|
423
|
+
return parts.join(".");
|
|
424
|
+
}
|
|
425
|
+
function setAtPath(root, segments, value) {
|
|
426
|
+
let node = root;
|
|
427
|
+
for (const seg of segments.slice(0, -1)) node = node[seg];
|
|
428
|
+
node[segments[segments.length - 1]] = value;
|
|
429
|
+
}
|
|
430
|
+
var resolvedCache = /* @__PURE__ */ new Map();
|
|
431
|
+
var lastUnresolved = [];
|
|
432
|
+
var warnedUnresolved = false;
|
|
433
|
+
function getUnresolvedKeychainRefs() {
|
|
434
|
+
return lastUnresolved;
|
|
435
|
+
}
|
|
436
|
+
function resetKeychainCache() {
|
|
437
|
+
resolvedCache.clear();
|
|
438
|
+
lastUnresolved = [];
|
|
439
|
+
warnedUnresolved = false;
|
|
440
|
+
}
|
|
441
|
+
function resolveKeychainRefs(config, backend, opts = {}) {
|
|
442
|
+
const all = findKeychainRefs(config);
|
|
443
|
+
if (all.length === 0) return config;
|
|
444
|
+
const copy = JSON.parse(JSON.stringify(config));
|
|
445
|
+
const refs = all.filter((r) => {
|
|
446
|
+
const key = r.path.join(".");
|
|
447
|
+
if (!opts.skipPaths?.has(key) && (!opts.onlyPaths || opts.onlyPaths.has(key))) return true;
|
|
448
|
+
setAtPath(copy, r.path, void 0);
|
|
449
|
+
return false;
|
|
450
|
+
});
|
|
451
|
+
if (refs.length === 0) return copy;
|
|
452
|
+
const unresolved = [];
|
|
453
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const ref of refs) {
|
|
455
|
+
try {
|
|
456
|
+
const account = accountFromRef(ref.value);
|
|
457
|
+
accounts.set(account, [...accounts.get(account) ?? [], ref.path]);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
unresolved.push({ path: ref.path.join("."), account: ref.value.slice(KEYCHAIN_PREFIX.length), reason: err.message });
|
|
460
|
+
setAtPath(copy, ref.path, void 0);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const activeBackend = backend ?? getKeychainBackend();
|
|
464
|
+
if (activeBackend.name === "none") {
|
|
465
|
+
for (const [account, paths] of accounts) {
|
|
466
|
+
for (const p of paths) {
|
|
467
|
+
setAtPath(copy, p, void 0);
|
|
468
|
+
unresolved.push({ path: p.join("."), account, reason: "keychain disabled (PNCLI_KEYCHAIN_BACKEND=none or unsupported platform)" });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
lastUnresolved = unresolved;
|
|
472
|
+
return copy;
|
|
473
|
+
}
|
|
474
|
+
const missing = [...accounts.keys()].filter((a) => !resolvedCache.has(a));
|
|
475
|
+
if (missing.length > 0) {
|
|
476
|
+
let failed = {};
|
|
477
|
+
try {
|
|
478
|
+
const found = activeBackend.getMany(missing);
|
|
479
|
+
for (const a of missing) resolvedCache.set(a, found[a] ?? null);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err instanceof KeychainBatchError) {
|
|
482
|
+
for (const a of missing) if (!(a in err.errors)) resolvedCache.set(a, err.partial[a] ?? null);
|
|
483
|
+
failed = err.errors;
|
|
484
|
+
} else {
|
|
485
|
+
failed = Object.fromEntries(missing.map((a) => [a, err.message]));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const [account, paths] of accounts) {
|
|
489
|
+
if (resolvedCache.has(account) || !(account in failed)) continue;
|
|
490
|
+
for (const p of paths) unresolved.push({ path: p.join("."), account, reason: failed[account] });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
for (const [account, paths] of accounts) {
|
|
494
|
+
const secret = resolvedCache.get(account);
|
|
495
|
+
for (const p of paths) {
|
|
496
|
+
if (secret != null) {
|
|
497
|
+
setAtPath(copy, p, secret);
|
|
498
|
+
} else {
|
|
499
|
+
setAtPath(copy, p, void 0);
|
|
500
|
+
if (resolvedCache.has(account)) unresolved.push({ path: p.join("."), account, reason: "no entry in the keychain" });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
lastUnresolved = unresolved;
|
|
505
|
+
if (unresolved.length > 0 && !warnedUnresolved) {
|
|
506
|
+
warnedUnresolved = true;
|
|
507
|
+
process.stderr.write(`Warning: ${unresolved.length} keychain reference(s) could not be resolved (${unresolved.map((u) => u.path).join(", ")}). Run: pncli doctor
|
|
508
|
+
`);
|
|
509
|
+
}
|
|
510
|
+
return copy;
|
|
511
|
+
}
|
|
512
|
+
function storeVerified(backend, account, secret) {
|
|
513
|
+
backend.set(account, secret);
|
|
514
|
+
let readBack;
|
|
515
|
+
try {
|
|
516
|
+
readBack = backend.getMany([account])[account];
|
|
517
|
+
} catch (err) {
|
|
518
|
+
throw new KeychainError(`Stored "${account}" but could not read it back to verify: ${err instanceof Error ? err.message : String(err)}`);
|
|
519
|
+
}
|
|
520
|
+
if (readBack !== secret) throw new KeychainError(`Stored "${account}" but reading it back did not return the same secret \u2014 config was not changed.`);
|
|
521
|
+
}
|
|
522
|
+
function resolveSecretValue(value, backend) {
|
|
523
|
+
if (!isKeychainRef(value)) return value;
|
|
524
|
+
const resolved = resolveKeychainRefs({ v: value }, backend);
|
|
525
|
+
return resolved.v;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/lib/config.ts
|
|
529
|
+
var ENV_KEYS = {
|
|
530
|
+
EMAIL: "PNCLI_EMAIL",
|
|
531
|
+
USERID: "PNCLI_USERID",
|
|
532
|
+
JIRA_BASE_URL: "PNCLI_JIRA_BASE_URL",
|
|
533
|
+
JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
|
|
534
|
+
BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
|
|
535
|
+
BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
|
|
536
|
+
GITHUB_BASE_URL: "PNCLI_GITHUB_BASE_URL",
|
|
537
|
+
GITHUB_TOKEN: "PNCLI_GITHUB_TOKEN",
|
|
538
|
+
// Well-known vars CI platforms provide without any pncli-specific setup.
|
|
539
|
+
GITHUB_TOKEN_FALLBACK: "GITHUB_TOKEN",
|
|
540
|
+
GITHUB_API_URL_FALLBACK: "GITHUB_API_URL",
|
|
541
|
+
CONFLUENCE_BASE_URL: "PNCLI_CONFLUENCE_BASE_URL",
|
|
542
|
+
CONFLUENCE_API_TOKEN: "PNCLI_CONFLUENCE_API_TOKEN",
|
|
543
|
+
ARTIFACTORY_BASE_URL: "PNCLI_ARTIFACTORY_BASE_URL",
|
|
544
|
+
ARTIFACTORY_TOKEN: "PNCLI_ARTIFACTORY_TOKEN",
|
|
545
|
+
ARTIFACTORY_REPO_NPM: "PNCLI_ARTIFACTORY_REPO_NPM",
|
|
546
|
+
ARTIFACTORY_REPO_NUGET: "PNCLI_ARTIFACTORY_REPO_NUGET",
|
|
547
|
+
ARTIFACTORY_REPO_MAVEN: "PNCLI_ARTIFACTORY_REPO_MAVEN",
|
|
548
|
+
SONAR_BASE_URL: "PNCLI_SONAR_BASE_URL",
|
|
549
|
+
SONAR_TOKEN: "PNCLI_SONAR_TOKEN",
|
|
550
|
+
// SonarSource's own scanner CLI standard var name — see the comment above GITHUB_TOKEN_FALLBACK.
|
|
551
|
+
SONAR_TOKEN_FALLBACK: "SONAR_TOKEN",
|
|
552
|
+
SDE_CONNECTION: "PNCLI_SDE_CONNECTION",
|
|
553
|
+
ADO_BASE_URL: "PNCLI_ADO_BASE_URL",
|
|
554
|
+
ADO_PAT: "PNCLI_ADO_PAT",
|
|
555
|
+
// Azure Pipelines' conventional name for $(System.AccessToken) — see the comment above GITHUB_TOKEN_FALLBACK.
|
|
556
|
+
ADO_PAT_FALLBACK: "SYSTEM_ACCESSTOKEN",
|
|
557
|
+
JENKINS_BASE_URL: "PNCLI_JENKINS_BASE_URL",
|
|
558
|
+
JENKINS_USERNAME: "PNCLI_JENKINS_USERNAME",
|
|
559
|
+
JENKINS_API_TOKEN: "PNCLI_JENKINS_API_TOKEN",
|
|
560
|
+
CHECKMARX_BASE_URL: "PNCLI_CHECKMARX_BASE_URL",
|
|
561
|
+
CHECKMARX_TENANT_NAME: "PNCLI_CHECKMARX_TENANT_NAME",
|
|
562
|
+
CHECKMARX_API_KEY: "PNCLI_CHECKMARX_API_KEY",
|
|
563
|
+
CHECKMARX_CLIENT_ID: "PNCLI_CHECKMARX_CLIENT_ID",
|
|
564
|
+
CHECKMARX_CLIENT_SECRET: "PNCLI_CHECKMARX_CLIENT_SECRET",
|
|
565
|
+
CONTRAST_BASE_URL: "PNCLI_CONTRAST_BASE_URL",
|
|
566
|
+
CONTRAST_ORG_UUID: "PNCLI_CONTRAST_ORG_UUID",
|
|
567
|
+
CONTRAST_API_KEY: "PNCLI_CONTRAST_API_KEY",
|
|
568
|
+
CONTRAST_SERVICE_KEY: "PNCLI_CONTRAST_SERVICE_KEY",
|
|
569
|
+
CONTRAST_USERNAME: "PNCLI_CONTRAST_USERNAME",
|
|
570
|
+
SONATYPEIQ_BASE_URL: "PNCLI_SONATYPEIQ_BASE_URL",
|
|
571
|
+
SONATYPEIQ_USER_CODE: "PNCLI_SONATYPEIQ_USER_CODE",
|
|
572
|
+
SONATYPEIQ_PASSCODE: "PNCLI_SONATYPEIQ_PASSCODE",
|
|
573
|
+
OPENSHIFT_BASE_URL: "PNCLI_OPENSHIFT_BASE_URL",
|
|
574
|
+
OPENSHIFT_TOKEN: "PNCLI_OPENSHIFT_TOKEN",
|
|
575
|
+
DYNATRACE_BASE_URL: "PNCLI_DYNATRACE_BASE_URL",
|
|
576
|
+
DYNATRACE_API_TOKEN: "PNCLI_DYNATRACE_API_TOKEN",
|
|
577
|
+
DYNATRACE_PLATFORM_URL: "PNCLI_DYNATRACE_PLATFORM_URL",
|
|
578
|
+
DYNATRACE_PLATFORM_TOKEN: "PNCLI_DYNATRACE_PLATFORM_TOKEN",
|
|
579
|
+
LOGSCALE_BASE_URL: "PNCLI_LOGSCALE_BASE_URL",
|
|
580
|
+
LOGSCALE_TOKEN: "PNCLI_LOGSCALE_TOKEN",
|
|
581
|
+
SPLITIO_BASE_URL: "PNCLI_SPLITIO_BASE_URL",
|
|
582
|
+
SPLITIO_ADMIN_API_KEY: "PNCLI_SPLITIO_ADMIN_API_KEY",
|
|
583
|
+
FIGMA_BASE_URL: "PNCLI_FIGMA_BASE_URL",
|
|
584
|
+
FIGMA_TOKEN: "PNCLI_FIGMA_TOKEN",
|
|
585
|
+
ALATION_BASE_URL: "PNCLI_ALATION_BASE_URL",
|
|
586
|
+
ALATION_REFRESH_TOKEN: "PNCLI_ALATION_REFRESH_TOKEN",
|
|
587
|
+
ALATION_USER_ID: "PNCLI_ALATION_USER_ID",
|
|
588
|
+
SAUCELABS_BASE_URL: "PNCLI_SAUCELABS_BASE_URL",
|
|
589
|
+
SAUCELABS_USERNAME: "PNCLI_SAUCELABS_USERNAME",
|
|
590
|
+
SAUCELABS_ACCESS_KEY: "PNCLI_SAUCELABS_ACCESS_KEY",
|
|
591
|
+
// The names saucectl, Sauce Connect and Sauce's own CI docs read — see the comment above GITHUB_TOKEN_FALLBACK.
|
|
592
|
+
SAUCELABS_USERNAME_FALLBACK: "SAUCE_USERNAME",
|
|
593
|
+
SAUCELABS_ACCESS_KEY_FALLBACK: "SAUCE_ACCESS_KEY",
|
|
594
|
+
CONFIG_PATH: "PNCLI_CONFIG_PATH"
|
|
595
|
+
};
|
|
596
|
+
function getGlobalConfigPath(overridePath) {
|
|
597
|
+
if (overridePath) return overridePath;
|
|
598
|
+
const envPath = process.env[ENV_KEYS.CONFIG_PATH];
|
|
599
|
+
if (envPath) return envPath;
|
|
600
|
+
return path.join(os.homedir(), ".pncli", "config.json");
|
|
601
|
+
}
|
|
602
|
+
function loadJsonFile(filePath) {
|
|
603
|
+
try {
|
|
604
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
605
|
+
return JSON.parse(content);
|
|
606
|
+
} catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function getRepoRoot() {
|
|
611
|
+
try {
|
|
612
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
613
|
+
} catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function parseSdeConnection(connection) {
|
|
618
|
+
const idx = connection.indexOf("@");
|
|
619
|
+
if (idx <= 0) return null;
|
|
620
|
+
const token = connection.slice(0, idx);
|
|
621
|
+
const host = connection.slice(idx + 1);
|
|
622
|
+
const baseUrl = /^https?:\/\//i.test(host) ? host : `https://${host}`;
|
|
623
|
+
return { token, baseUrl };
|
|
624
|
+
}
|
|
625
|
+
function mergeCustomFields(global, repo) {
|
|
626
|
+
const map = /* @__PURE__ */ new Map();
|
|
627
|
+
if (global != null) {
|
|
628
|
+
assertValidCustomFields(global, { source: "global config" });
|
|
629
|
+
for (const f of global) map.set(f.id, f);
|
|
630
|
+
}
|
|
631
|
+
if (repo != null) {
|
|
632
|
+
assertValidCustomFields(repo, { source: "repo config (.pncli.json)", repo: true });
|
|
633
|
+
for (const f of repo) map.set(f.id, f);
|
|
634
|
+
}
|
|
635
|
+
return Array.from(map.values());
|
|
636
|
+
}
|
|
637
|
+
function mergeDefaults(global, repo) {
|
|
638
|
+
return {
|
|
639
|
+
jira: { ...global?.jira, ...repo?.jira },
|
|
640
|
+
bitbucket: { ...global?.bitbucket, ...repo?.bitbucket },
|
|
641
|
+
github: { ...global?.github, ...repo?.github },
|
|
642
|
+
sonar: { ...global?.sonar, ...repo?.sonar },
|
|
643
|
+
sde: { ...global?.sde, ...repo?.sde },
|
|
644
|
+
ado: { ...global?.ado, ...repo?.ado },
|
|
645
|
+
jenkins: { ...global?.jenkins, ...repo?.jenkins }
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
var SECRET_ENV_OVERRIDES = [
|
|
649
|
+
["jira.apiToken", [ENV_KEYS.JIRA_API_TOKEN]],
|
|
650
|
+
["bitbucket.pat", [ENV_KEYS.BITBUCKET_PAT]],
|
|
651
|
+
["github.token", [ENV_KEYS.GITHUB_TOKEN, ENV_KEYS.GITHUB_TOKEN_FALLBACK]],
|
|
652
|
+
["confluence.apiToken", [ENV_KEYS.CONFLUENCE_API_TOKEN]],
|
|
653
|
+
["artifactory.token", [ENV_KEYS.ARTIFACTORY_TOKEN]],
|
|
654
|
+
["sonar.token", [ENV_KEYS.SONAR_TOKEN, ENV_KEYS.SONAR_TOKEN_FALLBACK]],
|
|
655
|
+
["ado.pat", [ENV_KEYS.ADO_PAT, ENV_KEYS.ADO_PAT_FALLBACK]],
|
|
656
|
+
["jenkins.apiToken", [ENV_KEYS.JENKINS_API_TOKEN]],
|
|
657
|
+
["checkmarx.apiKey", [ENV_KEYS.CHECKMARX_API_KEY]],
|
|
658
|
+
["checkmarx.clientSecret", [ENV_KEYS.CHECKMARX_CLIENT_SECRET]],
|
|
659
|
+
["contrast.apiKey", [ENV_KEYS.CONTRAST_API_KEY]],
|
|
660
|
+
["contrast.serviceKey", [ENV_KEYS.CONTRAST_SERVICE_KEY]],
|
|
661
|
+
["sonatypeiq.passcode", [ENV_KEYS.SONATYPEIQ_PASSCODE]],
|
|
662
|
+
["openshift.token", [ENV_KEYS.OPENSHIFT_TOKEN]],
|
|
663
|
+
["dynatrace.apiToken", [ENV_KEYS.DYNATRACE_API_TOKEN]],
|
|
664
|
+
["dynatrace.platformToken", [ENV_KEYS.DYNATRACE_PLATFORM_TOKEN]],
|
|
665
|
+
["logscale.token", [ENV_KEYS.LOGSCALE_TOKEN]],
|
|
666
|
+
["splitio.adminApiKey", [ENV_KEYS.SPLITIO_ADMIN_API_KEY]],
|
|
667
|
+
["figma.token", [ENV_KEYS.FIGMA_TOKEN]],
|
|
668
|
+
["alation.refreshToken", [ENV_KEYS.ALATION_REFRESH_TOKEN]],
|
|
669
|
+
["saucelabs.accessKey", [ENV_KEYS.SAUCELABS_ACCESS_KEY, ENV_KEYS.SAUCELABS_ACCESS_KEY_FALLBACK]]
|
|
670
|
+
];
|
|
671
|
+
function envOverriddenSecretPaths(env = process.env) {
|
|
672
|
+
return new Set(SECRET_ENV_OVERRIDES.filter(([, keys]) => keys.some((k) => env[k] !== void 0)).map(([p]) => p));
|
|
673
|
+
}
|
|
674
|
+
function loadConfig(opts = {}) {
|
|
675
|
+
const globalConfigPath = getGlobalConfigPath(opts.configPath);
|
|
676
|
+
const globalConfig = resolveKeychainRefs(loadJsonFile(globalConfigPath) ?? {}, void 0, {
|
|
677
|
+
skipPaths: envOverriddenSecretPaths(),
|
|
678
|
+
...opts.keychainPaths ? { onlyPaths: new Set(opts.keychainPaths) } : {}
|
|
679
|
+
});
|
|
680
|
+
const repoRoot = getRepoRoot();
|
|
681
|
+
let repoConfig = {};
|
|
682
|
+
if (repoRoot) {
|
|
683
|
+
repoConfig = loadJsonFile(path.join(repoRoot, ".pncli.json")) ?? {};
|
|
684
|
+
}
|
|
685
|
+
const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);
|
|
686
|
+
return {
|
|
687
|
+
user: {
|
|
688
|
+
email: process.env[ENV_KEYS.EMAIL] ?? globalConfig.user?.email,
|
|
689
|
+
userId: process.env[ENV_KEYS.USERID] ?? globalConfig.user?.userId
|
|
690
|
+
},
|
|
691
|
+
jira: {
|
|
692
|
+
baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,
|
|
693
|
+
apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,
|
|
694
|
+
customFields: mergeCustomFields(globalConfig.jira?.customFields, repoConfig.jira?.customFields)
|
|
695
|
+
},
|
|
696
|
+
bitbucket: {
|
|
697
|
+
baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
|
|
698
|
+
pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
|
|
699
|
+
},
|
|
700
|
+
github: {
|
|
701
|
+
baseUrl: process.env[ENV_KEYS.GITHUB_BASE_URL] ?? process.env[ENV_KEYS.GITHUB_API_URL_FALLBACK] ?? globalConfig.github?.baseUrl,
|
|
702
|
+
token: process.env[ENV_KEYS.GITHUB_TOKEN] ?? process.env[ENV_KEYS.GITHUB_TOKEN_FALLBACK] ?? globalConfig.github?.token
|
|
703
|
+
},
|
|
704
|
+
confluence: {
|
|
705
|
+
baseUrl: process.env[ENV_KEYS.CONFLUENCE_BASE_URL] ?? globalConfig.confluence?.baseUrl,
|
|
706
|
+
apiToken: process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken ?? process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,
|
|
707
|
+
apiTokenExplicit: !!(process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken)
|
|
708
|
+
},
|
|
709
|
+
artifactory: {
|
|
710
|
+
baseUrl: process.env[ENV_KEYS.ARTIFACTORY_BASE_URL] ?? globalConfig.artifactory?.baseUrl,
|
|
711
|
+
token: process.env[ENV_KEYS.ARTIFACTORY_TOKEN] ?? globalConfig.artifactory?.token,
|
|
712
|
+
npmRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NPM] ?? globalConfig.artifactory?.npmRepo,
|
|
713
|
+
nugetRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NUGET] ?? globalConfig.artifactory?.nugetRepo,
|
|
714
|
+
mavenRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_MAVEN] ?? globalConfig.artifactory?.mavenRepo
|
|
715
|
+
},
|
|
716
|
+
sonar: {
|
|
717
|
+
baseUrl: process.env[ENV_KEYS.SONAR_BASE_URL] ?? globalConfig.sonar?.baseUrl,
|
|
718
|
+
token: process.env[ENV_KEYS.SONAR_TOKEN] ?? process.env[ENV_KEYS.SONAR_TOKEN_FALLBACK] ?? globalConfig.sonar?.token
|
|
719
|
+
},
|
|
720
|
+
sde: (() => {
|
|
721
|
+
const raw = process.env[ENV_KEYS.SDE_CONNECTION] ?? globalConfig.sde?.connection;
|
|
722
|
+
const parsed = raw ? parseSdeConnection(raw) : null;
|
|
723
|
+
return { baseUrl: parsed?.baseUrl, token: parsed?.token };
|
|
724
|
+
})(),
|
|
725
|
+
ado: {
|
|
726
|
+
baseUrl: process.env[ENV_KEYS.ADO_BASE_URL] ?? globalConfig.ado?.baseUrl,
|
|
727
|
+
pat: process.env[ENV_KEYS.ADO_PAT] ?? process.env[ENV_KEYS.ADO_PAT_FALLBACK] ?? globalConfig.ado?.pat,
|
|
728
|
+
fieldAliases: globalConfig.ado?.fieldAliases ?? {},
|
|
729
|
+
discoveredFields: globalConfig.ado?.discoveredFields ?? [],
|
|
730
|
+
discoveredTypes: globalConfig.ado?.discoveredTypes ?? []
|
|
731
|
+
},
|
|
732
|
+
jenkins: {
|
|
733
|
+
baseUrl: repoConfig.defaults?.jenkins?.baseUrl ?? process.env[ENV_KEYS.JENKINS_BASE_URL] ?? globalConfig.jenkins?.baseUrl,
|
|
734
|
+
username: process.env[ENV_KEYS.JENKINS_USERNAME] ?? globalConfig.jenkins?.username,
|
|
735
|
+
apiToken: process.env[ENV_KEYS.JENKINS_API_TOKEN] ?? globalConfig.jenkins?.apiToken
|
|
736
|
+
},
|
|
737
|
+
jenkinsInstances: Array.isArray(globalConfig.jenkinsInstances) ? globalConfig.jenkinsInstances : [],
|
|
738
|
+
checkmarx: {
|
|
739
|
+
baseUrl: process.env[ENV_KEYS.CHECKMARX_BASE_URL] ?? globalConfig.checkmarx?.baseUrl,
|
|
740
|
+
tenantName: process.env[ENV_KEYS.CHECKMARX_TENANT_NAME] ?? globalConfig.checkmarx?.tenantName,
|
|
741
|
+
apiKey: process.env[ENV_KEYS.CHECKMARX_API_KEY] ?? globalConfig.checkmarx?.apiKey,
|
|
742
|
+
clientId: process.env[ENV_KEYS.CHECKMARX_CLIENT_ID] ?? globalConfig.checkmarx?.clientId,
|
|
743
|
+
clientSecret: process.env[ENV_KEYS.CHECKMARX_CLIENT_SECRET] ?? globalConfig.checkmarx?.clientSecret
|
|
744
|
+
},
|
|
745
|
+
contrast: {
|
|
746
|
+
baseUrl: process.env[ENV_KEYS.CONTRAST_BASE_URL] ?? globalConfig.contrast?.baseUrl,
|
|
747
|
+
orgUuid: process.env[ENV_KEYS.CONTRAST_ORG_UUID] ?? globalConfig.contrast?.orgUuid,
|
|
748
|
+
apiKey: process.env[ENV_KEYS.CONTRAST_API_KEY] ?? globalConfig.contrast?.apiKey,
|
|
749
|
+
serviceKey: process.env[ENV_KEYS.CONTRAST_SERVICE_KEY] ?? globalConfig.contrast?.serviceKey,
|
|
750
|
+
username: process.env[ENV_KEYS.CONTRAST_USERNAME] ?? globalConfig.contrast?.username
|
|
751
|
+
},
|
|
752
|
+
sonatypeiq: {
|
|
753
|
+
baseUrl: process.env[ENV_KEYS.SONATYPEIQ_BASE_URL] ?? globalConfig.sonatypeiq?.baseUrl,
|
|
754
|
+
userCode: process.env[ENV_KEYS.SONATYPEIQ_USER_CODE] ?? globalConfig.sonatypeiq?.userCode,
|
|
755
|
+
passcode: process.env[ENV_KEYS.SONATYPEIQ_PASSCODE] ?? globalConfig.sonatypeiq?.passcode
|
|
756
|
+
},
|
|
757
|
+
openshift: {
|
|
758
|
+
baseUrl: process.env[ENV_KEYS.OPENSHIFT_BASE_URL] ?? globalConfig.openshift?.baseUrl,
|
|
759
|
+
token: process.env[ENV_KEYS.OPENSHIFT_TOKEN] ?? globalConfig.openshift?.token,
|
|
760
|
+
defaultEnvironment: globalConfig.openshift?.defaultEnvironment,
|
|
761
|
+
defaultInstance: globalConfig.openshift?.defaultInstance,
|
|
762
|
+
environments: typeof globalConfig.openshift?.environments === "object" && globalConfig.openshift.environments !== null ? globalConfig.openshift.environments : {}
|
|
763
|
+
},
|
|
764
|
+
dynatrace: {
|
|
765
|
+
baseUrl: process.env[ENV_KEYS.DYNATRACE_BASE_URL] ?? globalConfig.dynatrace?.baseUrl,
|
|
766
|
+
apiToken: process.env[ENV_KEYS.DYNATRACE_API_TOKEN] ?? globalConfig.dynatrace?.apiToken,
|
|
767
|
+
platformUrl: process.env[ENV_KEYS.DYNATRACE_PLATFORM_URL] ?? globalConfig.dynatrace?.platformUrl,
|
|
768
|
+
platformToken: process.env[ENV_KEYS.DYNATRACE_PLATFORM_TOKEN] ?? globalConfig.dynatrace?.platformToken,
|
|
769
|
+
defaultEnvironment: globalConfig.dynatrace?.defaultEnvironment,
|
|
770
|
+
environments: typeof globalConfig.dynatrace?.environments === "object" && globalConfig.dynatrace.environments !== null ? globalConfig.dynatrace.environments : {}
|
|
771
|
+
},
|
|
772
|
+
logscale: {
|
|
773
|
+
baseUrl: process.env[ENV_KEYS.LOGSCALE_BASE_URL] ?? globalConfig.logscale?.baseUrl,
|
|
774
|
+
token: process.env[ENV_KEYS.LOGSCALE_TOKEN] ?? globalConfig.logscale?.token
|
|
775
|
+
},
|
|
776
|
+
splitio: {
|
|
777
|
+
baseUrl: process.env[ENV_KEYS.SPLITIO_BASE_URL] ?? globalConfig.splitio?.baseUrl,
|
|
778
|
+
adminApiKey: process.env[ENV_KEYS.SPLITIO_ADMIN_API_KEY] ?? globalConfig.splitio?.adminApiKey
|
|
779
|
+
},
|
|
780
|
+
figma: {
|
|
781
|
+
baseUrl: process.env[ENV_KEYS.FIGMA_BASE_URL] ?? globalConfig.figma?.baseUrl,
|
|
782
|
+
token: process.env[ENV_KEYS.FIGMA_TOKEN] ?? globalConfig.figma?.token
|
|
783
|
+
},
|
|
784
|
+
alation: {
|
|
785
|
+
baseUrl: process.env[ENV_KEYS.ALATION_BASE_URL] ?? globalConfig.alation?.baseUrl,
|
|
786
|
+
refreshToken: process.env[ENV_KEYS.ALATION_REFRESH_TOKEN] ?? globalConfig.alation?.refreshToken,
|
|
787
|
+
userId: process.env[ENV_KEYS.ALATION_USER_ID] ?? globalConfig.alation?.userId
|
|
788
|
+
},
|
|
789
|
+
saucelabs: {
|
|
790
|
+
baseUrl: process.env[ENV_KEYS.SAUCELABS_BASE_URL] ?? globalConfig.saucelabs?.baseUrl,
|
|
791
|
+
username: process.env[ENV_KEYS.SAUCELABS_USERNAME] ?? process.env[ENV_KEYS.SAUCELABS_USERNAME_FALLBACK] ?? globalConfig.saucelabs?.username,
|
|
792
|
+
accessKey: process.env[ENV_KEYS.SAUCELABS_ACCESS_KEY] ?? process.env[ENV_KEYS.SAUCELABS_ACCESS_KEY_FALLBACK] ?? globalConfig.saucelabs?.accessKey
|
|
793
|
+
},
|
|
794
|
+
defaults: mergedDefaults
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function normalizeBaseUrl(raw) {
|
|
798
|
+
const trimmed = raw.trim();
|
|
799
|
+
if (!trimmed) return trimmed;
|
|
800
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
801
|
+
}
|
|
802
|
+
function writeGlobalConfig(config, configPath) {
|
|
803
|
+
const filePath = getGlobalConfigPath(configPath);
|
|
804
|
+
const dir = path.dirname(filePath);
|
|
805
|
+
if (!fs.existsSync(dir)) {
|
|
806
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
807
|
+
}
|
|
808
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
809
|
+
}
|
|
810
|
+
function writeRepoConfig(config) {
|
|
811
|
+
const repoRoot = getRepoRoot();
|
|
812
|
+
const targetDir = repoRoot ?? process.cwd();
|
|
813
|
+
fs.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
814
|
+
}
|
|
815
|
+
function parseConfigValue(value) {
|
|
816
|
+
try {
|
|
817
|
+
const parsed = JSON.parse(value);
|
|
818
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
819
|
+
return parsed;
|
|
820
|
+
}
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
return value;
|
|
824
|
+
}
|
|
825
|
+
function setConfigValue(key, value, configPath) {
|
|
826
|
+
const filePath = getGlobalConfigPath(configPath);
|
|
827
|
+
const existing = loadJsonFile(filePath) ?? {};
|
|
828
|
+
const parts = key.split(".");
|
|
829
|
+
let current = existing;
|
|
830
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
831
|
+
const part = parts[i];
|
|
832
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
833
|
+
current[part] = {};
|
|
834
|
+
}
|
|
835
|
+
current = current[part];
|
|
836
|
+
}
|
|
837
|
+
current[parts[parts.length - 1]] = parseConfigValue(value);
|
|
838
|
+
writeGlobalConfig(existing, configPath);
|
|
839
|
+
}
|
|
840
|
+
function setRepoConfigValue(key, value) {
|
|
841
|
+
const repoRoot = getRepoRoot();
|
|
842
|
+
const targetDir = repoRoot ?? process.cwd();
|
|
843
|
+
const filePath = path.join(targetDir, ".pncli.json");
|
|
844
|
+
const existing = loadJsonFile(filePath) ?? {};
|
|
845
|
+
const parts = key.split(".");
|
|
846
|
+
let current = existing;
|
|
847
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
848
|
+
const part = parts[i];
|
|
849
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
850
|
+
current[part] = {};
|
|
851
|
+
}
|
|
852
|
+
current = current[part];
|
|
853
|
+
}
|
|
854
|
+
current[parts[parts.length - 1]] = parseConfigValue(value);
|
|
855
|
+
writeRepoConfig(existing);
|
|
856
|
+
}
|
|
857
|
+
function maskConfig(config) {
|
|
858
|
+
return {
|
|
859
|
+
...config,
|
|
860
|
+
jira: {
|
|
861
|
+
...config.jira,
|
|
862
|
+
apiToken: config.jira.apiToken ? "***" : void 0,
|
|
863
|
+
customFields: config.jira.customFields
|
|
864
|
+
},
|
|
865
|
+
bitbucket: {
|
|
866
|
+
...config.bitbucket,
|
|
867
|
+
pat: config.bitbucket.pat ? "***" : void 0
|
|
868
|
+
},
|
|
869
|
+
github: {
|
|
870
|
+
...config.github,
|
|
871
|
+
token: config.github.token ? "***" : void 0
|
|
872
|
+
},
|
|
873
|
+
confluence: {
|
|
874
|
+
...config.confluence,
|
|
875
|
+
apiToken: config.confluence.apiToken ? "***" : void 0
|
|
876
|
+
},
|
|
877
|
+
artifactory: {
|
|
878
|
+
...config.artifactory,
|
|
879
|
+
token: config.artifactory.token ? "***" : void 0
|
|
880
|
+
},
|
|
881
|
+
sonar: {
|
|
882
|
+
...config.sonar,
|
|
883
|
+
token: config.sonar.token ? "***" : void 0
|
|
884
|
+
},
|
|
885
|
+
sde: {
|
|
886
|
+
baseUrl: config.sde.baseUrl,
|
|
887
|
+
token: config.sde.token ? "***" : void 0
|
|
888
|
+
},
|
|
889
|
+
ado: {
|
|
890
|
+
...config.ado,
|
|
891
|
+
pat: config.ado.pat ? "***" : void 0
|
|
892
|
+
},
|
|
893
|
+
jenkins: {
|
|
894
|
+
...config.jenkins,
|
|
895
|
+
apiToken: config.jenkins.apiToken ? "***" : void 0
|
|
896
|
+
},
|
|
897
|
+
jenkinsInstances: config.jenkinsInstances.map((inst) => ({
|
|
898
|
+
...inst,
|
|
899
|
+
apiToken: inst.apiToken ? "***" : void 0
|
|
900
|
+
})),
|
|
901
|
+
checkmarx: {
|
|
902
|
+
...config.checkmarx,
|
|
903
|
+
apiKey: config.checkmarx.apiKey ? "***" : void 0,
|
|
904
|
+
clientSecret: config.checkmarx.clientSecret ? "***" : void 0
|
|
905
|
+
},
|
|
906
|
+
contrast: {
|
|
907
|
+
...config.contrast,
|
|
908
|
+
apiKey: config.contrast.apiKey ? "***" : void 0,
|
|
909
|
+
serviceKey: config.contrast.serviceKey ? "***" : void 0
|
|
910
|
+
},
|
|
911
|
+
sonatypeiq: {
|
|
912
|
+
...config.sonatypeiq,
|
|
913
|
+
passcode: config.sonatypeiq.passcode ? "***" : void 0
|
|
914
|
+
},
|
|
915
|
+
openshift: {
|
|
916
|
+
...config.openshift,
|
|
917
|
+
token: config.openshift.token ? "***" : void 0,
|
|
918
|
+
environments: Object.fromEntries(
|
|
919
|
+
Object.entries(config.openshift.environments).map(([envName, envCfg]) => [
|
|
920
|
+
envName,
|
|
921
|
+
{
|
|
922
|
+
instances: Object.fromEntries(
|
|
923
|
+
Object.entries(envCfg.instances ?? {}).map(([instName, instCfg]) => [
|
|
924
|
+
instName,
|
|
925
|
+
{ ...instCfg, token: instCfg.token ? "***" : void 0 }
|
|
926
|
+
])
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
])
|
|
930
|
+
)
|
|
931
|
+
},
|
|
932
|
+
dynatrace: {
|
|
933
|
+
...config.dynatrace,
|
|
934
|
+
apiToken: config.dynatrace.apiToken ? "***" : void 0,
|
|
935
|
+
platformToken: config.dynatrace.platformToken ? "***" : void 0,
|
|
936
|
+
environments: Object.fromEntries(
|
|
937
|
+
Object.entries(config.dynatrace.environments).map(([name, env]) => [
|
|
938
|
+
name,
|
|
939
|
+
{
|
|
940
|
+
...env,
|
|
941
|
+
apiToken: env.apiToken ? "***" : void 0,
|
|
942
|
+
platformToken: env.platformToken ? "***" : void 0
|
|
943
|
+
}
|
|
944
|
+
])
|
|
945
|
+
)
|
|
946
|
+
},
|
|
947
|
+
logscale: {
|
|
948
|
+
...config.logscale,
|
|
949
|
+
token: config.logscale.token ? "***" : void 0
|
|
950
|
+
},
|
|
951
|
+
splitio: {
|
|
952
|
+
...config.splitio,
|
|
953
|
+
adminApiKey: config.splitio.adminApiKey ? "***" : void 0
|
|
954
|
+
},
|
|
955
|
+
figma: {
|
|
956
|
+
...config.figma,
|
|
957
|
+
token: config.figma.token ? "***" : void 0
|
|
958
|
+
},
|
|
959
|
+
alation: {
|
|
960
|
+
...config.alation,
|
|
961
|
+
refreshToken: config.alation.refreshToken ? "***" : void 0
|
|
962
|
+
},
|
|
963
|
+
saucelabs: {
|
|
964
|
+
...config.saucelabs,
|
|
965
|
+
accessKey: config.saucelabs.accessKey ? "***" : void 0
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export {
|
|
971
|
+
assertValidCustomFields,
|
|
972
|
+
defaultRunner,
|
|
973
|
+
KeychainError,
|
|
974
|
+
KeychainBatchError,
|
|
975
|
+
isKeychainRef,
|
|
976
|
+
keychainRef,
|
|
977
|
+
accountFromRef,
|
|
978
|
+
windowsListCredentials,
|
|
979
|
+
getKeychainBackend,
|
|
980
|
+
purgeEntries,
|
|
981
|
+
findKeychainRefs,
|
|
982
|
+
findPlaintextSecrets,
|
|
983
|
+
accountForPath,
|
|
984
|
+
setAtPath,
|
|
985
|
+
getUnresolvedKeychainRefs,
|
|
986
|
+
resetKeychainCache,
|
|
987
|
+
resolveKeychainRefs,
|
|
988
|
+
storeVerified,
|
|
989
|
+
resolveSecretValue,
|
|
990
|
+
getGlobalConfigPath,
|
|
991
|
+
loadJsonFile,
|
|
992
|
+
envOverriddenSecretPaths,
|
|
993
|
+
loadConfig,
|
|
994
|
+
normalizeBaseUrl,
|
|
995
|
+
writeGlobalConfig,
|
|
996
|
+
writeRepoConfig,
|
|
997
|
+
setConfigValue,
|
|
998
|
+
setRepoConfigValue,
|
|
999
|
+
maskConfig
|
|
1000
|
+
};
|
|
1001
|
+
//# sourceMappingURL=chunk-WDWAPTVC.js.map
|