@kolatts/pncli 5.3.0 → 5.4.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/dist/cli.js CHANGED
@@ -1,16 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ KeychainBatchError,
4
+ KeychainError,
5
+ accountForPath,
6
+ accountFromRef,
3
7
  assertValidCustomFields,
8
+ defaultRunner,
9
+ findKeychainRefs,
10
+ findPlaintextSecrets,
4
11
  getGlobalConfigPath,
12
+ getKeychainBackend,
13
+ getUnresolvedKeychainRefs,
14
+ isKeychainRef,
15
+ keychainRef,
5
16
  loadConfig,
6
17
  loadJsonFile,
7
18
  maskConfig,
8
19
  normalizeBaseUrl,
20
+ purgeEntries,
21
+ resetKeychainCache,
22
+ resolveKeychainRefs,
23
+ resolveSecretValue,
24
+ setAtPath,
9
25
  setConfigValue,
10
26
  setRepoConfigValue,
27
+ storeVerified,
28
+ windowsListCredentials,
11
29
  writeGlobalConfig,
12
30
  writeRepoConfig
13
- } from "./chunk-QHJ2HM5J.js";
31
+ } from "./chunk-L6OY2546.js";
14
32
  import {
15
33
  createHttpClient,
16
34
  validateAlationAccessToken
@@ -447,34 +465,34 @@ function parseAdoRemote(remoteUrl, adoBaseUrl) {
447
465
  } catch {
448
466
  baseHostname = adoBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "").split(/[:/]/)[0].toLowerCase();
449
467
  }
450
- let path11 = remoteUrl;
468
+ let path14 = remoteUrl;
451
469
  let remoteHostname = "";
452
- const sshProtoMatch = path11.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
470
+ const sshProtoMatch = path14.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
453
471
  if (sshProtoMatch) {
454
472
  remoteHostname = sshProtoMatch[1].toLowerCase();
455
- path11 = sshProtoMatch[2];
473
+ path14 = sshProtoMatch[2];
456
474
  } else {
457
- const gitAtMatch = path11.match(/^git@([^:]+)(?::\d+)?:(.*)/);
475
+ const gitAtMatch = path14.match(/^git@([^:]+)(?::\d+)?:(.*)/);
458
476
  if (gitAtMatch) {
459
477
  remoteHostname = gitAtMatch[1].toLowerCase();
460
- path11 = "/" + gitAtMatch[2];
478
+ path14 = "/" + gitAtMatch[2];
461
479
  } else {
462
- const httpsMatch = path11.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
480
+ const httpsMatch = path14.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
463
481
  if (httpsMatch) {
464
482
  remoteHostname = httpsMatch[1].toLowerCase();
465
- path11 = httpsMatch[2];
483
+ path14 = httpsMatch[2];
466
484
  }
467
485
  }
468
486
  }
469
487
  if (!remoteHostname || remoteHostname !== baseHostname) return null;
470
- path11 = path11.replace(/\.git$/, "");
471
- const gitIdx = path11.indexOf("/_git/");
472
- const sshIdx = path11.indexOf("/_ssh/");
488
+ path14 = path14.replace(/\.git$/, "");
489
+ const gitIdx = path14.indexOf("/_git/");
490
+ const sshIdx = path14.indexOf("/_ssh/");
473
491
  const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
474
492
  if (markerIdx === -1) return null;
475
- const repo = decodeURIComponent(path11.slice(markerIdx + 6));
493
+ const repo = decodeURIComponent(path14.slice(markerIdx + 6));
476
494
  if (!repo) return null;
477
- const before = path11.slice(1, markerIdx);
495
+ const before = path14.slice(1, markerIdx);
478
496
  const parts = before.split("/").filter(Boolean);
479
497
  if (parts.length < 2) return null;
480
498
  const project = decodeURIComponent(parts[parts.length - 1]);
@@ -817,7 +835,936 @@ var BitbucketClient = class {
817
835
  }
818
836
  };
819
837
 
838
+ // src/services/git/credentials.ts
839
+ import fs from "fs";
840
+ import os from "os";
841
+ import path from "path";
842
+
843
+ // src/services/skills/git-auth.ts
844
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
845
+ var HELPER_COMMAND = "!pncli skills git-credential";
846
+ var HELPER_NAME_PATTERN = /^[^'\r\n]+$/;
847
+ function helperCommandFor(marketplaceName, configPath) {
848
+ let command = HELPER_COMMAND;
849
+ if (configPath !== void 0) {
850
+ if (!HELPER_NAME_PATTERN.test(configPath)) {
851
+ throw new Error(`Config path ${JSON.stringify(configPath)} cannot be used in a git credential helper (no quotes or newlines).`);
852
+ }
853
+ command += ` --config '${configPath}'`;
854
+ }
855
+ if (marketplaceName === void 0) return command;
856
+ if (!HELPER_NAME_PATTERN.test(marketplaceName)) {
857
+ throw new Error(`Marketplace name ${JSON.stringify(marketplaceName)} cannot be used in a git credential helper (no quotes or newlines). Re-add it with a simpler --name.`);
858
+ }
859
+ return `${command} --marketplace '${marketplaceName}'`;
860
+ }
861
+ function isPncliHelperValue(value) {
862
+ return value.trim().startsWith(HELPER_COMMAND);
863
+ }
864
+ function helperConfigKey(scope) {
865
+ return `credential.${scope}.helper`;
866
+ }
867
+ function useHttpPathKey(scope) {
868
+ return `credential.${scope}.useHttpPath`;
869
+ }
870
+ function gitUsernameForHost(host) {
871
+ return host === "github.com" ? "x-access-token" : "x-token-auth";
872
+ }
873
+ function gitHostFromApiBaseUrl(baseUrl) {
874
+ if (!baseUrl) return null;
875
+ try {
876
+ const host = new URL(baseUrl).host.toLowerCase();
877
+ return host === "api.github.com" ? "github.com" : host;
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
882
+ function apiBaseForHost(host, githubBaseUrl) {
883
+ if (host === "github.com") return "https://api.github.com";
884
+ if (githubBaseUrl && gitHostFromApiBaseUrl(githubBaseUrl) === host) return githubBaseUrl.replace(/\/+$/, "");
885
+ return `https://${host}/api/v3`;
886
+ }
887
+ function hostOf(url) {
888
+ if (!url) return null;
889
+ try {
890
+ const u = new URL(url);
891
+ return u.protocol === "https:" ? u.host.toLowerCase() : null;
892
+ } catch {
893
+ return null;
894
+ }
895
+ }
896
+ function httpHostOf(url) {
897
+ if (!url) return null;
898
+ try {
899
+ const u = new URL(url);
900
+ return u.protocol === "https:" || u.protocol === "http:" ? u.host.toLowerCase() : null;
901
+ } catch {
902
+ return null;
903
+ }
904
+ }
905
+ function hostnameOf(url) {
906
+ if (!url) return null;
907
+ try {
908
+ return new URL(url).hostname.toLowerCase();
909
+ } catch {
910
+ return null;
911
+ }
912
+ }
913
+ function normalizeRepoPath(p) {
914
+ return p.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
915
+ }
916
+ function classifyGitHubToken(token) {
917
+ if (token.startsWith("ghp_")) return "classic";
918
+ if (token.startsWith("github_pat_")) return "fine-grained";
919
+ if (token.startsWith("gho_")) return "oauth";
920
+ if (token.startsWith("ghu_") || token.startsWith("ghs_")) return "github-app";
921
+ if (/^[0-9a-f]{40}$/i.test(token)) return "legacy-hex";
922
+ return "unknown";
923
+ }
924
+ function allMarketplaces(globalConfig) {
925
+ return [...globalConfig.marketplaces ?? [], ...globalConfig.marketplace ? [globalConfig.marketplace] : []];
926
+ }
927
+ var PROVIDERS = ["github", "bitbucket", "ado", "git"];
928
+ function detectProvider(repoUrl, cfg, explicit) {
929
+ if (explicit && PROVIDERS.includes(explicit)) return explicit;
930
+ const hostname = hostnameOf(repoUrl);
931
+ if (!hostname) return "git";
932
+ const path14 = (() => {
933
+ try {
934
+ return new URL(repoUrl).pathname;
935
+ } catch {
936
+ return "";
937
+ }
938
+ })();
939
+ const githubHost = hostnameOf(cfg.github?.baseUrl);
940
+ if (hostname === "github.com" || githubHost && (hostname === githubHost || githubHost === `api.${hostname}`)) return "github";
941
+ if (hostname === hostnameOf(cfg.ado?.baseUrl) || /\/_git\//.test(path14) || hostname === "dev.azure.com" || hostname.endsWith(".visualstudio.com")) return "ado";
942
+ if (hostname === hostnameOf(cfg.bitbucket?.baseUrl) || /(^|\/)scm\//.test(path14) || hostname === "bitbucket.org") return "bitbucket";
943
+ return "git";
944
+ }
945
+ function providerFallbackToken(provider, repoUrl, cfg) {
946
+ const hostname = hostnameOf(repoUrl);
947
+ if (!hostname) return null;
948
+ if (provider === "github") {
949
+ const githubHost = hostnameOf(cfg.github?.baseUrl);
950
+ const isGitHubHost2 = hostname === "github.com" || !!githubHost && githubHost === hostname;
951
+ return isGitHubHost2 && cfg.github?.token ? { token: cfg.github.token, source: "github.token" } : null;
952
+ }
953
+ if (provider === "bitbucket") {
954
+ return hostname === hostnameOf(cfg.bitbucket?.baseUrl) && cfg.bitbucket?.pat ? { token: cfg.bitbucket.pat, source: "bitbucket.pat" } : null;
955
+ }
956
+ if (provider === "ado") {
957
+ return hostname === hostnameOf(cfg.ado?.baseUrl) && cfg.ado?.pat ? { token: cfg.ado.pat, source: "ado.pat" } : null;
958
+ }
959
+ return null;
960
+ }
961
+ var UnresolvedMarketplaceTokenError = class extends Error {
962
+ };
963
+ function marketplaceLabelOf(m) {
964
+ return m.name ?? m.repoUrl ?? "(unnamed)";
965
+ }
966
+ function resolveMarketplaceAuth(m, cfg, resolveSecret = resolveSecretValue) {
967
+ const host = httpHostOf(m.repoUrl);
968
+ if (!host) return null;
969
+ const provider = detectProvider(m.repoUrl, cfg, m.provider);
970
+ const username = m.username || gitUsernameForHost(host);
971
+ if (m.token) {
972
+ const secret = resolveSecret(m.token);
973
+ if (!secret && isKeychainRef(m.token)) {
974
+ throw new UnresolvedMarketplaceTokenError(`The token for marketplace "${marketplaceLabelOf(m)}" is stored in the OS keychain as "${m.token}", but it could not be read. Check with: pncli config keychain status`);
975
+ }
976
+ if (secret) return { username, password: secret, source: `marketplace:${marketplaceLabelOf(m)}`, provider };
977
+ }
978
+ const fallback = providerFallbackToken(provider, m.repoUrl, cfg);
979
+ return fallback ? { username, password: fallback.token, source: fallback.source, provider } : null;
980
+ }
981
+ function parseCredentialRequest(input3) {
982
+ const out = {};
983
+ for (const line of input3.split(/\r?\n/)) {
984
+ if (line === "") break;
985
+ const eq = line.indexOf("=");
986
+ if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1);
987
+ }
988
+ return out;
989
+ }
990
+ function resolveCredential(request, globalConfig, cfg, resolveSecret = resolveSecretValue, marketplaceName) {
991
+ if (request.protocol !== "https" || !request.host) return null;
992
+ const host = request.host.toLowerCase().replace(/:443$/, "");
993
+ const marketplaces = allMarketplaces(globalConfig).filter((m) => hostOf(m.repoUrl) === host);
994
+ if (marketplaceName !== void 0) {
995
+ const m = marketplaces.find((x) => x.name === marketplaceName || x.repoUrl === marketplaceName);
996
+ if (!m) return null;
997
+ const auth = resolveMarketplaceAuth(m, cfg, resolveSecret);
998
+ return auth ? { username: auth.username, password: auth.password, source: auth.source } : null;
999
+ }
1000
+ if (request.path) {
1001
+ const wanted = normalizeRepoPath(request.path);
1002
+ const exact = marketplaces.find((m) => normalizeRepoPath(new URL(m.repoUrl).pathname) === wanted);
1003
+ if (exact) {
1004
+ const auth = resolveMarketplaceAuth(exact, cfg, resolveSecret);
1005
+ if (auth) return { username: auth.username, password: auth.password, source: auth.source };
1006
+ }
1007
+ }
1008
+ const url = `https://${host}/`;
1009
+ for (const provider of ["github", "bitbucket", "ado"]) {
1010
+ const fallback = providerFallbackToken(provider, url, cfg);
1011
+ if (fallback) return { username: gitUsernameForHost(host), password: fallback.token, source: fallback.source };
1012
+ }
1013
+ return null;
1014
+ }
1015
+ function formatCredentialAnswer(answer) {
1016
+ return `username=${answer.username}
1017
+ password=${answer.password}
1018
+ `;
1019
+ }
1020
+ function parseScope(scope) {
1021
+ const u = new URL(scope);
1022
+ const path14 = u.pathname.replace(/^\/+/, "");
1023
+ return { scope, host: u.host.toLowerCase(), path: path14 || null };
1024
+ }
1025
+ function hostScope(host) {
1026
+ return `https://${host}`;
1027
+ }
1028
+ function marketplaceScopes(repoUrl, provider) {
1029
+ const u = new URL(repoUrl);
1030
+ u.username = "";
1031
+ u.password = "";
1032
+ u.search = "";
1033
+ u.hash = "";
1034
+ const base = `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
1035
+ if (provider === "ado") return [base];
1036
+ const toggled = /\.git$/i.test(base) ? base.replace(/\.git$/i, "") : `${base}.git`;
1037
+ return [base, toggled];
1038
+ }
1039
+ var defaultGitRunner = (args, opts) => {
1040
+ const r = spawnSync("git", args, {
1041
+ input: opts?.input,
1042
+ encoding: "utf8",
1043
+ windowsHide: true,
1044
+ timeout: opts?.timeoutMs ?? 3e4,
1045
+ env: { ...process.env, ...opts?.env }
1046
+ });
1047
+ if (r.error) throw r.error;
1048
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
1049
+ };
1050
+ var NON_INTERACTIVE_ENV = {
1051
+ GIT_TERMINAL_PROMPT: "0",
1052
+ GCM_INTERACTIVE: "never",
1053
+ GIT_ASKPASS: "",
1054
+ SSH_ASKPASS: ""
1055
+ };
1056
+ function readHelperEntries(scope, git = defaultGitRunner) {
1057
+ const r = git(["config", "--global", "--get-all", helperConfigKey(scope)]);
1058
+ return r.status === 0 ? r.stdout.split(/\r?\n/).filter((l, i, a) => i < a.length - 1 || l !== "") : [];
1059
+ }
1060
+ function readGlobalHelpers(git = defaultGitRunner) {
1061
+ const r = git(["config", "--get-all", "credential.helper"]);
1062
+ return r.status === 0 ? r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : [];
1063
+ }
1064
+ function readUseHttpPath(scope, git) {
1065
+ const r = git(["config", "--global", "--get", useHttpPathKey(scope)]);
1066
+ return r.status === 0 ? r.stdout.trim() : null;
1067
+ }
1068
+ function afterLastReset(entries) {
1069
+ const own = entries.filter((e) => !isPncliHelperValue(e));
1070
+ return own.slice(own.lastIndexOf("") + 1).filter((e) => e.trim() !== "");
1071
+ }
1072
+ function effectiveFallbackHelpers(scopeEntries, genericHelpers, hostEntries = []) {
1073
+ for (const entries of [scopeEntries, hostEntries]) {
1074
+ if (entries.some((e) => !isPncliHelperValue(e))) return afterLastReset(entries);
1075
+ }
1076
+ return genericHelpers.filter((h) => h.trim() !== "" && !isPncliHelperValue(h));
1077
+ }
1078
+ function enableHelper(scope, helperValue, git = defaultGitRunner) {
1079
+ const { host, path: path14 } = parseScope(scope);
1080
+ const previousHelpers = readHelperEntries(scope, git).filter((e) => !isPncliHelperValue(e));
1081
+ const previousUseHttpPath = readUseHttpPath(scope, git);
1082
+ const hostEntries = path14 ? readHelperEntries(hostScope(host), git) : [];
1083
+ const fallbacks = effectiveFallbackHelpers(previousHelpers, readGlobalHelpers(git), hostEntries);
1084
+ git(["config", "--global", "--unset-all", helperConfigKey(scope)]);
1085
+ for (const value of ["", helperValue, ...fallbacks]) {
1086
+ const r = git(["config", "--global", "--add", helperConfigKey(scope), value]);
1087
+ if (r.status !== 0) throw new Error(`git config failed: ${r.stderr.trim()}`);
1088
+ }
1089
+ return { previousHelpers, previousUseHttpPath };
1090
+ }
1091
+ function restoreUseHttpPath(scope, previous, git) {
1092
+ if (previous === null) git(["config", "--global", "--unset", useHttpPathKey(scope)]);
1093
+ else git(["config", "--global", useHttpPathKey(scope), previous]);
1094
+ }
1095
+ function disableHelper(scope, backup, git = defaultGitRunner) {
1096
+ const current = readHelperEntries(scope, git);
1097
+ if (!current.some(isPncliHelperValue)) return false;
1098
+ const restore = backup ? backup.previousHelpers : current.slice(current.findIndex(isPncliHelperValue) + 1);
1099
+ git(["config", "--global", "--unset-all", helperConfigKey(scope)]);
1100
+ for (const value of restore) git(["config", "--global", "--add", helperConfigKey(scope), value]);
1101
+ if (backup) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
1102
+ return true;
1103
+ }
1104
+ function inlineCredentialArgs(username, token) {
1105
+ const helper = `credential.helper=!f() { test "$1" = get || exit 0; printf '%s\\n' "username=$PNCLI_GIT_USERNAME" "password=$PNCLI_GIT_PASSWORD"; }; f`;
1106
+ return {
1107
+ args: ["-c", "credential.helper=", "-c", helper],
1108
+ env: { PNCLI_GIT_USERNAME: username, PNCLI_GIT_PASSWORD: token }
1109
+ };
1110
+ }
1111
+ function credentialInput(host, fields = {}) {
1112
+ return Object.entries({ protocol: "https", host, ...fields }).map(([k, v]) => `${k}=${v}`).join("\n") + "\n\n";
1113
+ }
1114
+ function keychainStore(scope, username, token, git = defaultGitRunner) {
1115
+ const helpers = readGlobalHelpers(git);
1116
+ if (helpers.length === 0) {
1117
+ throw new Error(
1118
+ "git has no credential.helper configured, so there is no OS keychain to store into. Configure one first \u2014 Windows: `git config --global credential.helper manager` (ships with Git for Windows); macOS: `git config --global credential.helper osxkeychain`; Linux: git-credential-libsecret. Or use --mode helper, which needs no keychain."
1119
+ );
1120
+ }
1121
+ const { host, path: path14 } = parseScope(scope);
1122
+ const backup = { previousHelpers: [], previousUseHttpPath: readUseHttpPath(scope, git) };
1123
+ if (path14) git(["config", "--global", useHttpPathKey(scope), "true"]);
1124
+ const r = git(["credential", "approve"], { input: credentialInput(host, { ...path14 ? { path: path14 } : {}, username, password: token }), env: NON_INTERACTIVE_ENV });
1125
+ if (r.status !== 0) {
1126
+ if (path14) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
1127
+ throw new Error(`git credential approve failed: ${r.stderr.trim()}`);
1128
+ }
1129
+ return { helpers, plaintextStore: helpers.some((h) => /^store\b/.test(h)), backup };
1130
+ }
1131
+ function keychainErase(scope, username, git = defaultGitRunner) {
1132
+ const { host, path: path14 } = parseScope(scope);
1133
+ git(["credential", "reject"], { input: credentialInput(host, { ...path14 ? { path: path14 } : {}, username }), env: NON_INTERACTIVE_ENV });
1134
+ }
1135
+ function disableScope(scope, record, forgetKeychain, git = defaultGitRunner) {
1136
+ const backup = record ? { previousHelpers: record.previousHelpers ?? [], previousUseHttpPath: record.previousUseHttpPath ?? null } : void 0;
1137
+ const helperRemoved = disableHelper(scope, record?.mode === "helper" ? backup : void 0, git);
1138
+ let keychainErased = false;
1139
+ if (record?.mode === "keychain") {
1140
+ if (forgetKeychain) {
1141
+ keychainErase(scope, record.username ?? gitUsernameForHost(parseScope(scope).host), git);
1142
+ keychainErased = true;
1143
+ }
1144
+ keychainRestoreScope(scope, backup, git);
1145
+ }
1146
+ return { helperRemoved, keychainErased };
1147
+ }
1148
+ function keychainRestoreScope(scope, backup, git = defaultGitRunner) {
1149
+ if (parseScope(scope).path) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
1150
+ }
1151
+ function credentialFill(host, path14, git = defaultGitRunner) {
1152
+ try {
1153
+ const r = git(["credential", "fill"], { input: credentialInput(host, path14 ? { path: path14 } : {}), env: NON_INTERACTIVE_ENV });
1154
+ if (r.status !== 0) return null;
1155
+ const parsed = parseCredentialRequest(r.stdout);
1156
+ return parsed.password ? { username: parsed.username ?? "", password: parsed.password } : null;
1157
+ } catch {
1158
+ return null;
1159
+ }
1160
+ }
1161
+ function lsRemote(repoUrl, auth, git = defaultGitRunner, opts = {}) {
1162
+ const inline = auth ? inlineCredentialArgs(auth.username, auth.password) : { args: opts.keepHelpers ? [] : ["-c", "credential.helper="], env: {} };
1163
+ try {
1164
+ const r = git([...inline.args, "ls-remote", "--heads", repoUrl], { env: { ...NON_INTERACTIVE_ENV, ...inline.env }, timeoutMs: 2e4 });
1165
+ if (r.status === 0) return { ok: true, error: null };
1166
+ const scrub2 = (s) => auth ? s.split(auth.password).join("***") : s;
1167
+ return { ok: false, error: scrub2(r.stderr.trim().split(/\r?\n/).slice(-2).join(" ")) || `git exited with ${r.status}` };
1168
+ } catch (err) {
1169
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1170
+ }
1171
+ }
1172
+ function originHasCredentials(repoPath) {
1173
+ try {
1174
+ const url = execFileSync2("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: "pipe" }).trim();
1175
+ const u = new URL(url);
1176
+ return !!u.password;
1177
+ } catch {
1178
+ return false;
1179
+ }
1180
+ }
1181
+ async function inspectGitHubToken(apiBase, token, fetchFn = fetch, now = /* @__PURE__ */ new Date()) {
1182
+ const kind = classifyGitHubToken(token);
1183
+ const base = {
1184
+ kind,
1185
+ status: null,
1186
+ valid: false,
1187
+ login: null,
1188
+ scopes: null,
1189
+ missingRepoScope: false,
1190
+ expiresAt: null,
1191
+ expiresInDays: null,
1192
+ ssoAuthorizationRequired: false,
1193
+ error: null
1194
+ };
1195
+ let res;
1196
+ try {
1197
+ res = await fetchFn(`${apiBase.replace(/\/+$/, "")}/user`, {
1198
+ headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "pncli" },
1199
+ signal: AbortSignal.timeout(1e4)
1200
+ });
1201
+ } catch (err) {
1202
+ return { ...base, error: err instanceof Error ? err.message : String(err) };
1203
+ }
1204
+ const scopesHeader = res.headers.get("x-oauth-scopes");
1205
+ const scopes = scopesHeader === null ? null : scopesHeader.split(",").map((s) => s.trim()).filter(Boolean);
1206
+ const expiryHeader = res.headers.get("github-authentication-token-expiration");
1207
+ let expiresAt = null;
1208
+ let expiresInDays = null;
1209
+ if (expiryHeader) {
1210
+ const t = Date.parse(expiryHeader.replace(/ UTC$/, "Z").replace(" ", "T").replace(" ", ""));
1211
+ if (!Number.isNaN(t)) {
1212
+ expiresAt = new Date(t).toISOString();
1213
+ expiresInDays = Math.floor((t - now.getTime()) / 864e5);
1214
+ }
1215
+ }
1216
+ let login = null;
1217
+ if (res.ok) {
1218
+ try {
1219
+ login = (await res.json()).login ?? null;
1220
+ } catch {
1221
+ }
1222
+ }
1223
+ const classicLike = kind === "classic" || kind === "legacy-hex";
1224
+ return {
1225
+ ...base,
1226
+ status: res.status,
1227
+ valid: res.ok,
1228
+ login,
1229
+ scopes,
1230
+ missingRepoScope: res.ok && classicLike && scopes !== null && !scopes.includes("repo"),
1231
+ expiresAt,
1232
+ expiresInDays,
1233
+ ssoAuthorizationRequired: res.status === 403 && res.headers.has("x-github-sso"),
1234
+ error: res.ok ? null : `GET /user returned HTTP ${res.status}`
1235
+ };
1236
+ }
1237
+
1238
+ // src/services/git/credentials.ts
1239
+ function fingerprint(token) {
1240
+ const prefix = /^(github_pat_|gh[pousr]_)/.exec(token)?.[1] ?? "";
1241
+ return token.length <= 8 ? "\u2026" : `${prefix}\u2026${token.slice(-4)}`;
1242
+ }
1243
+ function looksLikeToken(value) {
1244
+ return /^(github_pat_|gh[pousr]_)/.test(value) || /^[0-9a-f]{40}$/i.test(value) || value.length >= 30 && !/[@\s]/.test(value);
1245
+ }
1246
+ function tokenFromUrl(url) {
1247
+ try {
1248
+ const u = new URL(url);
1249
+ const user = decodeURIComponent(u.username);
1250
+ const pass = decodeURIComponent(u.password);
1251
+ if (pass) return { username: user, token: pass };
1252
+ if (user && looksLikeToken(user)) return { username: "", token: user };
1253
+ return null;
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ }
1258
+ function redactUrl(url) {
1259
+ try {
1260
+ const u = new URL(url);
1261
+ if (!u.username && !u.password) return url;
1262
+ const found = tokenFromUrl(url);
1263
+ const shown = found ? found.username ? `${found.username}:${fingerprint(found.token)}` : fingerprint(found.token) : "***";
1264
+ return `${u.protocol}//${shown}@${u.host}${u.pathname}${u.search}`;
1265
+ } catch {
1266
+ return url.replace(/\/\/[^/@]+@/, "//***@");
1267
+ }
1268
+ }
1269
+ function redactSecrets(text) {
1270
+ return text.replace(/(github_pat_|gh[pousr]_)[A-Za-z0-9_]{8,}/g, (m) => fingerprint(m)).replace(/\b[0-9a-f]{40}\b/gi, (m) => fingerprint(m)).replace(/(password=)([^\s;'"]+)/gi, (m, k, v) => v.includes("\u2026") ? m : `${k}${fingerprint(v)}`).replace(/(\/\/)([^/@\s:]+):([^/@\s]+)@/g, (_m, s, u, p) => `${s}${u}:${fingerprint(p)}@`);
1271
+ }
1272
+ function scrub(text, secret) {
1273
+ return secret ? text.split(secret).join(fingerprint(secret)) : text;
1274
+ }
1275
+ function parseInsteadOf(output) {
1276
+ const mappings = [];
1277
+ for (const line of output.split(/\r?\n/)) {
1278
+ const m = /^url\.(.+)\.insteadof (.+)$/i.exec(line.trim());
1279
+ if (m) mappings.push({ from: m[2], to: redactUrl(m[1]), rawTo: m[1] });
1280
+ }
1281
+ return mappings;
1282
+ }
1283
+ function matchMapping(url, mappings) {
1284
+ let best = null;
1285
+ for (const m of mappings) {
1286
+ if (url.startsWith(m.from) && (!best || m.from.length > best.from.length)) best = m;
1287
+ }
1288
+ return best;
1289
+ }
1290
+ function parseScopedHelpers(output) {
1291
+ const out = [];
1292
+ for (const line of output.split(/\r?\n/)) {
1293
+ const m = /^credential\.(.+)\.helper(?: (.*))?$/i.exec(line.trim());
1294
+ if (m) out.push({ scope: m[1], helper: m[2] ?? "" });
1295
+ }
1296
+ return out;
1297
+ }
1298
+ function githubRepoSlug(url) {
1299
+ try {
1300
+ const parts = new URL(url).pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
1301
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
1302
+ } catch {
1303
+ return null;
1304
+ }
1305
+ }
1306
+ async function checkGitHubRepoAccess(apiBase, slug, token, fetchFn = fetch) {
1307
+ try {
1308
+ const res = await fetchFn(`${apiBase.replace(/\/+$/, "")}/repos/${slug}`, {
1309
+ headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "pncli" },
1310
+ signal: AbortSignal.timeout(1e4)
1311
+ });
1312
+ return {
1313
+ status: res.status,
1314
+ canRead: res.ok,
1315
+ ssoAuthorizationRequired: res.status === 403 && res.headers.has("x-github-sso"),
1316
+ error: res.ok ? null : `GET /repos/${slug} returned HTTP ${res.status}`
1317
+ };
1318
+ } catch (err) {
1319
+ return { status: null, canRead: false, ssoAuthorizationRequired: false, error: err instanceof Error ? err.message : String(err) };
1320
+ }
1321
+ }
1322
+ function gitIn(cwd, git) {
1323
+ return cwd ? (args, opts) => git(["-C", cwd, ...args], opts) : git;
1324
+ }
1325
+ async function inspectRemoteAuth(remote, url, cwd, cfg, deps = {}) {
1326
+ const baseGit = deps.git ?? defaultGitRunner;
1327
+ const git = gitIn(cwd, baseGit);
1328
+ const fetchFn = deps.fetchFn ?? fetch;
1329
+ const effRaw = (() => {
1330
+ const r = git(["ls-remote", "--get-url", url]);
1331
+ return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : url;
1332
+ })();
1333
+ const mappingRaw = matchMapping(url, parseInsteadOf(git(["config", "--get-regexp", "^url\\..*\\.insteadof$"]).stdout));
1334
+ let host = null;
1335
+ let repoPath = null;
1336
+ try {
1337
+ const u = new URL(effRaw);
1338
+ if (u.protocol === "https:" || u.protocol === "http:") {
1339
+ host = u.host.toLowerCase();
1340
+ repoPath = u.pathname.replace(/^\/+/, "");
1341
+ }
1342
+ } catch {
1343
+ }
1344
+ const plainEffective = (() => {
1345
+ try {
1346
+ const u = new URL(effRaw);
1347
+ u.username = "";
1348
+ u.password = "";
1349
+ return u.toString();
1350
+ } catch {
1351
+ return effRaw;
1352
+ }
1353
+ })();
1354
+ const provider = detectProvider(plainEffective, cfg);
1355
+ let source = "none";
1356
+ let credential = null;
1357
+ let helper = null;
1358
+ const embedded = tokenFromUrl(effRaw);
1359
+ if (embedded) {
1360
+ credential = embedded;
1361
+ source = mappingRaw && tokenFromUrl(mappingRaw.rawTo) ? "insteadOf" : "url";
1362
+ } else if (host) {
1363
+ const filled = credentialFill(host, repoPath ?? void 0, git);
1364
+ if (filled) {
1365
+ credential = { username: filled.username, token: filled.password };
1366
+ source = "helper";
1367
+ const h = git(["config", "--get-urlmatch", "credential.helper", plainEffective]);
1368
+ helper = h.status === 0 ? redactSecrets(h.stdout.trim().split(/\r?\n/).pop() ?? "") || null : null;
1369
+ }
1370
+ }
1371
+ const report = {
1372
+ remote,
1373
+ url: redactUrl(url),
1374
+ effectiveUrl: redactUrl(effRaw),
1375
+ host,
1376
+ provider,
1377
+ mapping: mappingRaw ? { from: mappingRaw.from, to: mappingRaw.to } : null,
1378
+ credentialSource: source,
1379
+ helper,
1380
+ helperKind: helper ? classifyHelper(helper) : null,
1381
+ credentialStore: helper && classifyHelper(helper) === "git-credential-manager" ? gcmCredentialStore(git) : null,
1382
+ token: credential ? { kind: provider === "github" ? classifyGitHubToken(credential.token) : "unknown", fingerprint: fingerprint(credential.token), username: credential.username || null } : null,
1383
+ access: null,
1384
+ github: null,
1385
+ problems: []
1386
+ };
1387
+ if (host && !deps.offline) {
1388
+ const auth = source === "helper" && credential ? { username: credential.username, password: credential.token } : null;
1389
+ const r = lsRemote(remote && cwd ? remote : source === "helper" ? plainEffective : url, auth, git);
1390
+ report.access = { ok: r.ok, error: r.error ? scrub(r.error, credential?.token ?? null) : null };
1391
+ if (provider === "github" && credential) {
1392
+ const apiBase = apiBaseForHost(host, cfg.github?.baseUrl);
1393
+ const key = `${apiBase}\0${credential.token}`;
1394
+ if (!deps.cache?.has(key)) deps.cache?.set(key, await inspectGitHubToken(apiBase, credential.token, fetchFn));
1395
+ const tokenInfo = deps.cache?.get(key) ?? await inspectGitHubToken(apiBase, credential.token, fetchFn);
1396
+ const slug = githubRepoSlug(plainEffective);
1397
+ const repo = slug && tokenInfo.valid ? await checkGitHubRepoAccess(apiBase, slug, credential.token, fetchFn) : null;
1398
+ report.github = { token: tokenInfo, repo };
1399
+ }
1400
+ }
1401
+ report.problems = buildRemoteProblems(report, url);
1402
+ return report;
1403
+ }
1404
+ function buildRemoteProblems(r, originalUrl) {
1405
+ const problems = [];
1406
+ const where = r.credentialSource === "insteadOf" && r.mapping ? `the insteadOf mapping for ${r.mapping.from}` : r.credentialSource === "url" ? "the remote URL" : r.credentialSource === "helper" ? `the credential helper (${r.helper ?? "unknown"})` : "nothing";
1407
+ const org = (() => {
1408
+ try {
1409
+ const u = new URL(originalUrl);
1410
+ return `${u.protocol}//${u.host}/${u.pathname.split("/").filter(Boolean)[0] ?? ""}/`;
1411
+ } catch {
1412
+ return originalUrl;
1413
+ }
1414
+ })();
1415
+ if (!r.host) return problems;
1416
+ if (r.credentialSource === "url") {
1417
+ problems.push({
1418
+ message: "The token is embedded in the remote URL, so it sits in plaintext in this clone's .git/config",
1419
+ fix: `Move it to a mapping or helper and reset the remote: git remote set-url ${r.remote ?? "origin"} ${r.url.replace(/\/\/[^@]+@/, "//")}`
1420
+ });
1421
+ }
1422
+ if (r.access && !r.access.ok) {
1423
+ if (r.credentialSource === "none") {
1424
+ problems.push({
1425
+ message: `No credential is mapped for ${r.effectiveUrl} \u2014 git reached it anonymously and was refused (${r.access.error})`,
1426
+ fix: r.provider === "github" ? `Map a token for the org: git config --global url."https://<classic-PAT>@${r.host}/${org.split("/").slice(3).join("/")}".insteadOf "${org}" \u2014 or let pncli answer with github.token: pncli skills git-auth enable --host ${r.host}` : `Give git a credential for ${r.host} (a credential helper, or pncli skills git-auth enable --host ${r.host})`
1427
+ });
1428
+ } else if (!r.github) {
1429
+ problems.push({
1430
+ message: `The credential from ${where} (${r.token?.fingerprint}) cannot read ${r.effectiveUrl}: ${r.access.error}`,
1431
+ fix: "Check the token has not expired and its account can read this repo; replace it where it is mapped"
1432
+ });
1433
+ }
1434
+ }
1435
+ const g = r.github;
1436
+ if (g) {
1437
+ const t = g.token;
1438
+ const who = `The token from ${where} (${r.token?.fingerprint}${t.login ? `, ${t.login}` : ""})`;
1439
+ if (t.status === 401) problems.push({ message: `${who} was rejected \u2014 expired or revoked`, fix: "Generate a new token and replace it where it is mapped" });
1440
+ if (t.ssoAuthorizationRequired || g.repo?.ssoAuthorizationRequired) {
1441
+ problems.push({ message: `${who} is not authorized for the organization's SAML SSO`, fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize for the org" });
1442
+ }
1443
+ if (t.missingRepoScope) problems.push({ message: `${who} is a classic token without the "repo" scope, so it cannot read private repositories`, fix: 'Edit the token (Settings \u2192 Developer settings \u2192 Tokens (classic)) and tick "repo"' });
1444
+ if (g.repo && !g.repo.canRead && !g.repo.ssoAuthorizationRequired && t.valid) {
1445
+ problems.push({
1446
+ message: `${who} is valid but cannot see ${githubRepoSlug(r.effectiveUrl.replace(/\/\/[^@]+@/, "//"))} (HTTP ${g.repo.status})`,
1447
+ fix: r.token?.kind === "fine-grained" ? "Add this repository to the fine-grained token's repository access, or map a different token for it" : "The token's account needs access to this repo (or the org may block classic tokens); map a token from an account that has access"
1448
+ });
1449
+ }
1450
+ if (t.expiresInDays !== null && t.expiresInDays <= 14) {
1451
+ problems.push({ message: t.expiresInDays < 0 ? `${who} expired on ${t.expiresAt}` : `${who} expires in ${t.expiresInDays} day(s)`, fix: "Regenerate it and replace it where it is mapped" });
1452
+ }
1453
+ }
1454
+ return problems;
1455
+ }
1456
+ function listRemotes(cwd, git = defaultGitRunner) {
1457
+ const r = git(["-C", cwd, "config", "--get-regexp", "^remote\\..*\\.url$"]);
1458
+ if (r.status !== 0) return [];
1459
+ return r.stdout.split(/\r?\n/).map((l) => /^remote\.(.+)\.url (.+)$/.exec(l.trim())).filter((m) => !!m).map((m) => [m[1], m[2]]);
1460
+ }
1461
+ function findClones(root, depth) {
1462
+ const found = [];
1463
+ const walk = (dir, level) => {
1464
+ if (fs.existsSync(path.join(dir, ".git"))) {
1465
+ found.push(dir);
1466
+ return;
1467
+ }
1468
+ if (level >= depth) return;
1469
+ let entries;
1470
+ try {
1471
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1472
+ } catch {
1473
+ return;
1474
+ }
1475
+ for (const e of entries) {
1476
+ if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") walk(path.join(dir, e.name), level + 1);
1477
+ }
1478
+ };
1479
+ walk(path.resolve(root), 0);
1480
+ return found;
1481
+ }
1482
+ async function listMappings(cwd, cfg, deps = {}) {
1483
+ const git = gitIn(cwd, deps.git ?? defaultGitRunner);
1484
+ const fetchFn = deps.fetchFn ?? fetch;
1485
+ const reports = [];
1486
+ for (const m of parseInsteadOf(git(["config", "--get-regexp", "^url\\..*\\.insteadof$"]).stdout)) {
1487
+ const embedded = tokenFromUrl(m.rawTo);
1488
+ let host = null;
1489
+ try {
1490
+ host = new URL(m.rawTo).host.toLowerCase();
1491
+ } catch {
1492
+ }
1493
+ const provider = detectProvider(m.rawTo.replace(/\/\/[^@]+@/, "//"), cfg);
1494
+ let github = null;
1495
+ if (embedded && host && provider === "github" && !deps.offline) {
1496
+ github = await inspectGitHubToken(apiBaseForHost(host, cfg.github?.baseUrl), embedded.token, fetchFn);
1497
+ }
1498
+ reports.push({
1499
+ kind: "insteadOf",
1500
+ from: m.from,
1501
+ to: m.to,
1502
+ token: embedded ? { kind: provider === "github" ? classifyGitHubToken(embedded.token) : "unknown", fingerprint: fingerprint(embedded.token) } : null,
1503
+ github
1504
+ });
1505
+ }
1506
+ for (const h of parseScopedHelpers(git(["config", "--get-regexp", "^credential\\..*\\.helper$"]).stdout)) {
1507
+ reports.push({ kind: "helper", from: h.scope, to: redactSecrets(h.helper), token: null, github: null });
1508
+ }
1509
+ return reports;
1510
+ }
1511
+ function classifyHelper(helper) {
1512
+ const h = helper.trim().replace(/^!/, "").replace(/['"]/g, "").replace(/\\/g, "/");
1513
+ const exe = h.split(/\s+(?=[a-z-]+(\s|$))/i)[0].split("/").pop().replace(/\.exe$/i, "").toLowerCase();
1514
+ if (/^manager(-core)?(\s|$)/.test(h) || /git-credential-manager/i.test(h)) return "git-credential-manager";
1515
+ if (/^wincred(\s|$)/.test(h) || exe === "git-credential-wincred") return "wincred";
1516
+ if (/^osxkeychain(\s|$)/.test(h) || exe === "git-credential-osxkeychain") return "osxkeychain";
1517
+ if (/libsecret/i.test(h)) return "libsecret";
1518
+ if (/^store(\s|$)/.test(h)) return "store";
1519
+ if (/^cache(\s|$)/.test(h)) return "cache";
1520
+ if (/(^|\/)gh(\.exe)?\s+auth\s+git-credential/i.test(h)) return "gh";
1521
+ if (/(^|\/)pncli(\.cmd)?\s+skills\s+git-credential/i.test(h)) return "pncli";
1522
+ return "other";
1523
+ }
1524
+ function gcmCredentialStore(git, platform = process.platform, env = process.env) {
1525
+ const r = git(["config", "--get", "credential.credentialStore"]);
1526
+ if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
1527
+ if (env.GCM_CREDENTIAL_STORE) return env.GCM_CREDENTIAL_STORE;
1528
+ return platform === "win32" ? "wincredman" : platform === "darwin" ? "keychain" : "unset";
1529
+ }
1530
+ function parseGitTarget(target) {
1531
+ const m = /^git:(https?:\/\/.+)$/i.exec(target);
1532
+ if (!m) return null;
1533
+ try {
1534
+ const u = new URL(m[1]);
1535
+ const p = u.pathname.replace(/^\/+|\/+$/g, "");
1536
+ return { host: u.host.toLowerCase(), path: p || null, username: u.username ? decodeURIComponent(u.username) : null };
1537
+ } catch {
1538
+ return null;
1539
+ }
1540
+ }
1541
+ function parseGitCredentialsFile(content) {
1542
+ const out = [];
1543
+ for (const line of content.split(/\r?\n/)) {
1544
+ const t = line.trim();
1545
+ if (!t) continue;
1546
+ try {
1547
+ const u = new URL(t);
1548
+ const found = tokenFromUrl(t);
1549
+ if (!found) continue;
1550
+ const p = u.pathname.replace(/^\/+|\/+$/g, "");
1551
+ out.push({ host: u.host.toLowerCase(), path: p || null, username: found.username || null, secret: found.token });
1552
+ } catch {
1553
+ }
1554
+ }
1555
+ return out;
1556
+ }
1557
+ function parseMacKeychainDump(output) {
1558
+ const out = [];
1559
+ for (const block of output.split(/^keychain: /m)) {
1560
+ if (!/^class: "inet"/m.test(block)) continue;
1561
+ const attr = (name) => new RegExp(`"${name}"<[^>]+>="([^"]*)"`).exec(block)?.[1] ?? null;
1562
+ const protocol = attr("ptcl");
1563
+ if (protocol && protocol !== "htps" && protocol !== "http") continue;
1564
+ const host = attr("srvr");
1565
+ if (!host) continue;
1566
+ const p = attr("path");
1567
+ out.push({ host: host.toLowerCase(), path: p ? p.replace(/^\/+/, "") || null : null, username: attr("acct") });
1568
+ }
1569
+ return out;
1570
+ }
1571
+ function isGitHubHost(host, cfg) {
1572
+ return detectProvider(`https://${host}/`, cfg) === "github";
1573
+ }
1574
+ async function listStoredCredentials(hosts, cfg, deps = {}) {
1575
+ const git = deps.git ?? defaultGitRunner;
1576
+ const platform = deps.platform ?? process.platform;
1577
+ const env = deps.env ?? process.env;
1578
+ const fetchFn = deps.fetchFn ?? fetch;
1579
+ const run = deps.run ?? defaultRunner;
1580
+ const cache = deps.cache ?? /* @__PURE__ */ new Map();
1581
+ const readFile2 = deps.readFile ?? ((p) => {
1582
+ try {
1583
+ return fs.readFileSync(p, "utf8");
1584
+ } catch {
1585
+ return null;
1586
+ }
1587
+ });
1588
+ const home = deps.homedir ?? os.homedir();
1589
+ const helperOut = git(["config", "--get-all", "credential.helper"]);
1590
+ const rawHelpers = helperOut.status === 0 ? helperOut.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : [];
1591
+ const kinds = [...new Set(rawHelpers.map(classifyHelper))];
1592
+ const configured = rawHelpers.map(redactSecrets);
1593
+ const report = {
1594
+ helpers: { configured, kinds, gcmCredentialStore: kinds.includes("git-credential-manager") ? gcmCredentialStore(git, platform, env) : null },
1595
+ stores: [],
1596
+ probes: [],
1597
+ problems: []
1598
+ };
1599
+ const tokenInfo = (secret, host) => ({ kind: isGitHubHost(host, cfg) ? classifyGitHubToken(secret) : "unknown", fingerprint: fingerprint(secret) });
1600
+ if (platform === "win32") {
1601
+ try {
1602
+ const entries = [];
1603
+ for (const it of windowsListCredentials("git:*", run)) {
1604
+ const t = parseGitTarget(it.target);
1605
+ if (!t) continue;
1606
+ entries.push({
1607
+ store: "windows-credential-manager",
1608
+ host: t.host,
1609
+ path: t.path,
1610
+ username: it.user || t.username,
1611
+ token: it.secret ? tokenInfo(it.secret, t.host) : null,
1612
+ location: it.target,
1613
+ github: null,
1614
+ secret: it.secret || void 0
1615
+ });
1616
+ }
1617
+ report.stores.push({ store: "windows-credential-manager", available: true, error: null, entries });
1618
+ } catch (err) {
1619
+ report.stores.push({ store: "windows-credential-manager", available: false, error: err instanceof Error ? err.message : String(err), entries: [] });
1620
+ }
1621
+ }
1622
+ for (const file of [path.join(home, ".git-credentials"), path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "git", "credentials")]) {
1623
+ const content = readFile2(file);
1624
+ if (content === null) continue;
1625
+ const entries = parseGitCredentialsFile(content).map((e) => ({
1626
+ store: "git-credentials-file",
1627
+ host: e.host,
1628
+ path: e.path,
1629
+ username: e.username,
1630
+ token: tokenInfo(e.secret, e.host),
1631
+ location: file,
1632
+ github: null,
1633
+ secret: e.secret
1634
+ }));
1635
+ report.stores.push({ store: "git-credentials-file", available: true, error: null, entries });
1636
+ }
1637
+ if (platform === "darwin") {
1638
+ const r = run("security", ["dump-keychain"]);
1639
+ if (r.status === 0) {
1640
+ const entries = parseMacKeychainDump(r.stdout).map((e) => ({
1641
+ store: "macos-keychain",
1642
+ host: e.host,
1643
+ path: e.path,
1644
+ username: e.username,
1645
+ token: null,
1646
+ location: `login keychain: ${e.host}`,
1647
+ github: null
1648
+ }));
1649
+ report.stores.push({ store: "macos-keychain", available: true, error: null, entries });
1650
+ } else {
1651
+ report.stores.push({ store: "macos-keychain", available: false, error: r.stderr.trim() || `security exited ${r.status}`, entries: [] });
1652
+ }
1653
+ }
1654
+ const scopes = /* @__PURE__ */ new Map();
1655
+ for (const h of hosts) scopes.set(h.toLowerCase(), { host: h.toLowerCase(), path: null });
1656
+ for (const e of report.stores.flatMap((st) => st.entries)) scopes.set(e.path ? `${e.host}/${e.path}` : e.host, { host: e.host, path: e.path });
1657
+ const probeSecrets = /* @__PURE__ */ new Map();
1658
+ for (const [key, { host, path: p }] of [...scopes].sort(([a], [b]) => a.localeCompare(b))) {
1659
+ const filled = credentialFill(host, p ?? void 0, git);
1660
+ const h = git(["config", "--get-urlmatch", "credential.helper", `https://${host}/${p ?? ""}`]);
1661
+ const rawHelper = h.status === 0 ? h.stdout.trim().split(/\r?\n/).pop() ?? null : null;
1662
+ const helper = rawHelper ? redactSecrets(rawHelper) : null;
1663
+ if (filled) probeSecrets.set(key, filled.password);
1664
+ report.probes.push({
1665
+ host,
1666
+ path: p,
1667
+ username: filled?.username || null,
1668
+ token: filled ? tokenInfo(filled.password, host) : null,
1669
+ helper,
1670
+ helperKind: rawHelper ? classifyHelper(rawHelper) : null
1671
+ });
1672
+ }
1673
+ const shadowed = [];
1674
+ for (const store of report.stores) {
1675
+ for (const e of store.entries) {
1676
+ const key = e.path ? `${e.host}/${e.path}` : e.host;
1677
+ const sent = probeSecrets.get(key);
1678
+ const probe = report.probes.find((pr) => pr.host === e.host && pr.path === e.path);
1679
+ const siblings = store.entries.filter((x) => x.host === e.host && x.path === e.path).map((x) => x.secret);
1680
+ if (e.secret && sent && probe && !siblings.includes(sent)) shadowed.push({ entry: e, store: store.store, probe });
1681
+ }
1682
+ }
1683
+ if (!deps.offline) {
1684
+ for (const store of report.stores) {
1685
+ for (const e of store.entries) {
1686
+ if (!e.secret || !isGitHubHost(e.host, cfg)) continue;
1687
+ const apiBase = apiBaseForHost(e.host, cfg.github?.baseUrl);
1688
+ const key = `${apiBase}\0${e.secret}`;
1689
+ if (!cache.has(key)) cache.set(key, await inspectGitHubToken(apiBase, e.secret, fetchFn));
1690
+ e.github = cache.get(key);
1691
+ }
1692
+ }
1693
+ }
1694
+ report.problems = [...buildStoredProblems(report), ...shadowed.map(({ entry: e, store, probe }) => ({
1695
+ message: `git never sends the ${store} credential for ${e.path ? `${e.host}/${e.path}` : e.host} (${e.username ?? "no username"}, ${e.token?.fingerprint}): the ${probe.helperKind ?? "configured"} helper answers first with ${probe.username ?? "no username"}, ${probe.token?.fingerprint}`,
1696
+ fix: store === "windows-credential-manager" ? `If the stored one is obsolete, delete it: cmdkey /delete:"${e.location}" (git credential reject would go to the ${probe.helperKind ?? "other"} helper, not to this entry). If it should win instead, remove the helper entry from your gitconfig.` : `If the stored one is obsolete, remove it from ${e.location}; if it should win, remove the ${probe.helperKind ?? "other"} helper entry from your gitconfig.`
1697
+ }))];
1698
+ for (const store of report.stores) for (const e of store.entries) delete e.secret;
1699
+ return report;
1700
+ }
1701
+ function buildStoredProblems(r) {
1702
+ const problems = [];
1703
+ const forget = (e) => `pncli git credentials forget --host ${e.host}${e.username ? ` --username ${e.username}` : ""}${e.path ? ` --path ${e.path}` : ""}`;
1704
+ for (const store of r.stores) {
1705
+ for (const e of store.entries) {
1706
+ const who = `The ${store.store} credential for ${e.host}${e.path ? `/${e.path}` : ""} (${e.username ?? "no username"}, ${e.token?.fingerprint ?? "no secret"})`;
1707
+ const g = e.github;
1708
+ if (g?.status === 401) {
1709
+ problems.push({ message: `${who} is rejected by GitHub \u2014 expired or revoked. git keeps sending it until it is removed`, fix: forget(e) });
1710
+ } else if (g && g.expiresInDays !== null && g.expiresInDays <= 14) {
1711
+ problems.push({ message: g.expiresInDays < 0 ? `${who} expired on ${g.expiresAt}` : `${who} expires in ${g.expiresInDays} day(s)`, fix: `Replace it: ${forget(e)}, then sign in again` });
1712
+ }
1713
+ if (g?.missingRepoScope) problems.push({ message: `${who} is a classic token without the "repo" scope \u2014 it cannot read private repositories`, fix: `${forget(e)}, then store a token with "repo"` });
1714
+ if (g?.ssoAuthorizationRequired) problems.push({ message: `${who} is not authorized for the organization's SAML SSO`, fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize" });
1715
+ if (store.store === "git-credentials-file") {
1716
+ problems.push({ message: `${who} is stored in plaintext in ${e.location}`, fix: "Switch to an OS-backed helper (Windows: git config --global credential.helper manager; macOS: osxkeychain) and delete that line" });
1717
+ }
1718
+ }
1719
+ const byScope = /* @__PURE__ */ new Map();
1720
+ for (const e of store.entries) {
1721
+ const k = e.path ? `${e.host}/${e.path}` : e.host;
1722
+ if (!byScope.has(k)) byScope.set(k, /* @__PURE__ */ new Set());
1723
+ byScope.get(k).add(e.username ?? "");
1724
+ }
1725
+ for (const [k, users] of byScope) {
1726
+ if (users.size < 2) continue;
1727
+ problems.push({
1728
+ message: `${store.store} holds ${users.size} accounts for ${k} (${[...users].filter(Boolean).join(", ")}) \u2014 git may send a different one than you expect`,
1729
+ fix: `Pin the account: git config --global credential.https://${k}.username <account> (or remove the extra one with pncli git credentials forget)`
1730
+ });
1731
+ }
1732
+ }
1733
+ return problems;
1734
+ }
1735
+ function forgetCredential(host, opts = {}, git = defaultGitRunner) {
1736
+ if (opts.path) {
1737
+ const r = git(["config", "--get-urlmatch", "credential.useHttpPath", `https://${host}/${opts.path}`]);
1738
+ if (r.status !== 0 || r.stdout.trim().toLowerCase() !== "true") {
1739
+ throw new Error(`git ignores --path for ${host}: credential.useHttpPath is not enabled for that URL, so this would forget the host-wide credential. Omit --path to forget that deliberately.`);
1740
+ }
1741
+ }
1742
+ const before = credentialFill(host, opts.path, git);
1743
+ if (!before) return { host, before: null, after: null, removed: false };
1744
+ const username = opts.username ?? before?.username;
1745
+ const sameAsFilled = before && (!opts.username || opts.username === before.username);
1746
+ const fields = [
1747
+ "protocol=https",
1748
+ `host=${host}`,
1749
+ ...opts.path ? [`path=${opts.path}`] : [],
1750
+ ...username ? [`username=${username}`] : [],
1751
+ ...sameAsFilled ? [`password=${before.password}`] : []
1752
+ ];
1753
+ git(["credential", "reject"], { input: `${fields.join("\n")}
1754
+
1755
+ `, env: { GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" } });
1756
+ const after = credentialFill(host, opts.path, git);
1757
+ return {
1758
+ host,
1759
+ before: before ? fingerprint(before.password) : null,
1760
+ after: after ? fingerprint(after.password) : null,
1761
+ removed: !!before && (!after || after.password !== before.password)
1762
+ };
1763
+ }
1764
+
820
1765
  // src/services/git/commands.ts
1766
+ import fs2 from "fs";
1767
+ import path2 from "path";
821
1768
  function requireRepoRoot() {
822
1769
  const root = getRepoRoot();
823
1770
  if (!root) throw new PncliError("Not a git repository", 1);
@@ -915,11 +1862,136 @@ function registerGitCommands(program2) {
915
1862
  fail(err, "git", "current-pr", start);
916
1863
  }
917
1864
  });
1865
+ const credentials = git.command("credentials").description("Inspect which credential git uses for the remotes of a clone (insteadOf mappings, embedded tokens, helpers) and whether it can read the repo");
1866
+ credentials.addHelpText("after", `
1867
+ Read-only except forget. Tokens are never printed \u2014 only their type and a fingerprint (ghp_\u2026a1b2).
1868
+
1869
+ Examples:
1870
+ pncli git credentials inspect # every remote of the clone you are in
1871
+ pncli git credentials inspect ~/src/tools --remote upstream
1872
+ pncli git credentials inspect https://github.com/imagile/tools.git
1873
+ pncli git credentials inspect --scan ~/src --problems-only # clones mapped without a token, or without access
1874
+ pncli git credentials mappings # every insteadOf rewrite and URL-scoped helper, tokens checked
1875
+ pncli git credentials stored # Git Credential Manager / Windows Credential Manager /
1876
+ # Keychain / ~/.git-credentials: accounts, token health
1877
+ pncli git credentials forget --host github.com --username octo # drop a stale stored credential
1878
+ `);
1879
+ credentials.command("inspect").description("Show the credential each remote uses after insteadOf rewriting, and (online) whether it can read the repo").argument("[target]", "A clone directory or a remote URL (default: the current clone)").option("--remote <name>", "Only this remote (default: all remotes)").option("--scan <dir>", "Inspect every clone under this directory instead").option("--depth <n>", "How many directory levels --scan descends", "2").option("--problems-only", "With --scan, only list remotes that have problems").option("--offline", "Skip network checks (ls-remote and the GitHub API)").action(async (target, cmdOpts) => {
1880
+ const start = Date.now();
1881
+ try {
1882
+ const opts = program2.optsWithGlobals();
1883
+ const cfg = (() => {
1884
+ try {
1885
+ return loadConfig({ configPath: opts.config });
1886
+ } catch {
1887
+ return {};
1888
+ }
1889
+ })();
1890
+ const cache = /* @__PURE__ */ new Map();
1891
+ const deps = { offline: !!cmdOpts.offline, cache };
1892
+ const inspectClone = async (dir2) => {
1893
+ const remotes2 = listRemotes(dir2).filter(([name]) => !cmdOpts.remote || name === cmdOpts.remote);
1894
+ const out = [];
1895
+ for (const [name, url] of remotes2) out.push(await inspectRemoteAuth(name, url, dir2, cfg, deps));
1896
+ return out;
1897
+ };
1898
+ if (cmdOpts.scan) {
1899
+ const depth = Number.parseInt(cmdOpts.depth, 10);
1900
+ if (!Number.isInteger(depth) || depth < 0) throw new PncliError("--depth must be a non-negative integer", 1);
1901
+ const clones = findClones(cmdOpts.scan, depth);
1902
+ const results = [];
1903
+ for (const dir2 of clones) {
1904
+ const remotes2 = await inspectClone(dir2);
1905
+ const shown = cmdOpts.problemsOnly ? remotes2.filter((r) => r.problems.length > 0) : remotes2;
1906
+ if (shown.length > 0) results.push({ path: dir2, remotes: shown });
1907
+ }
1908
+ const all = results.flatMap((r) => r.remotes);
1909
+ success({
1910
+ root: path2.resolve(cmdOpts.scan),
1911
+ clonesScanned: clones.length,
1912
+ summary: {
1913
+ remotes: all.length,
1914
+ withProblems: all.filter((r) => r.problems.length > 0).length,
1915
+ noCredential: all.filter((r) => r.host && r.credentialSource === "none").length,
1916
+ cannotRead: all.filter((r) => r.access && !r.access.ok).length
1917
+ },
1918
+ clones: results
1919
+ }, "git", "credentials-inspect", start);
1920
+ return;
1921
+ }
1922
+ if (target && /^(https?:\/\/|git@|ssh:\/\/)/i.test(target)) {
1923
+ success({ remotes: [await inspectRemoteAuth(null, target, null, cfg, deps)] }, "git", "credentials-inspect", start);
1924
+ return;
1925
+ }
1926
+ const dir = target ? path2.resolve(target) : requireRepoRoot();
1927
+ if (!fs2.existsSync(path2.join(dir, ".git"))) throw new PncliError(`${dir} is not a git clone`, 1);
1928
+ const remotes = await inspectClone(dir);
1929
+ if (remotes.length === 0) throw new PncliError(cmdOpts.remote ? `No remote named "${cmdOpts.remote}" in ${dir}` : `${dir} has no remotes`, 1);
1930
+ success({ path: dir, remotes }, "git", "credentials-inspect", start);
1931
+ } catch (err) {
1932
+ fail(err, "git", "credentials-inspect", start);
1933
+ }
1934
+ });
1935
+ credentials.command("mappings").description("List every insteadOf URL rewrite and URL-scoped credential helper in your gitconfig, tokens redacted and (online) validated").option("--offline", "Skip validating embedded GitHub tokens").action(async (cmdOpts) => {
1936
+ const start = Date.now();
1937
+ try {
1938
+ const opts = program2.optsWithGlobals();
1939
+ const cfg = (() => {
1940
+ try {
1941
+ return loadConfig({ configPath: opts.config });
1942
+ } catch {
1943
+ return {};
1944
+ }
1945
+ })();
1946
+ const mappings = await listMappings(getRepoRoot(), cfg, { offline: !!cmdOpts.offline });
1947
+ success({ mappings, total: mappings.length }, "git", "credentials-mappings", start);
1948
+ } catch (err) {
1949
+ fail(err, "git", "credentials-mappings", start);
1950
+ }
1951
+ });
1952
+ credentials.command("stored").description("List credentials git keeps in Git Credential Manager, Windows Credential Manager, the macOS Keychain, or ~/.git-credentials, with what git would send per host and (online) token health").option("--host <host...>", "Also ask git what it would send for these hosts").option("--offline", "Skip validating GitHub tokens").action(async (cmdOpts) => {
1953
+ const start = Date.now();
1954
+ try {
1955
+ const opts = program2.optsWithGlobals();
1956
+ const cfg = (() => {
1957
+ try {
1958
+ return loadConfig({ configPath: opts.config });
1959
+ } catch {
1960
+ return {};
1961
+ }
1962
+ })();
1963
+ const hosts = new Set((cmdOpts.host ?? []).map((h) => h.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "")));
1964
+ for (const h of [gitHostFromApiBaseUrl(cfg.github?.baseUrl), hostOf(cfg.bitbucket?.baseUrl), hostOf(cfg.ado?.baseUrl)]) if (h) hosts.add(h);
1965
+ const root = getRepoRoot();
1966
+ if (root) for (const [, url] of listRemotes(root)) {
1967
+ const h = hostOf(url);
1968
+ if (h) hosts.add(h);
1969
+ }
1970
+ const report = await listStoredCredentials([...hosts], cfg, { offline: !!cmdOpts.offline });
1971
+ success(report, "git", "credentials-stored", start);
1972
+ } catch (err) {
1973
+ fail(err, "git", "credentials-stored", start);
1974
+ }
1975
+ });
1976
+ credentials.command("forget").description("Remove a stored credential for a host through git credential reject (works for every helper: Git Credential Manager, wincred, osxkeychain, libsecret, store)").requiredOption("--host <host>", "Host to forget, e.g. github.com").option("--username <username>", "Only this account (when several are stored for the host)").option("--path <path>", "Repository path, for helpers that store per repository (e.g. Azure Repos in Git Credential Manager)").action((cmdOpts) => {
1977
+ const start = Date.now();
1978
+ try {
1979
+ const host = cmdOpts.host.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
1980
+ const result = forgetCredential(host, { username: cmdOpts.username, path: cmdOpts.path });
1981
+ if (!result.before) throw new PncliError(`git has no stored credential for ${host}${cmdOpts.path ? `/${cmdOpts.path}` : ""} \u2014 nothing to forget`, 1);
1982
+ success({
1983
+ ...result,
1984
+ next: result.removed ? "git will ask for (or its helper will fetch) a fresh credential next time" : "git still supplies the same credential \u2014 it may come from an insteadOf mapping or a helper that ignores reject; see: pncli git credentials inspect"
1985
+ }, "git", "credentials-forget", start);
1986
+ } catch (err) {
1987
+ fail(err, "git", "credentials-forget", start);
1988
+ }
1989
+ });
918
1990
  }
919
1991
 
920
1992
  // src/services/jira/commands.ts
921
1993
  import { readFileSync as readFileSync3, existsSync, writeFileSync, mkdirSync } from "fs";
922
- import path from "path";
1994
+ import path3 from "path";
923
1995
 
924
1996
  // src/services/jira/client.ts
925
1997
  import { readFileSync } from "fs";
@@ -1026,6 +2098,16 @@ var JiraClient = class {
1026
2098
  return { ...result, values: result.comments };
1027
2099
  });
1028
2100
  }
2101
+ async addWorklog(key, opts) {
2102
+ return this.http.jira(`${API2}/issue/${key}/worklog`, {
2103
+ method: "POST",
2104
+ body: {
2105
+ timeSpent: opts.timeSpent,
2106
+ ...opts.comment ? { comment: opts.comment } : {},
2107
+ ...opts.started ? { started: opts.started } : {}
2108
+ }
2109
+ });
2110
+ }
1029
2111
  async search(jql, maxResults, customFields) {
1030
2112
  const standardFields = ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"];
1031
2113
  const fields = customFields?.length ? [...standardFields, ...customFields.map((f) => f.id)] : standardFields;
@@ -1239,6 +2321,11 @@ function buildFieldMap(fields) {
1239
2321
  }
1240
2322
  return { byName, byId };
1241
2323
  }
2324
+ function resolveFieldKey(rawKey, fieldMap) {
2325
+ const def = fieldMap.byName.get(rawKey.toLowerCase()) ?? fieldMap.byId.get(rawKey);
2326
+ if (def) return def.id;
2327
+ return /\s/.test(rawKey) ? void 0 : rawKey;
2328
+ }
1242
2329
  function translateJql(jql, fieldMap) {
1243
2330
  if (fieldMap.byName.size === 0) return jql;
1244
2331
  const sortedNames = Array.from(fieldMap.byName.keys()).sort((a, b) => b.length - a.length);
@@ -1383,17 +2470,14 @@ function splitFieldsDictionary(fields, fieldMap) {
1383
2470
  builtin[key] = value;
1384
2471
  continue;
1385
2472
  }
1386
- const def = fieldMap.byName.get(key) ?? fieldMap.byId.get(rawKey);
1387
- if (def) {
1388
- custom[def.id] = value;
1389
- } else if (!/\s/.test(rawKey)) {
1390
- custom[rawKey] = value;
1391
- } else {
2473
+ const fieldId = resolveFieldKey(rawKey, fieldMap);
2474
+ if (fieldId === void 0) {
1392
2475
  throw new PncliError(
1393
2476
  `Unknown custom field: "${rawKey}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1394
2477
  1
1395
2478
  );
1396
2479
  }
2480
+ custom[fieldId] = value;
1397
2481
  }
1398
2482
  return { builtin, custom };
1399
2483
  }
@@ -1574,6 +2658,16 @@ function registerJiraCommands(program2) {
1574
2658
  fail(err, "jira", "add-comment", start);
1575
2659
  }
1576
2660
  });
2661
+ jira.command("log-work").description("Add a worklog (time spent) entry to a Jira issue").requiredOption("--key <issue-key>", "Issue key").requiredOption("--time-spent <duration>", 'Time spent in Jira duration format (e.g. "2h 30m", "1d")').option("--comment <text>", "Worklog comment").option("--started <datetime>", "When the work started, in Jira datetime format (e.g. 2024-01-15T09:00:00.000+0000); defaults to now").action(async (opts) => {
2662
+ const start = Date.now();
2663
+ try {
2664
+ const client = getClient(program2);
2665
+ const data = await client.addWorklog(opts.key, { timeSpent: opts.timeSpent, comment: opts.comment, started: opts.started });
2666
+ success(data, "jira", "log-work", start);
2667
+ } catch (err) {
2668
+ fail(err, "jira", "log-work", start);
2669
+ }
2670
+ });
1577
2671
  jira.command("list-comments").description("List comments on a Jira issue").requiredOption("--key <issue-key>", "Issue key").action(async (opts) => {
1578
2672
  const start = Date.now();
1579
2673
  try {
@@ -1671,9 +2765,9 @@ function registerJiraCommands(program2) {
1671
2765
  const attachments = await client.listAttachments(opts.key);
1672
2766
  const attachment = attachments.find((a) => a.id === opts.attachmentId);
1673
2767
  if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
1674
- const outDir = opts.dir ?? path.join(process.cwd(), ".pncli");
2768
+ const outDir = opts.dir ?? path3.join(process.cwd(), ".pncli");
1675
2769
  mkdirSync(outDir, { recursive: true });
1676
- const outPath = path.join(outDir, path.basename(attachment.filename));
2770
+ const outPath = path3.join(outDir, path3.basename(attachment.filename));
1677
2771
  const buffer = await client.downloadAttachment(attachment.content);
1678
2772
  writeFileSync(outPath, buffer);
1679
2773
  success({ saved: outPath, filename: attachment.filename, size: buffer.length }, "jira", "download-attachment", start);
@@ -1738,7 +2832,8 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1738
2832
  const name = arg.slice(0, eq).trim();
1739
2833
  const value = arg.slice(eq + 1);
1740
2834
  const def = fieldMap.byName.get(name.toLowerCase()) ?? fieldMap.byId.get(name);
1741
- if (!def) throw new PncliError(
2835
+ const fieldId = resolveFieldKey(name, fieldMap);
2836
+ if (fieldId === void 0) throw new PncliError(
1742
2837
  `Unknown custom field: "${name}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1743
2838
  1
1744
2839
  );
@@ -1751,12 +2846,12 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1751
2846
  throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
1752
2847
  }
1753
2848
  try {
1754
- result[def.id] = JSON.parse(raw);
2849
+ result[fieldId] = JSON.parse(raw);
1755
2850
  } catch {
1756
- result[def.id] = raw;
2851
+ result[fieldId] = raw;
1757
2852
  }
1758
2853
  } else {
1759
- result[def.id] = formatFieldValue(value, def.type);
2854
+ result[fieldId] = def ? formatFieldValue(value, def.type) : value;
1760
2855
  }
1761
2856
  }
1762
2857
  return result;
@@ -1779,12 +2874,12 @@ function parseFieldsFile(filePath, fieldMap) {
1779
2874
  }
1780
2875
  const result = {};
1781
2876
  for (const [key, val] of Object.entries(data)) {
1782
- const def = fieldMap.byName.get(key.toLowerCase()) ?? fieldMap.byId.get(key);
1783
- if (!def) throw new PncliError(
2877
+ const fieldId = resolveFieldKey(key, fieldMap);
2878
+ if (fieldId === void 0) throw new PncliError(
1784
2879
  `Unknown field in "${filePath}": "${key}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1785
2880
  1
1786
2881
  );
1787
- result[def.id] = val;
2882
+ result[fieldId] = val;
1788
2883
  }
1789
2884
  return result;
1790
2885
  }
@@ -2306,8 +3401,8 @@ var GitHubClient = class {
2306
3401
  );
2307
3402
  }
2308
3403
  async createRepo(opts) {
2309
- const path11 = opts.org ? `/orgs/${opts.org}/repos` : "/user/repos";
2310
- return this.http.github(path11, {
3404
+ const path14 = opts.org ? `/orgs/${opts.org}/repos` : "/user/repos";
3405
+ return this.http.github(path14, {
2311
3406
  method: "POST",
2312
3407
  body: {
2313
3408
  name: opts.name,
@@ -3149,7 +4244,7 @@ function registerGitHubCommands(program2) {
3149
4244
 
3150
4245
  // src/services/confluence/commands.ts
3151
4246
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3152
- import path2 from "path";
4247
+ import path4 from "path";
3153
4248
 
3154
4249
  // src/services/confluence/client.ts
3155
4250
  import { readFileSync as readFileSync4 } from "fs";
@@ -3346,7 +4441,7 @@ var ConfluenceClient = class {
3346
4441
  function resolveAttachmentPath(outDir, title) {
3347
4442
  const segments = title.split(/[\\/]/).filter((s) => s !== "" && s !== "." && s !== "..");
3348
4443
  const name = segments.pop() ?? "attachment";
3349
- return path2.join(outDir, name);
4444
+ return path4.join(outDir, name);
3350
4445
  }
3351
4446
  function xmlParseHint(err, bodyContent, convertedFromMarkdown = false) {
3352
4447
  if (!(err instanceof Error)) return null;
@@ -3628,7 +4723,7 @@ ${hint}`, status, url), "confluence", "update-page", start);
3628
4723
  try {
3629
4724
  const client = getClient4(program2);
3630
4725
  const attachment = await client.getAttachment(opts.id);
3631
- const outDir = opts.dir ?? path2.join(process.cwd(), ".pncli");
4726
+ const outDir = opts.dir ?? path4.join(process.cwd(), ".pncli");
3632
4727
  mkdirSync2(outDir, { recursive: true });
3633
4728
  const outPath = resolveAttachmentPath(outDir, attachment.title);
3634
4729
  const buffer = await client.downloadAttachment(attachment);
@@ -4200,24 +5295,24 @@ function registerSdeCommands(program2) {
4200
5295
  import { Option as Option2 } from "commander";
4201
5296
 
4202
5297
  // src/services/deps/parsers/index.ts
4203
- import fs from "fs";
4204
- import path6 from "path";
4205
- import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
5298
+ import fs3 from "fs";
5299
+ import path8 from "path";
5300
+ import { execSync as execSync3, execFileSync as execFileSync3 } from "child_process";
4206
5301
 
4207
5302
  // src/services/deps/parsers/npm.ts
4208
- import path3 from "path";
5303
+ import path5 from "path";
4209
5304
  function findNpmManifests(files) {
4210
5305
  const lockFiles = new Set(
4211
5306
  files.filter((f) => {
4212
- const base = path3.basename(f);
5307
+ const base = path5.basename(f);
4213
5308
  return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
4214
5309
  })
4215
5310
  );
4216
5311
  const manifests = [];
4217
5312
  for (const file of files) {
4218
- if (path3.basename(file) !== "package.json") continue;
5313
+ if (path5.basename(file) !== "package.json") continue;
4219
5314
  if (file.split("/").includes("node_modules")) continue;
4220
- const dir = path3.dirname(file);
5315
+ const dir = path5.dirname(file);
4221
5316
  let lockFile;
4222
5317
  for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
4223
5318
  const candidate = dir === "." ? lf : `${dir}/${lf}`;
@@ -4232,7 +5327,7 @@ function findNpmManifests(files) {
4232
5327
  }
4233
5328
  function parseNpmPackages(content, manifest, opts, lockContent) {
4234
5329
  if (lockContent && manifest.lockFile) {
4235
- const base = path3.basename(manifest.lockFile);
5330
+ const base = path5.basename(manifest.lockFile);
4236
5331
  if (base === "package-lock.json") {
4237
5332
  const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
4238
5333
  if (pkgs.length > 0) return pkgs;
@@ -4380,26 +5475,26 @@ function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
4380
5475
  }
4381
5476
 
4382
5477
  // src/services/deps/parsers/nuget.ts
4383
- import path4 from "path";
5478
+ import path6 from "path";
4384
5479
  var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
4385
5480
  function findNugetManifests(files) {
4386
5481
  const lockFiles = /* @__PURE__ */ new Map();
4387
5482
  for (const f of files) {
4388
- if (path4.basename(f) === "packages.lock.json") {
4389
- lockFiles.set(path4.dirname(f), f);
5483
+ if (path6.basename(f) === "packages.lock.json") {
5484
+ lockFiles.set(path6.dirname(f), f);
4390
5485
  }
4391
5486
  }
4392
5487
  const manifests = [];
4393
5488
  for (const file of files) {
4394
- const base = path4.basename(file);
4395
- const ext = path4.extname(file);
5489
+ const base = path6.basename(file);
5490
+ const ext = path6.extname(file);
4396
5491
  if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
4397
- const dir = path4.dirname(file);
5492
+ const dir = path6.dirname(file);
4398
5493
  const lockFile = lockFiles.get(dir);
4399
5494
  manifests.push({ file, ecosystem: "nuget", lockFile });
4400
5495
  }
4401
5496
  if (base === "Directory.Packages.props") {
4402
- const dir = path4.dirname(file);
5497
+ const dir = path6.dirname(file);
4403
5498
  manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
4404
5499
  }
4405
5500
  }
@@ -4418,7 +5513,7 @@ function findNugetManifests(files) {
4418
5513
  return deduped;
4419
5514
  }
4420
5515
  function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
4421
- const base = path4.basename(manifest.file);
5516
+ const base = path6.basename(manifest.file);
4422
5517
  if (lockContent) {
4423
5518
  return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
4424
5519
  }
@@ -4539,14 +5634,14 @@ function parsePackagesLock(content, filePath, opts) {
4539
5634
  }
4540
5635
 
4541
5636
  // src/services/deps/parsers/maven.ts
4542
- import path5 from "path";
5637
+ import path7 from "path";
4543
5638
  function findMavenManifests(files) {
4544
- const lockFiles = new Set(files.filter((f) => path5.basename(f) === "gradle.lockfile"));
5639
+ const lockFiles = new Set(files.filter((f) => path7.basename(f) === "gradle.lockfile"));
4545
5640
  const manifests = [];
4546
5641
  for (const file of files) {
4547
- const base = path5.basename(file);
5642
+ const base = path7.basename(file);
4548
5643
  if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
4549
- const dir = path5.dirname(file);
5644
+ const dir = path7.dirname(file);
4550
5645
  const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
4551
5646
  const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
4552
5647
  manifests.push({ file, ecosystem: "maven", lockFile });
@@ -4555,7 +5650,7 @@ function findMavenManifests(files) {
4555
5650
  return manifests;
4556
5651
  }
4557
5652
  function parseMavenPackages(content, manifest, opts, lockContent) {
4558
- const base = path5.basename(manifest.file);
5653
+ const base = path7.basename(manifest.file);
4559
5654
  if (lockContent && manifest.lockFile) {
4560
5655
  return parseGradleLock(lockContent, manifest.lockFile, opts);
4561
5656
  }
@@ -4716,14 +5811,14 @@ function getRepoFiles(repoRoot) {
4716
5811
  }
4717
5812
  function readFile(repoRoot, relPath) {
4718
5813
  try {
4719
- return fs.readFileSync(path6.join(repoRoot, relPath), "utf8");
5814
+ return fs3.readFileSync(path8.join(repoRoot, relPath), "utf8");
4720
5815
  } catch {
4721
5816
  return null;
4722
5817
  }
4723
5818
  }
4724
5819
  function readFileAtRef(repoRoot, ref, relPath) {
4725
5820
  try {
4726
- return execFileSync2("git", ["show", `${ref}:${relPath}`], {
5821
+ return execFileSync3("git", ["show", `${ref}:${relPath}`], {
4727
5822
  encoding: "utf8",
4728
5823
  cwd: repoRoot,
4729
5824
  stdio: ["pipe", "pipe", "pipe"],
@@ -4734,13 +5829,13 @@ function readFileAtRef(repoRoot, ref, relPath) {
4734
5829
  }
4735
5830
  }
4736
5831
  function findPropsContent(manifestFile, readFn) {
4737
- let dir = path6.dirname(manifestFile);
5832
+ let dir = path8.dirname(manifestFile);
4738
5833
  for (let depth = 0; depth < 10; depth++) {
4739
5834
  const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
4740
5835
  const content = readFn(candidate);
4741
5836
  if (content) return content;
4742
5837
  if (dir === ".") break;
4743
- dir = path6.dirname(dir);
5838
+ dir = path8.dirname(dir);
4744
5839
  }
4745
5840
  return null;
4746
5841
  }
@@ -4784,7 +5879,7 @@ function scanRepoAtRef(repoRoot, ref, opts = {}) {
4784
5879
  const eco = opts.ecosystem ?? "all";
4785
5880
  let files = [];
4786
5881
  try {
4787
- const out = execFileSync2("git", ["ls-tree", "-r", "--name-only", ref], {
5882
+ const out = execFileSync3("git", ["ls-tree", "-r", "--name-only", ref], {
4788
5883
  encoding: "utf8",
4789
5884
  cwd: repoRoot,
4790
5885
  stdio: ["pipe", "pipe", "pipe"],
@@ -5972,7 +7067,7 @@ function registerDepsCommands(program2) {
5972
7067
 
5973
7068
  // src/services/config/commands.ts
5974
7069
  import input2 from "@inquirer/input";
5975
- import password from "@inquirer/password";
7070
+ import password2 from "@inquirer/password";
5976
7071
  import confirm from "@inquirer/confirm";
5977
7072
 
5978
7073
  // src/services/ado/client/work.ts
@@ -6272,8 +7367,8 @@ async function runCredentialChecks(cfg, http) {
6272
7367
  } else {
6273
7368
  try {
6274
7369
  const collection = cfg.defaults.ado?.collection;
6275
- const path11 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
6276
- await http.ado(path11, { timeoutMs: 1e4 });
7370
+ const path14 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
7371
+ await http.ado(path14, { timeoutMs: 1e4 });
6277
7372
  results.ado = { status: "valid", message: "ok" };
6278
7373
  } catch (err) {
6279
7374
  results.ado = categorize(err);
@@ -6478,44 +7573,304 @@ async function runCredentialChecks(cfg, http) {
6478
7573
  await http.splitio("/internal/api/v2/workspaces", { timeoutMs: 1e4 });
6479
7574
  results.splitio = { status: "valid", message: "ok" };
6480
7575
  } catch (err) {
6481
- results.splitio = categorize(err);
7576
+ results.splitio = categorize(err);
7577
+ }
7578
+ }
7579
+ if (!cfg.figma.token) {
7580
+ results.figma = { status: "blank", message: "not configured" };
7581
+ } else if (!cfg.figma.baseUrl) {
7582
+ results.figma = { status: "error", message: "baseUrl not configured" };
7583
+ } else {
7584
+ try {
7585
+ await http.figma("/v1/me", { timeoutMs: 1e4 });
7586
+ results.figma = { status: "valid", message: "ok" };
7587
+ } catch (err) {
7588
+ results.figma = categorize(err);
7589
+ }
7590
+ }
7591
+ if (!cfg.alation.refreshToken) {
7592
+ results.alation = { status: "blank", message: "not configured" };
7593
+ } else if (!cfg.alation.baseUrl) {
7594
+ results.alation = { status: "error", message: "baseUrl not configured" };
7595
+ } else if (cfg.alation.userId === void 0 || cfg.alation.userId === "") {
7596
+ results.alation = { status: "error", message: "userId not configured" };
7597
+ } else {
7598
+ try {
7599
+ await validateAlationAccessToken(cfg);
7600
+ results.alation = { status: "valid", message: "ok" };
7601
+ } catch (err) {
7602
+ results.alation = categorize(err);
7603
+ }
7604
+ }
7605
+ return results;
7606
+ }
7607
+
7608
+ // src/services/config/keychain-commands.ts
7609
+ import fs4 from "fs";
7610
+ import password from "@inquirer/password";
7611
+ function resolveConfigKey(root, key) {
7612
+ const parts = key.split(".").filter(Boolean);
7613
+ if (parts.length === 0) throw new KeychainError("A config key is required, e.g. github.token");
7614
+ const segments = [];
7615
+ let node = root;
7616
+ for (const [i, part] of parts.entries()) {
7617
+ if (Array.isArray(node)) {
7618
+ const idx = /^\d+$/.test(part) ? Number(part) : node.findIndex((e) => e && typeof e === "object" && e.name === part);
7619
+ if (idx < 0 || idx >= node.length) {
7620
+ throw new KeychainError(`No entry "${part}" in ${parts.slice(0, i).join(".")} \u2014 check the name with: pncli config show`);
7621
+ }
7622
+ segments.push(idx);
7623
+ node = node[idx];
7624
+ } else {
7625
+ segments.push(part);
7626
+ const obj = node;
7627
+ if (i < parts.length - 1 && (typeof obj[part] !== "object" || obj[part] === null)) obj[part] = {};
7628
+ node = obj[part];
7629
+ }
7630
+ }
7631
+ return segments;
7632
+ }
7633
+ function getAtPath(root, segments) {
7634
+ let node = root;
7635
+ for (const seg of segments) node = node && typeof node === "object" ? node[seg] : void 0;
7636
+ return node;
7637
+ }
7638
+ function deleteAtPath(root, segments) {
7639
+ const parent = getAtPath(root, segments.slice(0, -1));
7640
+ if (parent) delete parent[segments[segments.length - 1]];
7641
+ }
7642
+ async function readSecret(key, opts) {
7643
+ if (opts.value !== void 0) {
7644
+ warn("--value leaves the secret in your shell history. Prefer the interactive prompt or --stdin.");
7645
+ return opts.value;
7646
+ }
7647
+ if (opts.stdin || !process.stdin.isTTY) {
7648
+ const chunks = [];
7649
+ for await (const chunk of process.stdin) chunks.push(chunk);
7650
+ return Buffer.concat(chunks).toString("utf8").trim();
7651
+ }
7652
+ return (await password({ message: `Secret for ${key}:`, mask: "*" })).trim();
7653
+ }
7654
+ function matchesFilter(config, segments, only) {
7655
+ if (!only || only.size === 0) return true;
7656
+ return only.has(segments.join(".")) || only.has(accountForPath(config, segments));
7657
+ }
7658
+ function unmigrateSecrets(config, backend, dryRun = false, only) {
7659
+ const migrated = [];
7660
+ const failed = [];
7661
+ const refs = findKeychainRefs(config).filter((r) => matchesFilter(config, r.path, only));
7662
+ let secrets = {};
7663
+ if (!dryRun && refs.length > 0) {
7664
+ const accounts = [...new Set(refs.flatMap((r) => {
7665
+ try {
7666
+ return [accountFromRef(r.value)];
7667
+ } catch {
7668
+ return [];
7669
+ }
7670
+ }))];
7671
+ secrets = backend.getMany(accounts);
7672
+ }
7673
+ for (const ref of refs) {
7674
+ let account;
7675
+ try {
7676
+ account = accountFromRef(ref.value);
7677
+ } catch (err) {
7678
+ failed.push({ path: ref.path.join("."), account: ref.value, reason: err.message });
7679
+ continue;
7680
+ }
7681
+ const entry = { path: ref.path.join("."), account };
7682
+ if (dryRun) {
7683
+ migrated.push(entry);
7684
+ continue;
7685
+ }
7686
+ const secret = secrets[account];
7687
+ if (secret == null) {
7688
+ failed.push({ ...entry, reason: "no entry in the keychain" });
7689
+ continue;
7690
+ }
7691
+ setAtPath(config, ref.path, secret);
7692
+ migrated.push(entry);
7693
+ }
7694
+ return { migrated, failed };
7695
+ }
7696
+ function migrateSecrets(config, backend, dryRun = false, only) {
7697
+ const migrated = [];
7698
+ const failed = [];
7699
+ for (const secret of findPlaintextSecrets(config).filter((s) => matchesFilter(config, s.path, only))) {
7700
+ const account = accountForPath(config, secret.path);
7701
+ const entry = { path: secret.path.join("."), account };
7702
+ if (dryRun) {
7703
+ migrated.push(entry);
7704
+ continue;
7705
+ }
7706
+ try {
7707
+ storeVerified(backend, account, secret.value);
7708
+ setAtPath(config, secret.path, keychainRef(account));
7709
+ migrated.push(entry);
7710
+ } catch (err) {
7711
+ failed.push({ ...entry, reason: err instanceof Error ? err.message : String(err) });
7712
+ }
7713
+ }
7714
+ return { migrated, failed };
7715
+ }
7716
+ function registerKeychainCommands(config, program2) {
7717
+ const keychain = config.command("keychain").description("Store credentials in the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service) instead of plaintext config");
7718
+ keychain.addHelpText("after", `
7719
+ How it works:
7720
+ A config value of the form "keychain:<account>" is a reference. pncli looks the secret up in the
7721
+ OS credential store at run time; ~/.pncli/config.json only ever holds the reference.
7722
+ PNCLI_* environment variables still take precedence over anything stored here.
7723
+
7724
+ Examples:
7725
+ pncli config keychain migrate --dry-run # which plaintext secrets would move
7726
+ pncli config keychain migrate # config -> keychain, every plaintext secret
7727
+ pncli config keychain migrate github.token # just one key
7728
+ pncli config keychain migrate --to config # keychain -> config (entries kept)
7729
+ pncli config keychain migrate --to config --purge # keychain -> config, then delete entries
7730
+ pncli config keychain set github.token # prompts for the secret (or pipe it: --stdin)
7731
+ pncli config keychain set marketplaces.internal-ai.token
7732
+ pncli config keychain status # backend, every reference, and whether it resolves
7733
+ pncli config keychain remove github.token --restore # back to plaintext config
7734
+
7735
+ Set PNCLI_KEYCHAIN_BACKEND=none to ignore references entirely (e.g. on a CI runner).
7736
+ `);
7737
+ keychain.command("status").description("Show the keychain backend, every keychain reference in config, and plaintext secrets that could move").action(() => {
7738
+ const start = Date.now();
7739
+ try {
7740
+ const opts = program2.optsWithGlobals();
7741
+ const raw = loadJsonFile(getGlobalConfigPath(opts.config)) ?? {};
7742
+ const backend = getKeychainBackend();
7743
+ const refs = findKeychainRefs(raw);
7744
+ const available = backend.available();
7745
+ let lookups = {};
7746
+ let lookupError = null;
7747
+ if (available && refs.length > 0) {
7748
+ try {
7749
+ lookups = backend.getMany([...new Set(refs.map((r) => accountFromRef(r.value)))]);
7750
+ } catch (err) {
7751
+ if (err instanceof KeychainBatchError) lookups = err.partial;
7752
+ lookupError = err instanceof Error ? err.message : String(err);
7753
+ }
7754
+ }
7755
+ const plaintext = findPlaintextSecrets(raw).map((s) => s.path.join("."));
7756
+ success({
7757
+ backend: backend.name,
7758
+ store: backend.store,
7759
+ available,
7760
+ references: refs.map((r) => {
7761
+ const account = r.value.slice("keychain:".length);
7762
+ return { path: r.path.join("."), account, resolves: available ? lookups[account] != null : false };
7763
+ }),
7764
+ lookupError,
7765
+ plaintextSecrets: plaintext,
7766
+ hint: plaintext.length > 0 && available ? `Move ${plaintext.length} plaintext secret(s) into the keychain with: pncli config keychain migrate` : null
7767
+ }, "config", "keychain-status", start);
7768
+ } catch (err) {
7769
+ fail(err, "config", "keychain-status", start);
7770
+ }
7771
+ });
7772
+ keychain.command("set").description("Store a secret in the OS keychain and point a config key at it").argument("<key>", "Config key in dot notation, e.g. github.token or marketplaces.<name>.token").option("--stdin", "Read the secret from stdin instead of prompting").option("--value <secret>", "Pass the secret inline (ends up in shell history \u2014 prefer the prompt or --stdin)").option("--account <account>", "Keychain account name to store under (default: the config key)").action(async (key, cmdOpts) => {
7773
+ const start = Date.now();
7774
+ try {
7775
+ const opts = program2.optsWithGlobals();
7776
+ const configPath = getGlobalConfigPath(opts.config);
7777
+ const raw = loadJsonFile(configPath) ?? {};
7778
+ const segments = resolveConfigKey(raw, key);
7779
+ const account = cmdOpts.account ?? accountForPath(raw, segments);
7780
+ const secret = await readSecret(key, cmdOpts);
7781
+ const backend = getKeychainBackend();
7782
+ storeVerified(backend, account, secret);
7783
+ setAtPath(raw, segments, keychainRef(account));
7784
+ writeGlobalConfig(raw, opts.config);
7785
+ resetKeychainCache();
7786
+ success({ key: segments.join("."), account, store: backend.store, reference: keychainRef(account) }, "config", "keychain-set", start);
7787
+ } catch (err) {
7788
+ fail(err, "config", "keychain-set", start);
6482
7789
  }
6483
- }
6484
- if (!cfg.figma.token) {
6485
- results.figma = { status: "blank", message: "not configured" };
6486
- } else if (!cfg.figma.baseUrl) {
6487
- results.figma = { status: "error", message: "baseUrl not configured" };
6488
- } else {
7790
+ });
7791
+ keychain.command("remove").description("Delete a secret from the OS keychain and clear its config reference").argument("<key>", "Config key holding a keychain reference, e.g. github.token").option("--restore", "Write the secret back into config as plaintext instead of clearing the key").action((key, cmdOpts) => {
7792
+ const start = Date.now();
6489
7793
  try {
6490
- await http.figma("/v1/me", { timeoutMs: 1e4 });
6491
- results.figma = { status: "valid", message: "ok" };
7794
+ const opts = program2.optsWithGlobals();
7795
+ const raw = loadJsonFile(getGlobalConfigPath(opts.config)) ?? {};
7796
+ const segments = resolveConfigKey(raw, key);
7797
+ const current = getAtPath(raw, segments);
7798
+ if (!isKeychainRef(current)) {
7799
+ throw new KeychainError(`${key} is not a keychain reference (nothing to remove).`);
7800
+ }
7801
+ const account = accountFromRef(current);
7802
+ const backend = getKeychainBackend();
7803
+ let restored = false;
7804
+ if (cmdOpts.restore) {
7805
+ const secret = backend.getMany([account])[account];
7806
+ if (secret == null) throw new KeychainError(`No keychain entry for "${account}" \u2014 nothing to restore. Re-run without --restore to clear the reference.`);
7807
+ setAtPath(raw, segments, secret);
7808
+ restored = true;
7809
+ } else {
7810
+ deleteAtPath(raw, segments);
7811
+ }
7812
+ writeGlobalConfig(raw, opts.config);
7813
+ const deleted = purgeEntries(raw, backend, [account]).length > 0;
7814
+ resetKeychainCache();
7815
+ success({ key: segments.join("."), account, deleted, restoredToConfig: restored }, "config", "keychain-remove", start);
6492
7816
  } catch (err) {
6493
- results.figma = categorize(err);
7817
+ fail(err, "config", "keychain-remove", start);
6494
7818
  }
6495
- }
6496
- if (!cfg.alation.refreshToken) {
6497
- results.alation = { status: "blank", message: "not configured" };
6498
- } else if (!cfg.alation.baseUrl) {
6499
- results.alation = { status: "error", message: "baseUrl not configured" };
6500
- } else if (cfg.alation.userId === void 0 || cfg.alation.userId === "") {
6501
- results.alation = { status: "error", message: "userId not configured" };
6502
- } else {
7819
+ });
7820
+ keychain.command("migrate").description("Move secrets between plaintext config and the OS keychain (--to keychain, the default, or --to config)").argument("[keys...]", "Limit to these config keys (e.g. github.token marketplaces.internal-ai.token); default: all").option("--to <destination>", "keychain (move plaintext secrets in) or config (move keychain secrets back out)", "keychain").option("--purge", "With --to config: also delete the keychain entries once config holds the secrets").option("--dry-run", "List what would move without touching the keychain or config").action((keys, cmdOpts) => {
7821
+ const start = Date.now();
6503
7822
  try {
6504
- await validateAlationAccessToken(cfg);
6505
- results.alation = { status: "valid", message: "ok" };
7823
+ const opts = program2.optsWithGlobals();
7824
+ const dryRun = !!(cmdOpts.dryRun || opts.dryRun);
7825
+ const configPath = getGlobalConfigPath(opts.config);
7826
+ if (cmdOpts.to !== "keychain" && cmdOpts.to !== "config") {
7827
+ throw new KeychainError(`--to must be "keychain" or "config", got "${cmdOpts.to}".`);
7828
+ }
7829
+ if (cmdOpts.purge && cmdOpts.to !== "config") {
7830
+ throw new KeychainError("--purge only applies to --to config.");
7831
+ }
7832
+ if (!fs4.existsSync(configPath)) throw new KeychainError(`No global config at ${configPath} \u2014 nothing to migrate.`);
7833
+ const raw = loadJsonFile(configPath) ?? {};
7834
+ const backend = getKeychainBackend();
7835
+ if (!dryRun && !backend.available()) {
7836
+ throw new KeychainError(`The ${backend.store} backend is not available on this machine.`);
7837
+ }
7838
+ const only = new Set(keys);
7839
+ const result = cmdOpts.to === "keychain" ? migrateSecrets(raw, backend, dryRun, only) : unmigrateSecrets(raw, backend, dryRun, only);
7840
+ let purged = [];
7841
+ if (!dryRun && result.migrated.length > 0) {
7842
+ if (cmdOpts.to === "keychain") {
7843
+ const backup = `${configPath}.pre-keychain.bak`;
7844
+ fs4.copyFileSync(configPath, backup);
7845
+ try {
7846
+ fs4.chmodSync(backup, 384);
7847
+ } catch {
7848
+ }
7849
+ warn(`Plaintext backup written to ${backup} \u2014 delete it once \`pncli config check\` passes.`);
7850
+ }
7851
+ writeGlobalConfig(raw, opts.config);
7852
+ if (cmdOpts.purge) purged = purgeEntries(raw, backend, result.migrated.map((m) => m.account));
7853
+ }
7854
+ resetKeychainCache();
7855
+ success({
7856
+ to: cmdOpts.to,
7857
+ dryRun,
7858
+ store: backend.store,
7859
+ ...result,
7860
+ ...cmdOpts.to === "config" ? { purged } : {}
7861
+ }, "config", "keychain-migrate", start);
6506
7862
  } catch (err) {
6507
- results.alation = categorize(err);
7863
+ fail(err, "config", "keychain-migrate", start);
6508
7864
  }
6509
- }
6510
- return results;
7865
+ });
6511
7866
  }
6512
7867
 
6513
7868
  // src/services/skills/commands.ts
6514
- import fs3 from "fs";
6515
- import path8 from "path";
6516
- import os2 from "os";
7869
+ import fs6 from "fs";
7870
+ import path11 from "path";
7871
+ import os3 from "os";
6517
7872
  import { fileURLToPath } from "url";
6518
- import { execFileSync as execFileSync3 } from "child_process";
7873
+ import { execFileSync as execFileSync4 } from "child_process";
6519
7874
  import select from "@inquirer/select";
6520
7875
  import checkbox, { Separator } from "@inquirer/checkbox";
6521
7876
  import input from "@inquirer/input";
@@ -6541,22 +7896,22 @@ function getPncliVersion() {
6541
7896
  }
6542
7897
 
6543
7898
  // src/services/skills/instructions.ts
6544
- import fs2 from "fs";
6545
- import path7 from "path";
6546
- import os from "os";
7899
+ import fs5 from "fs";
7900
+ import path9 from "path";
7901
+ import os2 from "os";
6547
7902
  var INSTRUCTIONS_SUBDIR = "instructions";
6548
7903
  var INSTRUCTION_SOURCE_FILES = ["AGENTS.md", "CLAUDE.md"];
6549
7904
  var AGENT_INSTRUCTION_SPECS = {
6550
7905
  "codex": {
6551
- file: () => path7.join(process.env.CODEX_HOME || path7.join(os.homedir(), ".codex"), "AGENTS.md"),
7906
+ file: () => path9.join(process.env.CODEX_HOME || path9.join(os2.homedir(), ".codex"), "AGENTS.md"),
6552
7907
  sources: ["AGENTS.md", "CLAUDE.md"]
6553
7908
  },
6554
7909
  "github-copilot": {
6555
- file: () => path7.join(process.env.COPILOT_HOME || path7.join(os.homedir(), ".copilot"), "copilot-instructions.md"),
7910
+ file: () => path9.join(process.env.COPILOT_HOME || path9.join(os2.homedir(), ".copilot"), "copilot-instructions.md"),
6556
7911
  sources: ["AGENTS.md", "CLAUDE.md"]
6557
7912
  },
6558
7913
  "claude-code": {
6559
- file: () => path7.join(process.env.CLAUDE_CONFIG_DIR || path7.join(os.homedir(), ".claude"), "CLAUDE.md"),
7914
+ file: () => path9.join(process.env.CLAUDE_CONFIG_DIR || path9.join(os2.homedir(), ".claude"), "CLAUDE.md"),
6560
7915
  sources: ["CLAUDE.md", "AGENTS.md"]
6561
7916
  }
6562
7917
  };
@@ -6564,13 +7919,13 @@ var INSTRUCTION_AGENTS = Object.keys(AGENT_INSTRUCTION_SPECS);
6564
7919
  function agentInstructionsFile(agent) {
6565
7920
  const spec = AGENT_INSTRUCTION_SPECS[agent];
6566
7921
  if (!spec) throw new Error(`Unknown agent: "${agent}". Use: ${INSTRUCTION_AGENTS.join(" | ")}`);
6567
- return path7.resolve(spec.file());
7922
+ return path9.resolve(spec.file());
6568
7923
  }
6569
7924
  function findMarketplaceInstructions(marketplacePath) {
6570
- const dir = path7.join(marketplacePath, INSTRUCTIONS_SUBDIR);
6571
- return INSTRUCTION_SOURCE_FILES.map((file) => ({ file, path: path7.join(dir, file) })).filter((entry) => {
7925
+ const dir = path9.join(marketplacePath, INSTRUCTIONS_SUBDIR);
7926
+ return INSTRUCTION_SOURCE_FILES.map((file) => ({ file, path: path9.join(dir, file) })).filter((entry) => {
6572
7927
  try {
6573
- return fs2.statSync(entry.path).isFile();
7928
+ return fs5.statSync(entry.path).isFile();
6574
7929
  } catch {
6575
7930
  return false;
6576
7931
  }
@@ -6648,7 +8003,7 @@ function listManagedBlocks(content) {
6648
8003
  }
6649
8004
  function readFileOrEmpty(file) {
6650
8005
  try {
6651
- return fs2.readFileSync(file, "utf8");
8006
+ return fs5.readFileSync(file, "utf8");
6652
8007
  } catch {
6653
8008
  return "";
6654
8009
  }
@@ -6659,12 +8014,12 @@ function applyMarketplaceInstructions(marketplacePath, marketplaceName, agents)
6659
8014
  const file = agentInstructionsFile(agent);
6660
8015
  const source = pickInstructionSource(agent, shipped);
6661
8016
  if (!source) return { agent, file, source: null, action: "skipped" };
6662
- const body = fs2.readFileSync(source.path, "utf8");
8017
+ const body = fs5.readFileSync(source.path, "utf8");
6663
8018
  const block = renderManagedBlock(marketplaceName, body, `${INSTRUCTIONS_SUBDIR}/${source.file}`);
6664
8019
  const { content, action } = upsertManagedBlock(readFileOrEmpty(file), marketplaceName, block);
6665
8020
  if (action !== "unchanged") {
6666
- fs2.mkdirSync(path7.dirname(file), { recursive: true });
6667
- fs2.writeFileSync(file, content, "utf8");
8021
+ fs5.mkdirSync(path9.dirname(file), { recursive: true });
8022
+ fs5.writeFileSync(file, content, "utf8");
6668
8023
  }
6669
8024
  return { agent, file, source: `${INSTRUCTIONS_SUBDIR}/${source.file}`, action };
6670
8025
  });
@@ -6674,21 +8029,21 @@ function removeMarketplaceInstructions(marketplaceName, agents) {
6674
8029
  const file = agentInstructionsFile(agent);
6675
8030
  const existing = readFileOrEmpty(file);
6676
8031
  const { content, removed } = removeManagedBlock(existing, marketplaceName);
6677
- if (removed) fs2.writeFileSync(file, content, "utf8");
8032
+ if (removed) fs5.writeFileSync(file, content, "utf8");
6678
8033
  return { agent, file, removed };
6679
8034
  });
6680
8035
  }
6681
8036
  function marketplaceInstructionsStatus(marketplacePath, marketplaceName, agents) {
6682
- const shipped = marketplacePath && fs2.existsSync(marketplacePath) ? findMarketplaceInstructions(marketplacePath) : [];
8037
+ const shipped = marketplacePath && fs5.existsSync(marketplacePath) ? findMarketplaceInstructions(marketplacePath) : [];
6683
8038
  return agents.map((agent) => {
6684
8039
  const file = agentInstructionsFile(agent);
6685
- const fileExists = fs2.existsSync(file);
8040
+ const fileExists = fs5.existsSync(file);
6686
8041
  const existing = fileExists ? readFileOrEmpty(file) : "";
6687
8042
  const installed = listManagedBlocks(existing).includes(marketplaceName);
6688
8043
  const source = pickInstructionSource(agent, shipped);
6689
8044
  let upToDate = null;
6690
8045
  if (installed && source) {
6691
- const block = renderManagedBlock(marketplaceName, fs2.readFileSync(source.path, "utf8"), `${INSTRUCTIONS_SUBDIR}/${source.file}`);
8046
+ const block = renderManagedBlock(marketplaceName, fs5.readFileSync(source.path, "utf8"), `${INSTRUCTIONS_SUBDIR}/${source.file}`);
6692
8047
  upToDate = upsertManagedBlock(existing, marketplaceName, block).action === "unchanged";
6693
8048
  } else if (installed) {
6694
8049
  upToDate = false;
@@ -6704,20 +8059,270 @@ function marketplaceInstructionsStatus(marketplacePath, marketplaceName, agents)
6704
8059
  });
6705
8060
  }
6706
8061
 
6707
- // src/services/skills/commands.ts
6708
- var BACK = "__back__";
6709
- var ALL_MARKETPLACES = "__all_marketplaces__";
6710
- function injectTokenIntoUrl(url, token) {
6711
- let parsed;
8062
+ // src/services/skills/git-auth-commands.ts
8063
+ import path10 from "path";
8064
+ function readRawConfig(configPath) {
8065
+ return loadJsonFile(getGlobalConfigPath(configPath)) ?? {};
8066
+ }
8067
+ function saveScopeRecords(updates, configPath) {
8068
+ const raw = readRawConfig(configPath);
8069
+ const scopes = { ...raw.gitAuth?.scopes ?? {} };
8070
+ for (const [scope, record] of Object.entries(updates)) {
8071
+ if (record) scopes[scope] = record;
8072
+ else delete scopes[scope];
8073
+ }
8074
+ writeGlobalConfig({ ...raw, gitAuth: { ...raw.gitAuth, scopes } }, configPath);
8075
+ }
8076
+ function normalizeHostArg(host) {
8077
+ return host.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:443$/, "");
8078
+ }
8079
+ function findMarketplace(globalConfig, name) {
8080
+ const m = allMarketplaces(globalConfig).find((x) => x.name === name || x.repoUrl === name);
8081
+ if (!m) throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
8082
+ return m;
8083
+ }
8084
+ function backupOf(record) {
8085
+ return record ? { previousHelpers: record.previousHelpers ?? [], previousUseHttpPath: record.previousUseHttpPath ?? null } : void 0;
8086
+ }
8087
+ function scrubClones(marketplaces) {
8088
+ const fixed = [];
8089
+ for (const m of marketplaces) {
8090
+ if (!m.localPath || !m.repoUrl) continue;
8091
+ if (originHasCredentials(m.localPath) && stripOriginCredentials(m.localPath, m.repoUrl)) fixed.push(m.localPath);
8092
+ }
8093
+ return fixed;
8094
+ }
8095
+ function registerGitAuthCommands(skills) {
8096
+ const gitAuth = skills.command("git-auth").description("Give git itself each marketplace's credential, so agent hosts and plain git clones of private marketplaces authenticate (GitHub, Bitbucket, Azure DevOps)");
8097
+ const configPathOf = () => gitAuth.optsWithGlobals().config;
8098
+ gitAuth.addHelpText("after", `
8099
+ Why:
8100
+ pncli's own clone and pull are authenticated, but an agent host that clones a marketplace itself
8101
+ (e.g. Claude Code's /plugin marketplace add) runs plain git, which has no credential for it.
8102
+
8103
+ Scope:
8104
+ Each marketplace gets an entry scoped to its own repository URL, answered with that marketplace's
8105
+ own token (or the provider token pncli already has: github.token, bitbucket.pat, ado.pat).
8106
+ Every other repo on the same host is untouched. --host opts into a whole-host entry instead.
8107
+
8108
+ Modes:
8109
+ --mode helper (default) git asks pncli (${HELPER_COMMAND} --marketplace <name>). Nothing is copied
8110
+ into gitconfig; rotating the token in pncli is enough. If pncli cannot
8111
+ answer, git falls through to the helpers you already had.
8112
+ --mode keychain hands the token to git's own credential store (Git Credential Manager,
8113
+ macOS Keychain, libsecret), keyed to the repository. Works without pncli
8114
+ on PATH; re-run after rotating the token \u2014 pncli doctor flags a stale copy.
8115
+
8116
+ disable restores exactly what enable replaced (e.g. an entry \`gh auth setup-git\` wrote).
8117
+
8118
+ Examples:
8119
+ pncli skills git-auth enable # every marketplace with a credential
8120
+ pncli skills git-auth enable --marketplace internal-ai --mode keychain
8121
+ pncli skills git-auth enable --host ghe.imagile.dev # whole host, with github.token
8122
+ pncli skills git-auth status
8123
+ pncli skills git-auth disable --marketplace internal-ai
8124
+ `);
8125
+ gitAuth.command("enable").description("Configure git to authenticate to marketplace repositories with pncli's credential (helper or OS keychain)").option("--marketplace <name>", "Only this marketplace (default: every marketplace that has a credential)").option("--host <host>", "A whole-host entry instead, answered with the provider token pncli has for that host").option("--mode <mode>", "helper (git asks pncli) or keychain (store in the OS credential store)", "helper").action((opts) => {
8126
+ const start = Date.now();
8127
+ try {
8128
+ if (opts.mode !== "helper" && opts.mode !== "keychain") throw new Error(`--mode must be "helper" or "keychain", got "${opts.mode}".`);
8129
+ if (opts.marketplace && opts.host) throw new Error("Pass --marketplace or --host, not both.");
8130
+ const mode = opts.mode;
8131
+ const configPath = configPathOf();
8132
+ const cfg = loadConfig({ configPath });
8133
+ const globalConfig = readRawConfig(configPath);
8134
+ const overridePath = configPath ?? process.env.PNCLI_CONFIG_PATH;
8135
+ const helperConfigPath = overridePath ? path10.resolve(overridePath) : void 0;
8136
+ const targets = [];
8137
+ const skipped = [];
8138
+ if (opts.host) {
8139
+ const host = normalizeHostArg(opts.host);
8140
+ targets.push({
8141
+ scope: hostScope(host),
8142
+ marketplace: null,
8143
+ provider: null,
8144
+ helperValue: helperCommandFor(void 0, helperConfigPath),
8145
+ auth: resolveCredential({ protocol: "https", host }, globalConfig, cfg)
8146
+ });
8147
+ } else {
8148
+ const chosen = opts.marketplace ? [findMarketplace(globalConfig, opts.marketplace)] : allMarketplaces(globalConfig);
8149
+ if (chosen.length === 0) throw new Error("No marketplaces are registered. Add one first: pncli skills marketplace add <git-url>");
8150
+ for (const m of chosen) {
8151
+ if (!hostOf(m.repoUrl)) {
8152
+ skipped.push({ marketplace: marketplaceLabelOf(m), reason: "not an HTTPS URL (SSH marketplaces use your SSH keys)" });
8153
+ continue;
8154
+ }
8155
+ const auth = resolveMarketplaceAuth(m, cfg);
8156
+ if (!auth) {
8157
+ skipped.push({ marketplace: marketplaceLabelOf(m), reason: "no credential \u2014 a public repo needs none; for a private one: pncli skills marketplace update <name> --token <token>" });
8158
+ continue;
8159
+ }
8160
+ const helperValue = helperCommandFor(m.name ?? m.repoUrl, helperConfigPath);
8161
+ for (const scope of marketplaceScopes(m.repoUrl, auth.provider)) {
8162
+ targets.push({ scope, marketplace: m, provider: auth.provider, helperValue, auth });
8163
+ }
8164
+ }
8165
+ if (opts.marketplace && targets.length === 0) throw new Error(`Nothing to enable for "${opts.marketplace}": ${skipped[0]?.reason}`);
8166
+ }
8167
+ if (mode === "keychain") {
8168
+ const missing = targets.filter((t) => !t.auth).map((t) => t.scope);
8169
+ if (missing.length > 0) throw new Error(`No token is configured for ${missing.join(", ")}, so there is nothing to store. Nothing was changed.`);
8170
+ }
8171
+ const records = {};
8172
+ const results = targets.map((t) => {
8173
+ const existing = globalConfig.gitAuth?.scopes?.[t.scope];
8174
+ const marketplace = t.marketplace ? t.marketplace.name ?? t.marketplace.repoUrl : void 0;
8175
+ if (mode === "helper") {
8176
+ const backup = enableHelper(t.scope, t.helperValue);
8177
+ records[t.scope] = {
8178
+ mode: "helper",
8179
+ ...marketplace ? { marketplace } : {},
8180
+ previousHelpers: existing?.previousHelpers ?? backup.previousHelpers,
8181
+ previousUseHttpPath: existing && "previousUseHttpPath" in existing ? existing.previousUseHttpPath ?? null : backup.previousUseHttpPath,
8182
+ enabledAt: (/* @__PURE__ */ new Date()).toISOString()
8183
+ };
8184
+ if (!t.auth) warn(`No token is configured for ${t.scope} yet \u2014 git keeps using your existing credentials there until you add one.`);
8185
+ } else {
8186
+ if (existing?.mode === "helper") disableHelper(t.scope, backupOf(existing));
8187
+ const { plaintextStore, backup } = keychainStore(t.scope, t.auth.username, t.auth.password);
8188
+ records[t.scope] = {
8189
+ mode: "keychain",
8190
+ ...marketplace ? { marketplace } : {},
8191
+ username: t.auth.username,
8192
+ previousHelpers: existing?.mode === "helper" ? existing.previousHelpers ?? [] : [],
8193
+ previousUseHttpPath: existing && "previousUseHttpPath" in existing ? existing.previousUseHttpPath ?? null : backup.previousUseHttpPath,
8194
+ enabledAt: (/* @__PURE__ */ new Date()).toISOString()
8195
+ };
8196
+ if (plaintextStore) warn(`git's credential.helper includes "store", which writes ~/.git-credentials in plaintext. Consider "manager" (Windows) or "osxkeychain" (macOS).`);
8197
+ }
8198
+ return {
8199
+ scope: t.scope,
8200
+ marketplace: marketplace ?? null,
8201
+ provider: t.provider,
8202
+ mode,
8203
+ tokenSource: t.auth?.source ?? null,
8204
+ tokenKind: t.auth && t.provider === "github" ? classifyGitHubToken(t.auth.password) : null
8205
+ };
8206
+ });
8207
+ saveScopeRecords(records, configPath);
8208
+ const touched = opts.host ? allMarketplaces(globalConfig).filter((m) => hostOf(m.repoUrl) === normalizeHostArg(opts.host)) : [...new Set(targets.map((t) => t.marketplace).filter((m) => !!m))];
8209
+ success({ scopes: results, skipped, scrubbedClones: scrubClones(touched), next: "Verify with: pncli doctor" }, "skills", "git-auth-enable", start);
8210
+ } catch (err) {
8211
+ fail(err, "skills", "git-auth-enable", start);
8212
+ }
8213
+ });
8214
+ gitAuth.command("disable").description("Remove pncli's git credential entries and restore what they replaced (optionally also the OS keychain copy)").option("--marketplace <name>", "Only this marketplace's entries").option("--host <host>", "Only the whole-host entry for this host").option("--forget-keychain", "Also erase the credential git stored in the OS keychain").action((opts) => {
8215
+ const start = Date.now();
8216
+ try {
8217
+ const configPath = configPathOf();
8218
+ const globalConfig = readRawConfig(configPath);
8219
+ const recorded = globalConfig.gitAuth?.scopes ?? {};
8220
+ let scopes;
8221
+ if (opts.host) scopes = [hostScope(normalizeHostArg(opts.host))];
8222
+ else if (opts.marketplace) {
8223
+ const m = findMarketplace(globalConfig, opts.marketplace);
8224
+ const key = m.name ?? m.repoUrl;
8225
+ scopes = Object.keys(recorded).filter((s) => recorded[s].marketplace === key);
8226
+ } else scopes = Object.keys(recorded);
8227
+ if (scopes.length === 0) throw new Error("pncli has not enabled git-auth for anything matching that. See: pncli skills git-auth status");
8228
+ const updates = {};
8229
+ const results = scopes.map((scope) => {
8230
+ const record = recorded[scope];
8231
+ const { helperRemoved, keychainErased } = disableScope(scope, record, !!opts.forgetKeychain);
8232
+ updates[scope] = null;
8233
+ return { scope, marketplace: record?.marketplace ?? null, mode: record?.mode ?? null, helperRemoved, keychainErased, restoredHelpers: record?.previousHelpers ?? [] };
8234
+ });
8235
+ if (opts.forgetKeychain && !results.some((r) => r.keychainErased)) {
8236
+ warn("--forget-keychain had nothing to erase: helper mode never stores a credential in the keychain.");
8237
+ }
8238
+ saveScopeRecords(updates, configPath);
8239
+ success({ scopes: results }, "skills", "git-auth-disable", start);
8240
+ } catch (err) {
8241
+ fail(err, "skills", "git-auth-disable", start);
8242
+ }
8243
+ });
8244
+ gitAuth.command("status").description("Show, per marketplace (and whole-host entry), how git authenticates and whether it sends the token pncli uses").option("--marketplace <name>", "Only this marketplace").action((opts) => {
8245
+ const start = Date.now();
8246
+ try {
8247
+ const configPath = configPathOf();
8248
+ const cfg = loadConfig({ configPath });
8249
+ const globalConfig = readRawConfig(configPath);
8250
+ const reports = opts.marketplace ? [describeMarketplaceAuth(findMarketplace(globalConfig, opts.marketplace), globalConfig, cfg)] : describeAllGitAuth(globalConfig, cfg);
8251
+ success({ entries: reports }, "skills", "git-auth-status", start);
8252
+ } catch (err) {
8253
+ fail(err, "skills", "git-auth-status", start);
8254
+ }
8255
+ });
8256
+ }
8257
+ var summarize = (e) => e === "" || isPncliHelperValue(e) ? e : "(other helper)";
8258
+ function describeMarketplaceAuth(m, globalConfig, cfg, git = defaultGitRunner, hasEmbeddedToken = originHasCredentials) {
8259
+ const host = hostOf(m.repoUrl) ?? "";
8260
+ let auth = null;
8261
+ let credentialError = null;
6712
8262
  try {
6713
- parsed = new URL(url);
6714
- } catch {
6715
- throw new Error(`--token requires an HTTPS clone URL; got: ${url}`);
8263
+ auth = resolveMarketplaceAuth(m, cfg);
8264
+ } catch (err) {
8265
+ credentialError = err instanceof Error ? err.message : String(err);
8266
+ }
8267
+ const provider = auth?.provider ?? detectProvider(m.repoUrl, cfg, m.provider);
8268
+ const scope = host ? marketplaceScopes(m.repoUrl, provider)[0] : m.repoUrl ?? "";
8269
+ const scopeEntries = host ? readHelperEntries(scope, git) : [];
8270
+ const hostEntries = host ? readHelperEntries(hostScope(host), git) : [];
8271
+ const record = globalConfig.gitAuth?.scopes?.[scope];
8272
+ const filled = host ? credentialFill(host, parseScope(scope).path ?? void 0, git) : null;
8273
+ const pncliHelper = scopeEntries.some(isPncliHelperValue) || !scopeEntries.some((e) => e.trim() !== "") && hostEntries.some(isPncliHelperValue);
8274
+ const mode = pncliHelper ? "helper" : record?.mode === "keychain" ? "keychain" : filled || [...scopeEntries, ...hostEntries].some((e) => e.trim() !== "") ? "other" : "none";
8275
+ return {
8276
+ scope,
8277
+ host,
8278
+ marketplace: m.name ?? m.repoUrl ?? null,
8279
+ repoUrl: m.repoUrl ?? null,
8280
+ provider,
8281
+ mode,
8282
+ helperEntries: scopeEntries.map(summarize),
8283
+ pncliTokenSource: auth?.source ?? null,
8284
+ username: auth?.username ?? null,
8285
+ credentialError,
8286
+ gitHasCredential: !!filled,
8287
+ matchesPncliToken: (mode === "helper" || mode === "keychain") && filled && auth ? filled.password === auth.password : null,
8288
+ clonesWithEmbeddedToken: m.localPath && hasEmbeddedToken(m.localPath) ? [m.localPath] : []
8289
+ };
8290
+ }
8291
+ function describeHostScopeAuth(host, globalConfig, cfg, git = defaultGitRunner) {
8292
+ const scope = hostScope(host);
8293
+ const entries = readHelperEntries(scope, git);
8294
+ const record = globalConfig.gitAuth?.scopes?.[scope];
8295
+ const answer = resolveCredential({ protocol: "https", host }, globalConfig, cfg);
8296
+ const filled = credentialFill(host, void 0, git);
8297
+ const mode = entries.some(isPncliHelperValue) ? "helper" : record?.mode === "keychain" ? "keychain" : filled || entries.some((e) => e.trim() !== "") ? "other" : "none";
8298
+ return {
8299
+ scope,
8300
+ host,
8301
+ marketplace: null,
8302
+ repoUrl: null,
8303
+ provider: null,
8304
+ mode,
8305
+ helperEntries: entries.map(summarize),
8306
+ pncliTokenSource: answer?.source ?? null,
8307
+ username: answer?.username ?? null,
8308
+ credentialError: null,
8309
+ gitHasCredential: !!filled,
8310
+ matchesPncliToken: (mode === "helper" || mode === "keychain") && filled && answer ? filled.password === answer.password : null,
8311
+ clonesWithEmbeddedToken: []
8312
+ };
8313
+ }
8314
+ function describeAllGitAuth(globalConfig, cfg, git = defaultGitRunner) {
8315
+ const reports = allMarketplaces(globalConfig).filter((m) => hostOf(m.repoUrl)).map((m) => describeMarketplaceAuth(m, globalConfig, cfg, git));
8316
+ for (const scope of Object.keys(globalConfig.gitAuth?.scopes ?? {})) {
8317
+ const parsed = parseScope(scope);
8318
+ if (!parsed.path) reports.push(describeHostScopeAuth(parsed.host, globalConfig, cfg, git));
6716
8319
  }
6717
- parsed.username = parsed.hostname === "github.com" ? "x-access-token" : "x-token-auth";
6718
- parsed.password = token;
6719
- return parsed.toString();
8320
+ return reports;
6720
8321
  }
8322
+
8323
+ // src/services/skills/commands.ts
8324
+ var BACK = "__back__";
8325
+ var ALL_MARKETPLACES = "__all_marketplaces__";
6721
8326
  function repoNameFromUrl(url) {
6722
8327
  try {
6723
8328
  const parsed = new URL(url);
@@ -6728,7 +8333,7 @@ function repoNameFromUrl(url) {
6728
8333
  }
6729
8334
  }
6730
8335
  function defaultMarketplacePath(url) {
6731
- return path8.join(os2.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
8336
+ return path11.join(os3.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
6732
8337
  }
6733
8338
  function marketplaceLabel(m) {
6734
8339
  return m.name ?? repoNameFromUrl(m.repoUrl ?? "");
@@ -6736,22 +8341,19 @@ function marketplaceLabel(m) {
6736
8341
  function scrubToken(msg) {
6737
8342
  return msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@");
6738
8343
  }
6739
- function isConfiguredGitHubHost(repoUrl, githubBaseUrl) {
8344
+ function marketplaceGitAuth(m) {
8345
+ if (m.token && m.repoUrl && !httpHostOf(m.repoUrl)) {
8346
+ throw new Error(`--token requires an HTTPS clone URL; got: ${m.repoUrl}`);
8347
+ }
8348
+ let auth;
6740
8349
  try {
6741
- const host = new URL(repoUrl).hostname;
6742
- if (host === "github.com") return true;
6743
- if (!githubBaseUrl) return false;
6744
- return new URL(githubBaseUrl).hostname === host;
6745
- } catch {
6746
- return false;
8350
+ auth = resolveMarketplaceAuth(m, loadConfig());
8351
+ } catch (err) {
8352
+ if (err instanceof UnresolvedMarketplaceTokenError) throw new Error(err.message);
8353
+ throw err;
6747
8354
  }
6748
- }
6749
- function resolveMarketplaceToken(explicitToken, repoUrl) {
6750
- if (explicitToken) return explicitToken;
6751
- if (!repoUrl) return void 0;
6752
- const resolved = loadConfig();
6753
- if (!isConfiguredGitHubHost(repoUrl, resolved.github.baseUrl)) return void 0;
6754
- return resolved.github.token;
8355
+ const tokenSource = !auth ? "none" : auth.source.startsWith("marketplace:") ? "explicit" : "fallback";
8356
+ return { auth, tokenSource };
6755
8357
  }
6756
8358
  var GIT_AUTH_FAILURE_PATTERNS = [
6757
8359
  /invalid username or (password|token)/i,
@@ -6761,16 +8363,40 @@ var GIT_AUTH_FAILURE_PATTERNS = [
6761
8363
  /support for password authentication was removed/i
6762
8364
  ];
6763
8365
  var GIT_NOT_FOUND_PATTERNS = [/repository not found/i];
6764
- function describeGitFailure(rawMessage, marketplaceName, tokenSource) {
8366
+ var FALLBACK_LABELS = {
8367
+ "bitbucket.pat": "The Bitbucket token pncli is using (PNCLI_BITBUCKET_PAT / bitbucket.pat)",
8368
+ "ado.pat": "The Azure DevOps token pncli is using (PNCLI_ADO_PAT / SYSTEM_ACCESSTOKEN / ado.pat)"
8369
+ };
8370
+ function providerAuthAdvice(marketplaceName, ctx) {
8371
+ if (ctx.provider === "bitbucket" && !ctx.customUsername) {
8372
+ return `
8373
+
8374
+ Bitbucket Data Center personal access tokens are usually sent with your Bitbucket username. Set it with: pncli skills marketplace update ${marketplaceName} --username <your-bitbucket-username>`;
8375
+ }
8376
+ if (ctx.provider === "ado") {
8377
+ return "\n\nAzure DevOps personal access tokens need the Code (Read) scope for the collection that hosts this repo.";
8378
+ }
8379
+ return "";
8380
+ }
8381
+ function describeGitFailure(rawMessage, marketplaceName, tokenSource, ctx = {}) {
6765
8382
  const msg = scrubToken(rawMessage);
6766
- if (GIT_AUTH_FAILURE_PATTERNS.some((p) => p.test(msg))) {
6767
- const hint = tokenSource === "explicit" ? `The token configured for marketplace "${marketplaceName}" was rejected \u2014 it may be expired, revoked, or missing required scopes. Update it with: pncli skills marketplace add <url> --token <new-token>` : tokenSource === "fallback" ? `The GitHub token pncli is using (PNCLI_GITHUB_TOKEN / GITHUB_TOKEN / github.token) was rejected for marketplace "${marketplaceName}" \u2014 it may be expired, revoked, or missing required scopes for this repo. Rotate that token, or set one specifically for this marketplace: pncli skills marketplace add <url> --token <new-token>` : `Marketplace "${marketplaceName}" requires authentication but no token is configured. Add one with: pncli skills marketplace add <url> --token <token>`;
8383
+ if (ctx.userCredentialHost && (GIT_AUTH_FAILURE_PATTERNS.some((p) => p.test(msg)) || GIT_NOT_FOUND_PATTERNS.some((p) => p.test(msg)))) {
8384
+ const host = ctx.userCredentialHost;
8385
+ const hint = `git used your own stored credential for ${host} for marketplace "${marketplaceName}" (pncli's ${ctx.fallbackSource ?? "token"} was not sent), and it was refused. If that stored credential is stale, see which one git sends with: pncli git credentials inspect \u2014 and remove it with: pncli git credentials forget --host ${host}. The next sync then uses ${ctx.fallbackSource ?? "pncli's token"}.`;
6768
8386
  return new Error(`${hint}
6769
8387
 
8388
+ Git reported: ${msg}`);
8389
+ }
8390
+ if (GIT_AUTH_FAILURE_PATTERNS.some((p) => p.test(msg))) {
8391
+ const fallbackLabel = ctx.fallbackSource ? FALLBACK_LABELS[ctx.fallbackSource] : void 0;
8392
+ const hint = tokenSource === "fallback" && fallbackLabel ? `${fallbackLabel} was rejected for marketplace "${marketplaceName}" \u2014 it may be expired, revoked, or missing required scopes for this repo. Rotate that token, or set one specifically for this marketplace: pncli skills marketplace update ${marketplaceName} --token <new-token>` : tokenSource === "explicit" ? `The token configured for marketplace "${marketplaceName}" was rejected \u2014 it may be expired, revoked, or missing required scopes. Update it with: pncli skills marketplace add <url> --token <new-token>` : tokenSource === "fallback" ? `The GitHub token pncli is using (PNCLI_GITHUB_TOKEN / GITHUB_TOKEN / github.token) was rejected for marketplace "${marketplaceName}" \u2014 it may be expired, revoked, or missing required scopes for this repo. Rotate that token, or set one specifically for this marketplace: pncli skills marketplace add <url> --token <new-token>` : `Marketplace "${marketplaceName}" requires authentication but no token is configured. Add one with: pncli skills marketplace add <url> --token <token>`;
8393
+ return new Error(`${hint}${tokenSource !== "none" ? providerAuthAdvice(marketplaceName, ctx) : ""}
8394
+
6770
8395
  Git reported: ${msg}`);
6771
8396
  }
6772
8397
  if (GIT_NOT_FOUND_PATTERNS.some((p) => p.test(msg))) {
6773
- const hint = tokenSource === "explicit" ? `Repository for marketplace "${marketplaceName}" was not found \u2014 check the URL and that the configured token has access to it.` : tokenSource === "fallback" ? `Repository for marketplace "${marketplaceName}" was not found \u2014 check the URL and that pncli's configured GitHub token has access to it.` : `Repository for marketplace "${marketplaceName}" was not found \u2014 if it's private, add a token: pncli skills marketplace add <url> --token <token>`;
8398
+ const fallbackLabel = ctx.fallbackSource ? FALLBACK_LABELS[ctx.fallbackSource] : void 0;
8399
+ const hint = tokenSource === "fallback" && fallbackLabel ? `Repository for marketplace "${marketplaceName}" was not found \u2014 check the URL and that ${fallbackLabel.charAt(0).toLowerCase()}${fallbackLabel.slice(1)} has access to it.` : tokenSource === "explicit" ? `Repository for marketplace "${marketplaceName}" was not found \u2014 check the URL and that the configured token has access to it.` : tokenSource === "fallback" ? `Repository for marketplace "${marketplaceName}" was not found \u2014 check the URL and that pncli's configured GitHub token has access to it.` : `Repository for marketplace "${marketplaceName}" was not found \u2014 if it's private, add a token: pncli skills marketplace add <url> --token <token>`;
6774
8400
  return new Error(`${hint}
6775
8401
 
6776
8402
  Git reported: ${msg}`);
@@ -6783,9 +8409,9 @@ function assertInteractive(hint) {
6783
8409
  }
6784
8410
  }
6785
8411
  var AGENT_PATHS = {
6786
- "codex": { project: ".agents/skills", user: path8.join(os2.homedir(), ".agents/skills") },
6787
- "github-copilot": { project: ".github/skills", user: path8.join(os2.homedir(), ".copilot/skills") },
6788
- "claude-code": { project: ".claude/skills", user: path8.join(os2.homedir(), ".claude/skills") }
8412
+ "codex": { project: ".agents/skills", user: path11.join(os3.homedir(), ".agents/skills") },
8413
+ "github-copilot": { project: ".github/skills", user: path11.join(os3.homedir(), ".copilot/skills") },
8414
+ "claude-code": { project: ".claude/skills", user: path11.join(os3.homedir(), ".claude/skills") }
6789
8415
  };
6790
8416
  var DEFAULT_AGENT = "codex";
6791
8417
  var AGENT_CHOICES = Object.keys(AGENT_PATHS).join(" | ");
@@ -6808,27 +8434,27 @@ function resolveAgentPaths(agentName) {
6808
8434
  }
6809
8435
  function findGitRoot(from = process.cwd()) {
6810
8436
  try {
6811
- const root = execFileSync3("git", ["-C", from, "rev-parse", "--show-toplevel"], {
8437
+ const root = execFileSync4("git", ["-C", from, "rev-parse", "--show-toplevel"], {
6812
8438
  encoding: "utf8",
6813
8439
  stdio: ["pipe", "pipe", "pipe"]
6814
8440
  }).trim();
6815
- return root ? path8.resolve(root) : null;
8441
+ return root ? path11.resolve(root) : null;
6816
8442
  } catch {
6817
8443
  return null;
6818
8444
  }
6819
8445
  }
6820
8446
  function resolveProjectPath(relativePath) {
6821
8447
  const root = findGitRoot();
6822
- return root ? path8.resolve(root, relativePath) : path8.resolve(relativePath);
8448
+ return root ? path11.resolve(root, relativePath) : path11.resolve(relativePath);
6823
8449
  }
6824
8450
  function resolveScopedPath(agentPaths, scope) {
6825
- return scope === "project" ? resolveProjectPath(agentPaths.project) : path8.resolve(agentPaths.user);
8451
+ return scope === "project" ? resolveProjectPath(agentPaths.project) : path11.resolve(agentPaths.user);
6826
8452
  }
6827
8453
  function hasInstalledPncliSkill() {
6828
8454
  try {
6829
8455
  for (const agentPaths of Object.values(AGENT_PATHS)) {
6830
8456
  for (const scope of ["project", "user"]) {
6831
- if (fs3.existsSync(path8.join(resolveScopedPath(agentPaths, scope), "pncli", "SKILL.md"))) {
8457
+ if (fs6.existsSync(path11.join(resolveScopedPath(agentPaths, scope), "pncli", "SKILL.md"))) {
6832
8458
  return true;
6833
8459
  }
6834
8460
  }
@@ -6838,7 +8464,7 @@ function hasInstalledPncliSkill() {
6838
8464
  return false;
6839
8465
  }
6840
8466
  function resolveTargetDir(opts) {
6841
- if (opts.target) return path8.resolve(opts.target);
8467
+ if (opts.target) return path11.resolve(opts.target);
6842
8468
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
6843
8469
  return resolveScopedPath(agentConfig, opts.scope ?? "user");
6844
8470
  }
@@ -6855,13 +8481,46 @@ function resolveInstallTargets(opts, scope = "user") {
6855
8481
  function getBundledSkillsDir() {
6856
8482
  try {
6857
8483
  const thisFile = fileURLToPath(import.meta.url);
6858
- return path8.resolve(path8.dirname(thisFile), "..", "skills");
8484
+ return path11.resolve(path11.dirname(thisFile), "..", "skills");
6859
8485
  } catch {
6860
8486
  return "";
6861
8487
  }
6862
8488
  }
8489
+ var GUIDE_FILE = "skills-guide.md";
8490
+ function findGuidePath() {
8491
+ const bundled = path11.join(getBundledSkillsDir(), "pncli", GUIDE_FILE);
8492
+ if (fs6.existsSync(bundled)) return bundled;
8493
+ try {
8494
+ let dir = path11.dirname(fileURLToPath(import.meta.url));
8495
+ for (let i = 0; i < 6; i++) {
8496
+ const candidate = path11.join(dir, "skills", "pncli", GUIDE_FILE);
8497
+ if (fs6.existsSync(candidate)) return candidate;
8498
+ dir = path11.dirname(dir);
8499
+ }
8500
+ } catch {
8501
+ }
8502
+ return null;
8503
+ }
8504
+ function slugify(title) {
8505
+ return title.toLowerCase().replace(/[`*_]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8506
+ }
8507
+ function listGuideSections(markdown) {
8508
+ return markdown.split(/\r?\n/).filter((l) => /^## /.test(l)).map((l) => {
8509
+ const title = l.slice(3).trim();
8510
+ return { title, slug: slugify(title) };
8511
+ });
8512
+ }
8513
+ function extractGuideSection(markdown, query) {
8514
+ const lines = markdown.split(/\r?\n/);
8515
+ const heads = lines.map((l, i) => ({ l, i })).filter((x) => /^## /.test(x.l));
8516
+ const q = slugify(query);
8517
+ const pick = heads.find((h) => slugify(h.l.slice(3)) === q) ?? heads.find((h) => slugify(h.l.slice(3)).includes(q));
8518
+ if (!pick) return null;
8519
+ const next = heads.find((h) => h.i > pick.i);
8520
+ return lines.slice(pick.i, next ? next.i : lines.length).join("\n").trimEnd() + "\n";
8521
+ }
6863
8522
  function getInstalledMetaPath(targetDir) {
6864
- return path8.join(targetDir, ".pncli-installed.json");
8523
+ return path11.join(targetDir, ".pncli-installed.json");
6865
8524
  }
6866
8525
  var SKILL_ORIGIN_FILENAME = "pncli-origin.json";
6867
8526
  var DISABLED_SUBDIR = ".pncli-disabled";
@@ -6872,13 +8531,13 @@ function readInstalledMeta(targetDir) {
6872
8531
  return { version: 1, skills: {} };
6873
8532
  }
6874
8533
  function getSkillOriginPath(skillDir) {
6875
- return path8.join(skillDir, SKILL_ORIGIN_FILENAME);
8534
+ return path11.join(skillDir, SKILL_ORIGIN_FILENAME);
6876
8535
  }
6877
8536
  function readSkillOrigin(skillDir) {
6878
8537
  const originPath = getSkillOriginPath(skillDir);
6879
- if (!fs3.existsSync(originPath)) return null;
8538
+ if (!fs6.existsSync(originPath)) return null;
6880
8539
  try {
6881
- const raw = JSON.parse(fs3.readFileSync(originPath, "utf8"));
8540
+ const raw = JSON.parse(fs6.readFileSync(originPath, "utf8"));
6882
8541
  if (raw && raw.version === 1 && raw.source) return raw;
6883
8542
  } catch {
6884
8543
  }
@@ -6890,7 +8549,7 @@ function matchesMarketplaceFilter(record, filter) {
6890
8549
  function resolveSkillRecord(meta, targetDir, skillName) {
6891
8550
  const fromMeta = meta.skills[skillName];
6892
8551
  if (fromMeta) return fromMeta;
6893
- const perSkill = readSkillOrigin(path8.join(targetDir, skillName));
8552
+ const perSkill = readSkillOrigin(path11.join(targetDir, skillName));
6894
8553
  if (!perSkill) return null;
6895
8554
  return {
6896
8555
  source: perSkill.source,
@@ -6903,28 +8562,28 @@ function resolveSkillRecord(meta, targetDir, skillName) {
6903
8562
  };
6904
8563
  }
6905
8564
  function listActiveSkillDirs(targetDir) {
6906
- if (!fs3.existsSync(targetDir)) return [];
6907
- return fs3.readdirSync(targetDir).filter((name) => {
8565
+ if (!fs6.existsSync(targetDir)) return [];
8566
+ return fs6.readdirSync(targetDir).filter((name) => {
6908
8567
  if (name.startsWith(".")) return false;
6909
- const dir = path8.join(targetDir, name);
8568
+ const dir = path11.join(targetDir, name);
6910
8569
  try {
6911
- if (!fs3.statSync(dir).isDirectory()) return false;
8570
+ if (!fs6.statSync(dir).isDirectory()) return false;
6912
8571
  } catch {
6913
8572
  return false;
6914
8573
  }
6915
- return fs3.existsSync(path8.join(dir, "SKILL.md"));
8574
+ return fs6.existsSync(path11.join(dir, "SKILL.md"));
6916
8575
  });
6917
8576
  }
6918
8577
  function disablePluginSkills(targetDir, plugin, marketplaceFilter) {
6919
- const resolvedTarget = path8.resolve(targetDir);
6920
- const stashDir = path8.join(targetDir, DISABLED_SUBDIR);
8578
+ const resolvedTarget = path11.resolve(targetDir);
8579
+ const stashDir = path11.join(targetDir, DISABLED_SUBDIR);
6921
8580
  const meta = readInstalledMeta(targetDir);
6922
8581
  const disabled = [];
6923
8582
  const alreadyDisabled = [];
6924
8583
  const skipped = [];
6925
8584
  for (const skillName of listActiveSkillDirs(targetDir)) {
6926
- const skillDir = path8.resolve(targetDir, skillName);
6927
- if (!skillDir.startsWith(resolvedTarget + path8.sep)) continue;
8585
+ const skillDir = path11.resolve(targetDir, skillName);
8586
+ if (!skillDir.startsWith(resolvedTarget + path11.sep)) continue;
6928
8587
  const record = resolveSkillRecord(meta, targetDir, skillName);
6929
8588
  if (!record || record.source !== "marketplace") {
6930
8589
  skipped.push(skillName);
@@ -6934,10 +8593,10 @@ function disablePluginSkills(targetDir, plugin, marketplaceFilter) {
6934
8593
  skipped.push(skillName);
6935
8594
  continue;
6936
8595
  }
6937
- const stashDest = path8.join(stashDir, skillName);
6938
- fs3.mkdirSync(stashDir, { recursive: true });
6939
- if (fs3.existsSync(stashDest)) fs3.rmSync(stashDest, { recursive: true, force: true });
6940
- fs3.renameSync(skillDir, stashDest);
8596
+ const stashDest = path11.join(stashDir, skillName);
8597
+ fs6.mkdirSync(stashDir, { recursive: true });
8598
+ if (fs6.existsSync(stashDest)) fs6.rmSync(stashDest, { recursive: true, force: true });
8599
+ fs6.renameSync(skillDir, stashDest);
6941
8600
  delete meta.skills[skillName];
6942
8601
  meta.disabled = meta.disabled ?? {};
6943
8602
  meta.disabled[skillName] = record;
@@ -6949,13 +8608,13 @@ function disablePluginSkills(targetDir, plugin, marketplaceFilter) {
6949
8608
  }
6950
8609
  }
6951
8610
  if (disabled.length > 0) {
6952
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8611
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
6953
8612
  }
6954
8613
  return { disabled, alreadyDisabled, skipped };
6955
8614
  }
6956
8615
  function enablePluginSkills(targetDir, plugin, marketplaceFilter) {
6957
- const resolvedTarget = path8.resolve(targetDir);
6958
- const stashDir = path8.join(targetDir, DISABLED_SUBDIR);
8616
+ const resolvedTarget = path11.resolve(targetDir);
8617
+ const stashDir = path11.join(targetDir, DISABLED_SUBDIR);
6959
8618
  const meta = readInstalledMeta(targetDir);
6960
8619
  const enabled = [];
6961
8620
  const stashMissing = [];
@@ -6966,27 +8625,27 @@ function enablePluginSkills(targetDir, plugin, marketplaceFilter) {
6966
8625
  skipped.push(skillName);
6967
8626
  continue;
6968
8627
  }
6969
- const dest = path8.resolve(targetDir, skillName);
6970
- if (!dest.startsWith(resolvedTarget + path8.sep)) {
8628
+ const dest = path11.resolve(targetDir, skillName);
8629
+ if (!dest.startsWith(resolvedTarget + path11.sep)) {
6971
8630
  skipped.push(skillName);
6972
8631
  continue;
6973
8632
  }
6974
- const stashSrc = path8.join(stashDir, skillName);
6975
- if (!fs3.existsSync(stashSrc)) {
8633
+ const stashSrc = path11.join(stashDir, skillName);
8634
+ if (!fs6.existsSync(stashSrc)) {
6976
8635
  stashMissing.push(skillName);
6977
8636
  continue;
6978
8637
  }
6979
- if (fs3.existsSync(dest)) fs3.rmSync(dest, { recursive: true, force: true });
6980
- fs3.renameSync(stashSrc, dest);
8638
+ if (fs6.existsSync(dest)) fs6.rmSync(dest, { recursive: true, force: true });
8639
+ fs6.renameSync(stashSrc, dest);
6981
8640
  meta.skills[skillName] = record;
6982
8641
  delete meta.disabled[skillName];
6983
8642
  enabled.push(skillName);
6984
8643
  }
6985
- if (fs3.existsSync(stashDir) && fs3.readdirSync(stashDir).length === 0) {
6986
- fs3.rmdirSync(stashDir);
8644
+ if (fs6.existsSync(stashDir) && fs6.readdirSync(stashDir).length === 0) {
8645
+ fs6.rmdirSync(stashDir);
6987
8646
  }
6988
8647
  if (enabled.length > 0) {
6989
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8648
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
6990
8649
  }
6991
8650
  return { enabled, stashMissing, skipped, hadDisabled: disabledEntries.length > 0 };
6992
8651
  }
@@ -7015,7 +8674,7 @@ function listPluginStates(targetDir) {
7015
8674
  }
7016
8675
  function detectRepoBranch(repoPath) {
7017
8676
  try {
7018
- const result = execFileSync3("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
8677
+ const result = execFileSync4("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
7019
8678
  encoding: "utf8",
7020
8679
  stdio: ["pipe", "pipe", "pipe"]
7021
8680
  }).trim();
@@ -7026,7 +8685,7 @@ function detectRepoBranch(repoPath) {
7026
8685
  }
7027
8686
  function getRepoRemoteUrl(repoPath) {
7028
8687
  try {
7029
- const raw = execFileSync3("git", ["-C", repoPath, "remote", "get-url", "origin"], {
8688
+ const raw = execFileSync4("git", ["-C", repoPath, "remote", "get-url", "origin"], {
7030
8689
  encoding: "utf8",
7031
8690
  stdio: ["pipe", "pipe", "pipe"]
7032
8691
  }).trim();
@@ -7052,11 +8711,11 @@ function getInstalledPluginsForMarketplace(targetDir, marketplaceName, repoUrl)
7052
8711
  function readCustomTargets(globalConfig) {
7053
8712
  const raw = globalConfig.skillsTargets;
7054
8713
  if (!Array.isArray(raw)) return [];
7055
- return [...new Set(raw.filter((t) => typeof t === "string" && t.length > 0).map((t) => path8.resolve(t)))];
8714
+ return [...new Set(raw.filter((t) => typeof t === "string" && t.length > 0).map((t) => path11.resolve(t)))];
7056
8715
  }
7057
8716
  function rememberCustomTarget(configPath, targetDir) {
7058
- const resolved = path8.resolve(targetDir);
7059
- const builtIn = Object.values(AGENT_PATHS).flatMap((a) => [path8.resolve(a.user), resolveProjectPath(a.project)]);
8717
+ const resolved = path11.resolve(targetDir);
8718
+ const builtIn = Object.values(AGENT_PATHS).flatMap((a) => [path11.resolve(a.user), resolveProjectPath(a.project)]);
7060
8719
  if (builtIn.includes(resolved)) return false;
7061
8720
  const existing = loadJsonFile(configPath) ?? {};
7062
8721
  const targets = readCustomTargets(existing);
@@ -7065,7 +8724,7 @@ function rememberCustomTarget(configPath, targetDir) {
7065
8724
  return true;
7066
8725
  }
7067
8726
  function forgetCustomTarget(configPath, targetDir) {
7068
- const resolved = path8.resolve(targetDir);
8727
+ const resolved = path11.resolve(targetDir);
7069
8728
  const existing = loadJsonFile(configPath) ?? {};
7070
8729
  const targets = readCustomTargets(existing);
7071
8730
  if (!targets.includes(resolved)) return false;
@@ -7073,12 +8732,12 @@ function forgetCustomTarget(configPath, targetDir) {
7073
8732
  return true;
7074
8733
  }
7075
8734
  function summarizeLocation(agent, scope, targetDir) {
7076
- const resolved = path8.resolve(targetDir);
8735
+ const resolved = path11.resolve(targetDir);
7077
8736
  const base = {
7078
8737
  agent,
7079
8738
  scope,
7080
8739
  path: resolved,
7081
- exists: fs3.existsSync(resolved),
8740
+ exists: fs6.existsSync(resolved),
7082
8741
  totalSkills: 0,
7083
8742
  marketplaceSkills: 0,
7084
8743
  bundledSkills: 0,
@@ -7097,8 +8756,8 @@ function summarizeLocation(agent, scope, targetDir) {
7097
8756
  }
7098
8757
  const disabledNames = Object.keys(meta.disabled ?? {});
7099
8758
  base.disabledSkills = disabledNames.length;
7100
- const stashDir = path8.join(resolved, DISABLED_SUBDIR);
7101
- base.disabledStashMissing = disabledNames.filter((name) => !fs3.existsSync(path8.join(stashDir, name)));
8759
+ const stashDir = path11.join(resolved, DISABLED_SUBDIR);
8760
+ base.disabledStashMissing = disabledNames.filter((name) => !fs6.existsSync(path11.join(stashDir, name)));
7102
8761
  return base;
7103
8762
  }
7104
8763
  function listKnownLocations(globalConfig) {
@@ -7123,7 +8782,7 @@ function collectSkillStatus(globalConfig, locations) {
7123
8782
  const { byName, byUrl } = buildMarketplaceIndex(getAllMarketplaces(globalConfig));
7124
8783
  const remoteCache = /* @__PURE__ */ new Map();
7125
8784
  const remoteFor = (localPath) => {
7126
- if (!localPath || !fs3.existsSync(localPath)) return null;
8785
+ if (!localPath || !fs6.existsSync(localPath)) return null;
7127
8786
  if (!remoteCache.has(localPath)) remoteCache.set(localPath, getRepoRemoteUrl(localPath));
7128
8787
  return remoteCache.get(localPath) ?? null;
7129
8788
  };
@@ -7136,7 +8795,7 @@ function collectSkillStatus(globalConfig, locations) {
7136
8795
  const registered = (recordedName ? byName.get(recordedName) : void 0) ?? (record?.installedFrom ? byUrl.get(record.installedFrom) : void 0) ?? null;
7137
8796
  records.push({
7138
8797
  skill: skillName,
7139
- path: enabled ? path8.join(location.path, skillName) : path8.join(location.path, DISABLED_SUBDIR, skillName),
8798
+ path: enabled ? path11.join(location.path, skillName) : path11.join(location.path, DISABLED_SUBDIR, skillName),
7140
8799
  agent: location.agent,
7141
8800
  scope: location.scope,
7142
8801
  location: location.path,
@@ -7169,16 +8828,16 @@ function recordInstalledSkills(targetDir, skillNames, record) {
7169
8828
  for (const skillName of skillNames) {
7170
8829
  const fullRecord = { pncliVersion: getPncliVersion(), ...record, installedAt: now };
7171
8830
  meta.skills[skillName] = fullRecord;
7172
- const skillDir = path8.join(targetDir, skillName);
7173
- if (fs3.existsSync(skillDir)) {
8831
+ const skillDir = path11.join(targetDir, skillName);
8832
+ if (fs6.existsSync(skillDir)) {
7174
8833
  const origin = { version: 1, ...fullRecord };
7175
8834
  try {
7176
- fs3.writeFileSync(getSkillOriginPath(skillDir), JSON.stringify(origin, null, 2), "utf8");
8835
+ fs6.writeFileSync(getSkillOriginPath(skillDir), JSON.stringify(origin, null, 2), "utf8");
7177
8836
  } catch {
7178
8837
  }
7179
8838
  }
7180
8839
  }
7181
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8840
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
7182
8841
  }
7183
8842
  function findStaleBundledSkills(targetDir) {
7184
8843
  const currentVersion = getPncliVersion();
@@ -7246,7 +8905,13 @@ function upsertMarketplace(all, entry) {
7246
8905
  if (idxByName !== -1 && idxByName !== idxByUrl) {
7247
8906
  throw new Error(`Marketplace name "${entry.name}" is already used by a different marketplace (${all[idxByName].repoUrl}). Choose a different --name.`);
7248
8907
  }
7249
- all[idxByUrl] = { ...entry, token: entry.token ?? all[idxByUrl].token };
8908
+ const previous = all[idxByUrl];
8909
+ all[idxByUrl] = {
8910
+ ...entry,
8911
+ token: entry.token ?? previous.token,
8912
+ ...entry.username ?? previous.username ? { username: entry.username ?? previous.username } : {},
8913
+ ...entry.provider ?? previous.provider ? { provider: entry.provider ?? previous.provider } : {}
8914
+ };
7250
8915
  } else if (idxByName !== -1) {
7251
8916
  throw new Error(`Marketplace name "${entry.name}" is already registered for a different repo (${all[idxByName].repoUrl}). Choose a different --name, or remove the existing marketplace first.`);
7252
8917
  } else {
@@ -7254,52 +8919,69 @@ function upsertMarketplace(all, entry) {
7254
8919
  }
7255
8920
  }
7256
8921
  function cloneOrReuseMarketplace(url, resolvedPath, opts, marketplaceName) {
7257
- const hasGit = fs3.existsSync(path8.join(resolvedPath, ".git"));
7258
- if (fs3.existsSync(resolvedPath) && !hasGit && fs3.readdirSync(resolvedPath).length > 0) {
8922
+ const hasGit = fs6.existsSync(path11.join(resolvedPath, ".git"));
8923
+ if (fs6.existsSync(resolvedPath) && !hasGit && fs6.readdirSync(resolvedPath).length > 0) {
7259
8924
  throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
7260
8925
  }
7261
8926
  if (hasGit) {
7262
8927
  warn(`Directory already contains a git repo at ${resolvedPath} \u2014 skipping clone, updating config and re-installing plugins.`);
7263
8928
  return;
7264
8929
  }
7265
- const resolvedToken = resolveMarketplaceToken(opts.token, url);
8930
+ const { auth: credential, tokenSource } = marketplaceGitAuth({ name: marketplaceName, repoUrl: url, token: opts.token, username: opts.username, provider: opts.provider });
7266
8931
  const branchLabel = opts.branch ?? "remote default";
7267
8932
  warn(`Cloning ${url} (branch: ${branchLabel}) \u2192 ${resolvedPath}...`);
7268
- const cloneUrl = resolvedToken ? injectTokenIntoUrl(url, resolvedToken) : url;
7269
- const cloneArgs = ["clone"];
8933
+ const auth = gitAuthFor(url, credential);
8934
+ const cloneArgs = [...auth.args, "clone"];
7270
8935
  if (opts.branch) cloneArgs.push("--branch", opts.branch);
7271
- cloneArgs.push(cloneUrl, resolvedPath);
8936
+ cloneArgs.push(url, resolvedPath);
7272
8937
  try {
7273
- execFileSync3("git", cloneArgs, { stdio: ["inherit", "inherit", "pipe"] });
8938
+ execFileSync4("git", cloneArgs, { stdio: ["inherit", "inherit", "pipe"], env: { ...process.env, ...auth.env } });
7274
8939
  } catch (e) {
7275
8940
  let cloneActuallySucceeded = false;
7276
8941
  try {
7277
- execFileSync3("git", ["-C", resolvedPath, "rev-parse", "HEAD"], { stdio: "pipe" });
8942
+ execFileSync4("git", ["-C", resolvedPath, "rev-parse", "HEAD"], { stdio: "pipe" });
7278
8943
  cloneActuallySucceeded = true;
7279
8944
  } catch {
7280
8945
  }
7281
8946
  if (!cloneActuallySucceeded) {
7282
8947
  const msg = e instanceof Error ? e.message : String(e);
7283
- const tokenSource = opts.token ? "explicit" : resolvedToken ? "fallback" : "none";
7284
- throw describeGitFailure(msg, marketplaceName, tokenSource);
8948
+ throw describeGitFailure(msg, marketplaceName, tokenSource, { provider: credential?.provider, fallbackSource: credential?.source, customUsername: !!opts.username, userCredentialHost: auth.deferredToUser ?? void 0 });
7285
8949
  }
7286
8950
  }
7287
8951
  }
7288
- function pullMarketplace(marketplacePath, repoUrl, token, marketplaceName) {
7289
- const resolvedToken = resolveMarketplaceToken(token, repoUrl);
7290
- warn(`Pulling latest content for "${marketplaceName}"...`);
7291
- const gitArgs = ["-C", marketplacePath];
7292
- if (repoUrl && resolvedToken) {
7293
- gitArgs.push("-c", `remote.origin.url=${injectTokenIntoUrl(repoUrl, resolvedToken)}`);
8952
+ function gitAuthFor(url, credential, opts = {}) {
8953
+ if (!credential || !httpHostOf(url)) return { args: [], env: {}, deferredToUser: null };
8954
+ if (credential.source === "bitbucket.pat" || credential.source === "ado.pat") {
8955
+ const host = httpHostOf(url);
8956
+ const path14 = new URL(url).pathname.replace(/^\//, "");
8957
+ const base = opts.git ?? defaultGitRunner;
8958
+ const git = opts.cwd ? (args, o) => base(["-C", opts.cwd, ...args], o) : base;
8959
+ const fill = opts.fill ?? credentialFill;
8960
+ if (fill(host, path14 || void 0, git)) return { args: [], env: {}, deferredToUser: host };
8961
+ }
8962
+ return { ...inlineCredentialArgs(credential.username, credential.password), deferredToUser: null };
8963
+ }
8964
+ function stripOriginCredentials(repoPath, plainUrl) {
8965
+ try {
8966
+ execFileSync4("git", ["-C", repoPath, "remote", "set-url", "origin", plainUrl], { stdio: "pipe" });
8967
+ return true;
8968
+ } catch {
8969
+ return false;
7294
8970
  }
7295
- gitArgs.push("pull");
8971
+ }
8972
+ function pullMarketplace(marketplacePath, m, marketplaceName) {
8973
+ const repoUrl = m.repoUrl;
8974
+ const { auth: credential, tokenSource } = marketplaceGitAuth(m);
8975
+ warn(`Pulling latest content for "${marketplaceName}"...`);
8976
+ if (repoUrl && originHasCredentials(marketplacePath)) stripOriginCredentials(marketplacePath, repoUrl);
8977
+ const auth = gitAuthFor(repoUrl, credential, { cwd: marketplacePath });
8978
+ const gitArgs = ["-C", marketplacePath, ...auth.args, "pull"];
7296
8979
  let pullOutput;
7297
8980
  try {
7298
- pullOutput = execFileSync3("git", gitArgs, { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } });
8981
+ pullOutput = execFileSync4("git", gitArgs, { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, ...auth.env, LANG: "C", LC_ALL: "C" } });
7299
8982
  } catch (e) {
7300
8983
  const msg = e instanceof Error ? e.message : String(e);
7301
- const tokenSource = token ? "explicit" : resolvedToken ? "fallback" : "none";
7302
- throw describeGitFailure(msg, marketplaceName, tokenSource);
8984
+ throw describeGitFailure(msg, marketplaceName, tokenSource, { provider: credential?.provider, fallbackSource: credential?.source, customUsername: !!m.username, userCredentialHost: auth.deferredToUser ?? void 0 });
7303
8985
  }
7304
8986
  const updated = !pullOutput.includes("Already up to date");
7305
8987
  if (pullOutput.trim() && updated) warn(pullOutput.trim());
@@ -7312,7 +8994,7 @@ function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl,
7312
8994
  const branch = detectRepoBranch(marketplacePath);
7313
8995
  for (const pluginName of pluginNames) {
7314
8996
  const skillsSrc = resolveSkillsSrc(marketplacePath, pluginName);
7315
- if (!fs3.existsSync(skillsSrc)) {
8997
+ if (!fs6.existsSync(skillsSrc)) {
7316
8998
  results[pluginName] = { installed: [], failed: [] };
7317
8999
  warn(`No skills directory found for plugin "${pluginName}" in "${marketplaceName}" \u2014 skipping.`);
7318
9000
  continue;
@@ -7326,7 +9008,7 @@ function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl,
7326
9008
  results[pluginName] = { installed, failed };
7327
9009
  totalInstalled += installed.length;
7328
9010
  for (const skill of installed) {
7329
- log(` ${skill}: ${path8.join(skillsSrc, skill)} \u2192 ${path8.join(targetDir, skill)}`);
9011
+ log(` ${skill}: ${path11.join(skillsSrc, skill)} \u2192 ${path11.join(targetDir, skill)}`);
7330
9012
  }
7331
9013
  if (failed.length > 0) {
7332
9014
  warn(`Skipped ${failed.length} skill(s) with invalid names in "${pluginName}": ${failed.join(", ")}`);
@@ -7411,11 +9093,11 @@ function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
7411
9093
  const marketplaceName = marketplaceLabel(m);
7412
9094
  try {
7413
9095
  const marketplacePath = m.localPath;
7414
- if (!marketplacePath || !fs3.existsSync(marketplacePath)) {
9096
+ if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
7415
9097
  warn(`Marketplace "${marketplaceName}" local path not found \u2014 skipping.`);
7416
9098
  return { marketplace: marketplaceName, skipped: true, message: "Local path not found." };
7417
9099
  }
7418
- const { updated } = pullMarketplace(marketplacePath, m.repoUrl, m.token, marketplaceName);
9100
+ const { updated } = pullMarketplace(marketplacePath, m, marketplaceName);
7419
9101
  const pluginChoices = resolvePluginChoices(marketplacePath);
7420
9102
  if (pluginChoices.length === 0) {
7421
9103
  warn(`No plugins found in marketplace "${marketplaceName}" \u2014 skipping.`);
@@ -7434,19 +9116,40 @@ function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
7434
9116
  return { marketplace: marketplaceName, skipped: true, error: message };
7435
9117
  }
7436
9118
  }
9119
+ function parseProviderOption(value) {
9120
+ if (value === void 0) return void 0;
9121
+ if (!PROVIDERS.includes(value)) {
9122
+ throw new Error(`--provider must be one of: ${PROVIDERS.join(", ")} (got "${value}").`);
9123
+ }
9124
+ return value;
9125
+ }
9126
+ function safeProviderConfig() {
9127
+ try {
9128
+ return loadConfig();
9129
+ } catch {
9130
+ return {};
9131
+ }
9132
+ }
7437
9133
  function performMarketplaceAdd(url, localPath, opts) {
7438
- const resolvedPath = path8.resolve(localPath ?? defaultMarketplacePath(url));
9134
+ const resolvedPath = path11.resolve(localPath ?? defaultMarketplacePath(url));
7439
9135
  const marketplaceName = opts.name ?? repoNameFromUrl(url);
7440
- cloneOrReuseMarketplace(url, resolvedPath, opts, marketplaceName);
9136
+ const provider = parseProviderOption(opts.provider);
9137
+ cloneOrReuseMarketplace(url, resolvedPath, { ...opts, provider }, marketplaceName);
7441
9138
  const configPath = getGlobalConfigPath();
7442
9139
  const { existing, all } = loadMarketplacesConfig(configPath);
9140
+ if (opts.keychain && !opts.token) warn("--keychain has no effect without --token.");
9141
+ const account = `marketplaces.${marketplaceName}.token`;
9142
+ const storeInKeychain = !!(opts.token && opts.keychain);
7443
9143
  const entry = {
7444
9144
  name: marketplaceName,
7445
9145
  repoUrl: url,
7446
9146
  localPath: resolvedPath,
7447
- ...opts.token ? { token: opts.token } : {}
9147
+ ...opts.token ? { token: storeInKeychain ? keychainRef(account) : opts.token } : {},
9148
+ ...opts.username ? { username: opts.username } : {},
9149
+ ...provider ? { provider } : {}
7448
9150
  };
7449
9151
  upsertMarketplace(all, entry);
9152
+ if (storeInKeychain) getKeychainBackend().set(account, opts.token);
7450
9153
  saveMarketplaces(configPath, existing, all);
7451
9154
  const targets = resolveInstallTargets(opts);
7452
9155
  const targetResults = installAllPlugins(entry, resolvedPath, targets);
@@ -7457,6 +9160,8 @@ function performMarketplaceAdd(url, localPath, opts) {
7457
9160
  localPath: resolvedPath,
7458
9161
  branch: opts.branch ?? null,
7459
9162
  tokenConfigured: !!opts.token,
9163
+ tokenStorage: !opts.token ? null : opts.keychain ? "keychain" : "config",
9164
+ provider: detectProvider(url, safeProviderConfig(), provider),
7460
9165
  // One target keeps the flat shape earlier versions emitted; several nest under `targets`.
7461
9166
  ...targetResults.length === 1 ? { plugins: targetResults[0].plugins, total: targetResults[0].total, target: targetResults[0].target } : { targets: targetResults, total: targetResults.reduce((sum, r) => sum + r.total, 0) },
7462
9167
  ...instructions ? { instructions } : {}
@@ -7471,12 +9176,22 @@ function performMarketplaceRemove(name) {
7471
9176
  }
7472
9177
  const removed = all.splice(idx, 1)[0];
7473
9178
  saveMarketplaces(configPath, existing, all);
9179
+ let keychainEntryDeleted = false;
9180
+ if (isKeychainRef(removed.token)) {
9181
+ try {
9182
+ const account = removed.token.slice("keychain:".length);
9183
+ keychainEntryDeleted = purgeEntries(loadJsonFile(configPath) ?? {}, getKeychainBackend(), [account]).length > 0;
9184
+ } catch (err) {
9185
+ warn(`Could not delete the marketplace's keychain entry: ${err instanceof Error ? err.message : String(err)}`);
9186
+ }
9187
+ }
7474
9188
  return {
7475
9189
  removed: {
7476
9190
  name: marketplaceLabel(removed),
7477
9191
  repoUrl: removed.repoUrl,
7478
9192
  localPath: removed.localPath
7479
9193
  },
9194
+ keychainEntryDeleted,
7480
9195
  remaining: all.length
7481
9196
  };
7482
9197
  }
@@ -7493,8 +9208,8 @@ function listBundledSkillDirs() {
7493
9208
  let skillDirs = [];
7494
9209
  if (bundledDir !== "") {
7495
9210
  try {
7496
- skillDirs = fs3.readdirSync(bundledDir).filter(
7497
- (name) => fs3.existsSync(path8.join(bundledDir, name, "SKILL.md"))
9211
+ skillDirs = fs6.readdirSync(bundledDir).filter(
9212
+ (name) => fs6.existsSync(path11.join(bundledDir, name, "SKILL.md"))
7498
9213
  );
7499
9214
  } catch {
7500
9215
  }
@@ -7505,30 +9220,30 @@ function listBundledSkillDirs() {
7505
9220
  return skillDirs;
7506
9221
  }
7507
9222
  function installBundledSkillsTo(targetDir, skillDirs) {
7508
- const resolvedTarget = path8.resolve(targetDir);
9223
+ const resolvedTarget = path11.resolve(targetDir);
7509
9224
  const bundledDir = getBundledSkillsDir();
7510
9225
  const installed = [];
7511
9226
  const failed = [];
7512
9227
  for (const skillName of skillDirs) {
7513
- const existingDir = path8.resolve(targetDir, skillName);
7514
- if (!existingDir.startsWith(resolvedTarget + path8.sep)) continue;
7515
- if (fs3.existsSync(existingDir)) {
7516
- fs3.rmSync(existingDir, { recursive: true, force: true });
9228
+ const existingDir = path11.resolve(targetDir, skillName);
9229
+ if (!existingDir.startsWith(resolvedTarget + path11.sep)) continue;
9230
+ if (fs6.existsSync(existingDir)) {
9231
+ fs6.rmSync(existingDir, { recursive: true, force: true });
7517
9232
  }
7518
9233
  }
7519
9234
  log(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
7520
9235
  for (const skillName of skillDirs) {
7521
- const skillDir = path8.resolve(targetDir, skillName);
7522
- if (!skillDir.startsWith(resolvedTarget + path8.sep)) {
9236
+ const skillDir = path11.resolve(targetDir, skillName);
9237
+ if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
7523
9238
  failed.push(skillName);
7524
9239
  continue;
7525
9240
  }
7526
9241
  try {
7527
- const mdFiles = fs3.readdirSync(path8.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
7528
- fs3.mkdirSync(skillDir, { recursive: true });
9242
+ const mdFiles = fs6.readdirSync(path11.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
9243
+ fs6.mkdirSync(skillDir, { recursive: true });
7529
9244
  for (const mdFile of mdFiles) {
7530
- const content = fs3.readFileSync(path8.join(bundledDir, skillName, mdFile), "utf8");
7531
- fs3.writeFileSync(path8.join(skillDir, mdFile), content, "utf8");
9245
+ const content = fs6.readFileSync(path11.join(bundledDir, skillName, mdFile), "utf8");
9246
+ fs6.writeFileSync(path11.join(skillDir, mdFile), content, "utf8");
7532
9247
  }
7533
9248
  installed.push(skillName);
7534
9249
  } catch {
@@ -7563,6 +9278,12 @@ Org plugins (skills and AGENTS.md/CLAUDE.md from a git-hosted marketplace):
7563
9278
 
7564
9279
  Check what is installed:
7565
9280
  pncli skills status | pncli skills locations | pncli doctor
9281
+
9282
+ Private marketplaces and credentials:
9283
+ pncli skills git-auth enable # let git (and agent hosts) authenticate to marketplace hosts
9284
+ pncli config keychain migrate # move tokens out of plaintext config into the OS keychain
9285
+
9286
+ New to this? pncli skills guide # how skills management fits together (or: guide <section>)
7566
9287
  `);
7567
9288
  skills.command("install").description("Install the bundled pncli skills (project scope by default; --scope user for every repo)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES}`, DEFAULT_AGENT).option("--scope <scope>", "Installation scope: project | user", "project").option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts, cmd) => {
7568
9289
  const start = Date.now();
@@ -7587,7 +9308,7 @@ Check what is installed:
7587
9308
  return;
7588
9309
  }
7589
9310
  const agentName = resolveAgentName(opts);
7590
- const targetDir = opts.target ? path8.resolve(opts.target) : resolveScopedPath(resolveAgentPaths(agentName), scope);
9311
+ const targetDir = opts.target ? path11.resolve(opts.target) : resolveScopedPath(resolveAgentPaths(agentName), scope);
7591
9312
  const { installed, failed } = installBundledSkillsTo(targetDir, skillDirs);
7592
9313
  let tracked = false;
7593
9314
  if (opts.target && installed.length > 0) {
@@ -7616,12 +9337,12 @@ Check what is installed:
7616
9337
  try {
7617
9338
  let targetDir;
7618
9339
  if (opts.target) {
7619
- targetDir = path8.resolve(opts.target);
9340
+ targetDir = path11.resolve(opts.target);
7620
9341
  } else {
7621
9342
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
7622
9343
  targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
7623
9344
  }
7624
- if (!fs3.existsSync(targetDir)) {
9345
+ if (!fs6.existsSync(targetDir)) {
7625
9346
  success({
7626
9347
  skills: [],
7627
9348
  total: 0,
@@ -7632,13 +9353,13 @@ Check what is installed:
7632
9353
  return;
7633
9354
  }
7634
9355
  const meta = readInstalledMeta(targetDir);
7635
- const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
9356
+ const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
7636
9357
  if (name.startsWith(".")) return false;
7637
- const skillPath = path8.join(targetDir, name, "SKILL.md");
7638
- return fs3.existsSync(skillPath);
9358
+ const skillPath = path11.join(targetDir, name, "SKILL.md");
9359
+ return fs6.existsSync(skillPath);
7639
9360
  });
7640
9361
  const skillsList = skillDirs.map((name) => {
7641
- const content = fs3.readFileSync(path8.join(targetDir, name, "SKILL.md"), "utf8");
9362
+ const content = fs6.readFileSync(path11.join(targetDir, name, "SKILL.md"), "utf8");
7642
9363
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7643
9364
  const data = {};
7644
9365
  const metadata = {};
@@ -7691,24 +9412,24 @@ Check what is installed:
7691
9412
  try {
7692
9413
  let targetDir;
7693
9414
  if (opts.target) {
7694
- targetDir = path8.resolve(opts.target);
9415
+ targetDir = path11.resolve(opts.target);
7695
9416
  } else {
7696
9417
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
7697
9418
  targetDir = resolveScopedPath(agentConfig, opts.scope === "project" ? "project" : "user");
7698
9419
  }
7699
- const resolvedTarget = path8.resolve(targetDir);
7700
- const skillDir = path8.resolve(targetDir, name);
7701
- if (!skillDir.startsWith(resolvedTarget + path8.sep)) {
9420
+ const resolvedTarget = path11.resolve(targetDir);
9421
+ const skillDir = path11.resolve(targetDir, name);
9422
+ if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
7702
9423
  throw new Error(`Invalid skill name: "${name}"`);
7703
9424
  }
7704
- if (!fs3.existsSync(skillDir)) {
9425
+ if (!fs6.existsSync(skillDir)) {
7705
9426
  throw new Error(`Skill "${name}" not found at ${skillDir}`);
7706
9427
  }
7707
9428
  const meta = readInstalledMeta(targetDir);
7708
9429
  const record = meta.skills[name];
7709
- fs3.rmSync(skillDir, { recursive: true, force: true });
9430
+ fs6.rmSync(skillDir, { recursive: true, force: true });
7710
9431
  delete meta.skills[name];
7711
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9432
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
7712
9433
  success({
7713
9434
  uninstalled: name,
7714
9435
  target: targetDir,
@@ -7779,7 +9500,7 @@ Check what is installed:
7779
9500
  const start = Date.now();
7780
9501
  try {
7781
9502
  const configPath = getGlobalConfigPath();
7782
- const resolved = path8.resolve(dir);
9503
+ const resolved = path11.resolve(dir);
7783
9504
  const forgotten = forgetCustomTarget(configPath, resolved);
7784
9505
  if (!forgotten) {
7785
9506
  warn(`${resolved} is not a tracked custom target \u2014 nothing to forget.`);
@@ -7793,6 +9514,44 @@ Check what is installed:
7793
9514
  fail(err, "skills", "forget-target", start);
7794
9515
  }
7795
9516
  });
9517
+ skills.command("guide").description("Explain how skills management works: bundled skill vs marketplace plugins, agent hosts, scopes, sync, and auth").argument("[section]", "Print only one section (e.g. hosts, sync, auth, troubleshooting)").option("--sections", "List the section names as JSON instead of printing the guide").action((section, cmdOpts) => {
9518
+ const start = Date.now();
9519
+ try {
9520
+ const guidePath = findGuidePath();
9521
+ if (!guidePath) throw new Error(`${GUIDE_FILE} is missing from this pncli install \u2014 reinstall pncli, or read it online at https://kolatts.github.io/pncli/skills-guide/`);
9522
+ const markdown = fs6.readFileSync(guidePath, "utf8").replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n/, "");
9523
+ if (cmdOpts.sections) {
9524
+ success({ sections: listGuideSections(markdown), path: guidePath }, "skills", "guide", start);
9525
+ return;
9526
+ }
9527
+ if (section) {
9528
+ const text = extractGuideSection(markdown, section);
9529
+ if (!text) {
9530
+ throw new Error(`No guide section matches "${section}". Sections: ${listGuideSections(markdown).map((s) => s.slug).join(", ")}`);
9531
+ }
9532
+ writeRawOutput(text);
9533
+ return;
9534
+ }
9535
+ writeRawOutput(markdown);
9536
+ } catch (err) {
9537
+ fail(err, "skills", "guide", start);
9538
+ }
9539
+ });
9540
+ registerGitAuthCommands(skills);
9541
+ skills.command("git-credential", { hidden: true }).description("Git credential helper (used by git after `skills git-auth enable`; not for direct use)").option("--marketplace <name>", "Answer with this marketplace's credential (written by git-auth enable)").argument("<operation>", "get | store | erase (supplied by git)").action(async (operation, cmdOpts) => {
9542
+ if (operation !== "get") return;
9543
+ try {
9544
+ const chunks = [];
9545
+ for await (const chunk of process.stdin) chunks.push(chunk);
9546
+ const request = parseCredentialRequest(Buffer.concat(chunks).toString("utf8"));
9547
+ const configPath = skills.optsWithGlobals().config;
9548
+ const globalConfig = loadJsonFile(getGlobalConfigPath(configPath)) ?? {};
9549
+ const cfg = loadConfig({ configPath, keychainPaths: ["github.token", "bitbucket.pat", "ado.pat"] });
9550
+ const answer = resolveCredential(request, globalConfig, cfg, void 0, cmdOpts.marketplace);
9551
+ if (answer) process.stdout.write(formatCredentialAnswer(answer));
9552
+ } catch {
9553
+ }
9554
+ });
7796
9555
  const marketplace = skills.command("marketplace").description("Org plugins from git-hosted marketplaces: add, sync, enable/disable, and shipped AGENTS.md/CLAUDE.md");
7797
9556
  marketplace.addHelpText("after", `
7798
9557
  Workflow:
@@ -7810,7 +9569,7 @@ Shipped instructions:
7810
9569
 
7811
9570
  Plugin skills always install at user scope. --agent picks the host (default: ${DEFAULT_AGENT}); --all-agents covers all of them.
7812
9571
  `);
7813
- marketplace.command("add").alias("setup").description("Register a new marketplace, clone it, install all its plugins, and apply its shipped AGENTS.md / CLAUDE.md (`setup` is an alias)").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action((url, localPath, opts) => {
9572
+ marketplace.command("add").alias("setup").description("Register a new marketplace, clone it, install all its plugins, and apply its shipped AGENTS.md / CLAUDE.md (`setup` is an alias)").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--keychain", "Store --token in the OS keychain instead of plaintext config (see: pncli config keychain --help)").option("--username <username>", "Username sent with the token (default: x-access-token on github.com, x-token-auth elsewhere; Bitbucket Data Center personal tokens usually need your username)").option("--provider <provider>", "Git host type, when detection guesses wrong: github | bitbucket | ado | git").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action((url, localPath, opts) => {
7814
9573
  const marketplaceIdx = process.argv.indexOf("marketplace");
7815
9574
  const invokedAs = marketplaceIdx !== -1 && process.argv[marketplaceIdx + 1] === "setup" ? "marketplace-setup" : "marketplace-add";
7816
9575
  return marketplaceAddAction(url, localPath, opts, invokedAs);
@@ -7820,13 +9579,17 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7820
9579
  try {
7821
9580
  const configPath = getGlobalConfigPath();
7822
9581
  const all = loadMarketplaces(configPath);
9582
+ const providerCfg = safeProviderConfig();
7823
9583
  success({
7824
9584
  marketplaces: all.map((m) => ({
7825
9585
  name: marketplaceLabel(m),
7826
9586
  repoUrl: m.repoUrl,
7827
9587
  localPath: m.localPath,
7828
9588
  tokenConfigured: !!m.token,
7829
- upstreamRemote: m.localPath && fs3.existsSync(m.localPath) ? getRepoRemoteUrl(m.localPath) : null
9589
+ tokenStorage: !m.token ? null : isKeychainRef(m.token) ? "keychain" : "config",
9590
+ provider: detectProvider(m.repoUrl, providerCfg, m.provider),
9591
+ username: m.username ?? null,
9592
+ upstreamRemote: m.localPath && fs6.existsSync(m.localPath) ? getRepoRemoteUrl(m.localPath) : null
7830
9593
  })),
7831
9594
  total: all.length
7832
9595
  }, "skills", "marketplace-list", start);
@@ -7834,6 +9597,54 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7834
9597
  fail(err, "skills", "marketplace-list", start);
7835
9598
  }
7836
9599
  });
9600
+ marketplace.command("update").description("Change a registered marketplace's token, username, or provider without re-adding it (rotate a token here)").argument("<name>", "Marketplace name (or repo URL)").option("--token <token>", "New access token for clone and pull").option("--keychain", "Store --token in the OS keychain instead of plaintext config").option("--username <username>", "Username sent with the token (Bitbucket Data Center personal tokens usually need your username)").option("--provider <provider>", "Git host type: github | bitbucket | ado | git").option("--clear-token", "Remove the marketplace's own token (falls back to the provider token pncli has, if any)").option("--clear-username", "Go back to the default username").option("--clear-provider", "Go back to automatic provider detection").action((name, opts) => {
9601
+ const start = Date.now();
9602
+ try {
9603
+ if (opts.token && opts.clearToken) throw new Error("Pass --token or --clear-token, not both.");
9604
+ if (opts.keychain && !opts.token) throw new Error("--keychain needs --token.");
9605
+ const provider = parseProviderOption(opts.provider);
9606
+ const configPath = getGlobalConfigPath();
9607
+ const { existing, all } = loadMarketplacesConfig(configPath);
9608
+ const idx = all.findIndex((m) => m.name === name || m.repoUrl === name);
9609
+ if (idx === -1) throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
9610
+ const entry = { ...all[idx] };
9611
+ const oldToken = entry.token;
9612
+ const account = `marketplaces.${marketplaceLabel(entry)}.token`;
9613
+ if (opts.token) {
9614
+ if (opts.keychain) {
9615
+ getKeychainBackend().set(account, opts.token);
9616
+ entry.token = keychainRef(account);
9617
+ } else {
9618
+ entry.token = opts.token;
9619
+ }
9620
+ }
9621
+ if (opts.clearToken) delete entry.token;
9622
+ if (opts.username) entry.username = opts.username;
9623
+ if (opts.clearUsername) delete entry.username;
9624
+ if (provider) entry.provider = provider;
9625
+ if (opts.clearProvider) delete entry.provider;
9626
+ all[idx] = entry;
9627
+ saveMarketplaces(configPath, existing, all);
9628
+ if (isKeychainRef(oldToken) && oldToken !== entry.token) {
9629
+ try {
9630
+ purgeEntries(loadJsonFile(configPath) ?? {}, getKeychainBackend(), [oldToken.slice("keychain:".length)]);
9631
+ } catch (err) {
9632
+ warn(`Could not delete the old keychain entry: ${err instanceof Error ? err.message : String(err)}`);
9633
+ }
9634
+ }
9635
+ success({
9636
+ name: marketplaceLabel(entry),
9637
+ repoUrl: entry.repoUrl,
9638
+ tokenConfigured: !!entry.token,
9639
+ tokenStorage: !entry.token ? null : isKeychainRef(entry.token) ? "keychain" : "config",
9640
+ username: entry.username ?? null,
9641
+ provider: detectProvider(entry.repoUrl, safeProviderConfig(), entry.provider),
9642
+ next: opts.username || opts.provider || opts.clearUsername || opts.clearProvider ? `If git-auth is enabled for it, refresh that too: pncli skills git-auth enable --marketplace ${marketplaceLabel(entry)}. Then verify with: pncli doctor` : "Verify with: pncli doctor"
9643
+ }, "skills", "marketplace-update", start);
9644
+ } catch (err) {
9645
+ fail(err, "skills", "marketplace-update", start);
9646
+ }
9647
+ });
7837
9648
  marketplace.command("plugins").description("List the plugins available in a registered marketplace").argument("<name>", "Marketplace name (or repo URL) to inspect").action((name) => {
7838
9649
  const start = Date.now();
7839
9650
  try {
@@ -7843,7 +9654,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7843
9654
  if (!found) {
7844
9655
  throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
7845
9656
  }
7846
- if (!found.localPath || !fs3.existsSync(found.localPath)) {
9657
+ if (!found.localPath || !fs6.existsSync(found.localPath)) {
7847
9658
  throw new Error(`Marketplace "${marketplaceLabel(found)}" local path not found at ${found.localPath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
7848
9659
  }
7849
9660
  const plugins = resolvePluginChoices(found.localPath);
@@ -7868,8 +9679,8 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7868
9679
  const start = Date.now();
7869
9680
  try {
7870
9681
  const configPath = getGlobalConfigPath();
7871
- const allMarketplaces = loadMarketplaces(configPath);
7872
- if (allMarketplaces.length === 0) {
9682
+ const allMarketplaces2 = loadMarketplaces(configPath);
9683
+ if (allMarketplaces2.length === 0) {
7873
9684
  throw new Error("No marketplaces configured. Run: pncli skills marketplace add <url>");
7874
9685
  }
7875
9686
  const bareSync = isBareMarketplaceSync(plugin, opts);
@@ -7877,49 +9688,49 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7877
9688
  const syncOpts = { force: opts.force ?? false, installedOnly: bareSync ? true : opts.installedOnly ?? false, instructions: opts.instructions !== false };
7878
9689
  const targetSummary = targets.length === 1 ? { target: targets[0].target } : { targets: targets.map((t) => t.target) };
7879
9690
  if (opts.marketplace === "all" || bareSync) {
7880
- const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targets, plugin ?? "all", syncOpts));
9691
+ const results = allMarketplaces2.map((m) => syncMarketplacePlugins(m, targets, plugin ?? "all", syncOpts));
7881
9692
  success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
7882
9693
  return;
7883
9694
  }
7884
9695
  let selectedMarketplace;
7885
9696
  let selectedPlugin = plugin;
7886
- const canGoBack = !opts.marketplace && allMarketplaces.length > 1;
9697
+ const canGoBack = !opts.marketplace && allMarketplaces2.length > 1;
7887
9698
  if (opts.marketplace) {
7888
- const found = allMarketplaces.find((m) => m.name === opts.marketplace || m.repoUrl === opts.marketplace);
9699
+ const found = allMarketplaces2.find((m) => m.name === opts.marketplace || m.repoUrl === opts.marketplace);
7889
9700
  if (!found) {
7890
9701
  throw new Error(`Marketplace "${opts.marketplace}" not found. Run: pncli skills marketplace list`);
7891
9702
  }
7892
9703
  selectedMarketplace = found;
7893
- } else if (allMarketplaces.length === 1) {
7894
- selectedMarketplace = allMarketplaces[0];
9704
+ } else if (allMarketplaces2.length === 1) {
9705
+ selectedMarketplace = allMarketplaces2[0];
7895
9706
  }
7896
9707
  for (; ; ) {
7897
9708
  if (!selectedMarketplace) {
7898
- assertInteractive(`Multiple marketplaces are registered (${allMarketplaces.map(marketplaceLabel).join(", ")}). Pass --marketplace <name> (or --marketplace all) to run this non-interactively.`);
9709
+ assertInteractive(`Multiple marketplaces are registered (${allMarketplaces2.map(marketplaceLabel).join(", ")}). Pass --marketplace <name> (or --marketplace all) to run this non-interactively.`);
7899
9710
  const chosen = await select({
7900
9711
  message: "Select a marketplace to sync:",
7901
9712
  choices: [
7902
9713
  { value: ALL_MARKETPLACES, name: "All marketplaces \u2014 sync every plugin from every marketplace" },
7903
9714
  // Index-based values: labels alone aren't guaranteed unique (two marketplaces can share a derived name).
7904
- ...allMarketplaces.map((m2, i) => ({ value: String(i), name: `${marketplaceLabel(m2)} \u2014 ${m2.repoUrl ?? ""}` }))
9715
+ ...allMarketplaces2.map((m2, i) => ({ value: String(i), name: `${marketplaceLabel(m2)} \u2014 ${m2.repoUrl ?? ""}` }))
7905
9716
  ]
7906
9717
  });
7907
9718
  if (chosen === ALL_MARKETPLACES) {
7908
- const results = allMarketplaces.map((m2) => syncMarketplacePlugins(m2, targets, selectedPlugin ?? "all", syncOpts));
9719
+ const results = allMarketplaces2.map((m2) => syncMarketplacePlugins(m2, targets, selectedPlugin ?? "all", syncOpts));
7909
9720
  success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
7910
9721
  return;
7911
9722
  }
7912
- selectedMarketplace = allMarketplaces[Number(chosen)];
9723
+ selectedMarketplace = allMarketplaces2[Number(chosen)];
7913
9724
  if (!selectedMarketplace) {
7914
9725
  throw new Error(`Marketplace "${chosen}" not found.`);
7915
9726
  }
7916
9727
  }
7917
9728
  const marketplaceName = marketplaceLabel(selectedMarketplace);
7918
9729
  const marketplacePath = selectedMarketplace.localPath;
7919
- if (!marketplacePath || !fs3.existsSync(marketplacePath)) {
9730
+ if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
7920
9731
  throw new Error(`Marketplace "${marketplaceName}" local path not found at ${marketplacePath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
7921
9732
  }
7922
- const { updated } = pullMarketplace(marketplacePath, selectedMarketplace.repoUrl, selectedMarketplace.token, marketplaceName);
9733
+ const { updated } = pullMarketplace(marketplacePath, selectedMarketplace, marketplaceName);
7923
9734
  const pluginChoices = resolvePluginChoices(marketplacePath);
7924
9735
  if (pluginChoices.length === 0) {
7925
9736
  throw new Error(`No plugins found in marketplace "${marketplaceName}". Check the marketplace repository structure.`);
@@ -7973,7 +9784,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7973
9784
  const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
7974
9785
  const agents = Object.keys(AGENT_PATHS);
7975
9786
  const marketplaces = selected.map((m) => {
7976
- const shipped = m.localPath && fs3.existsSync(m.localPath) ? findMarketplaceInstructions(m.localPath).map((s) => s.file) : [];
9787
+ const shipped = m.localPath && fs6.existsSync(m.localPath) ? findMarketplaceInstructions(m.localPath).map((s) => s.file) : [];
7977
9788
  return {
7978
9789
  marketplace: marketplaceLabel(m),
7979
9790
  localPath: m.localPath ?? null,
@@ -7999,7 +9810,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7999
9810
  const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
8000
9811
  const results = selected.map((m) => {
8001
9812
  const name = marketplaceLabel(m);
8002
- if (!m.localPath || !fs3.existsSync(m.localPath)) {
9813
+ if (!m.localPath || !fs6.existsSync(m.localPath)) {
8003
9814
  return { marketplace: name, skipped: true, message: "Local path not found. Run: pncli skills marketplace add <url>" };
8004
9815
  }
8005
9816
  if (findMarketplaceInstructions(m.localPath).length === 0) {
@@ -8037,31 +9848,31 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8037
9848
  try {
8038
9849
  let targetDir;
8039
9850
  if (opts.target) {
8040
- targetDir = path8.resolve(opts.target);
9851
+ targetDir = path11.resolve(opts.target);
8041
9852
  } else {
8042
9853
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
8043
9854
  targetDir = resolveScopedPath(agentConfig, opts.scope ?? "user");
8044
9855
  }
8045
- const resolvedTarget = path8.resolve(targetDir);
8046
- if (!fs3.existsSync(targetDir)) {
9856
+ const resolvedTarget = path11.resolve(targetDir);
9857
+ if (!fs6.existsSync(targetDir)) {
8047
9858
  success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
8048
9859
  return;
8049
9860
  }
8050
9861
  const meta = readInstalledMeta(targetDir);
8051
9862
  const removed = [];
8052
9863
  const skipped = [];
8053
- const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
9864
+ const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
8054
9865
  if (name.startsWith(".")) return false;
8055
- const p = path8.join(targetDir, name);
9866
+ const p = path11.join(targetDir, name);
8056
9867
  try {
8057
- return fs3.statSync(p).isDirectory();
9868
+ return fs6.statSync(p).isDirectory();
8058
9869
  } catch {
8059
9870
  return false;
8060
9871
  }
8061
9872
  });
8062
9873
  for (const skillName of skillDirs) {
8063
- const skillDir = path8.resolve(targetDir, skillName);
8064
- if (!skillDir.startsWith(resolvedTarget + path8.sep)) continue;
9874
+ const skillDir = path11.resolve(targetDir, skillName);
9875
+ if (!skillDir.startsWith(resolvedTarget + path11.sep)) continue;
8065
9876
  let record = meta.skills[skillName] ?? null;
8066
9877
  if (!record) {
8067
9878
  const perSkill = readSkillOrigin(skillDir);
@@ -8086,12 +9897,12 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8086
9897
  skipped.push(skillName);
8087
9898
  continue;
8088
9899
  }
8089
- fs3.rmSync(skillDir, { recursive: true, force: true });
9900
+ fs6.rmSync(skillDir, { recursive: true, force: true });
8090
9901
  delete meta.skills[skillName];
8091
9902
  removed.push(skillName);
8092
9903
  }
8093
9904
  if (removed.length > 0) {
8094
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9905
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8095
9906
  }
8096
9907
  success({
8097
9908
  removed,
@@ -8109,7 +9920,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8109
9920
  const start = Date.now();
8110
9921
  try {
8111
9922
  const targetDir = resolveTargetDir(opts);
8112
- if (!fs3.existsSync(targetDir)) {
9923
+ if (!fs6.existsSync(targetDir)) {
8113
9924
  success({ disabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-disable", start);
8114
9925
  return;
8115
9926
  }
@@ -8121,7 +9932,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8121
9932
  total: result.disabled.length,
8122
9933
  target: targetDir,
8123
9934
  plugin,
8124
- stash: path8.join(targetDir, DISABLED_SUBDIR),
9935
+ stash: path11.join(targetDir, DISABLED_SUBDIR),
8125
9936
  ...opts.marketplace ? { marketplace: opts.marketplace } : {}
8126
9937
  }, "skills", "marketplace-disable", start);
8127
9938
  } catch (err) {
@@ -8132,7 +9943,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8132
9943
  const start = Date.now();
8133
9944
  try {
8134
9945
  const targetDir = resolveTargetDir(opts);
8135
- if (!fs3.existsSync(targetDir)) {
9946
+ if (!fs6.existsSync(targetDir)) {
8136
9947
  success({ enabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-enable", start);
8137
9948
  return;
8138
9949
  }
@@ -8171,9 +9982,9 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8171
9982
  const appliedInstructions = /* @__PURE__ */ new Map();
8172
9983
  const instructionAgents = opts.target ? [] : [resolveAgentName(opts)];
8173
9984
  for (; ; ) {
8174
- const states = fs3.existsSync(targetDir) ? listPluginStates(targetDir) : [];
9985
+ const states = fs6.existsSync(targetDir) ? listPluginStates(targetDir) : [];
8175
9986
  const registered = loadMarketplaces(getGlobalConfigPath());
8176
- const shipping = registered.filter((m) => m.localPath && fs3.existsSync(m.localPath) && findMarketplaceInstructions(m.localPath).length > 0);
9987
+ const shipping = registered.filter((m) => m.localPath && fs6.existsSync(m.localPath) && findMarketplaceInstructions(m.localPath).length > 0);
8177
9988
  const action = await select({
8178
9989
  message: `Manage skills marketplaces (target: ${targetDir}):`,
8179
9990
  choices: [
@@ -8287,15 +10098,15 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8287
10098
  const start = Date.now();
8288
10099
  try {
8289
10100
  const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
8290
- if (!fs3.existsSync(userDir)) {
10101
+ if (!fs6.existsSync(userDir)) {
8291
10102
  success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
8292
10103
  return;
8293
10104
  }
8294
- const skillDirs = fs3.readdirSync(userDir).filter((name) => {
10105
+ const skillDirs = fs6.readdirSync(userDir).filter((name) => {
8295
10106
  if (name.startsWith(".")) return false;
8296
- const p = path8.join(userDir, name);
10107
+ const p = path11.join(userDir, name);
8297
10108
  try {
8298
- return fs3.statSync(p).isDirectory();
10109
+ return fs6.statSync(p).isDirectory();
8299
10110
  } catch {
8300
10111
  return false;
8301
10112
  }
@@ -8311,13 +10122,13 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
8311
10122
  );
8312
10123
  }
8313
10124
  for (const name of skillDirs) {
8314
- const skillDir = path8.resolve(userDir, name);
8315
- if (!skillDir.startsWith(path8.resolve(userDir) + path8.sep)) continue;
8316
- fs3.rmSync(skillDir, { recursive: true, force: true });
10125
+ const skillDir = path11.resolve(userDir, name);
10126
+ if (!skillDir.startsWith(path11.resolve(userDir) + path11.sep)) continue;
10127
+ fs6.rmSync(skillDir, { recursive: true, force: true });
8317
10128
  }
8318
10129
  const metaPath = getInstalledMetaPath(userDir);
8319
- if (fs3.existsSync(metaPath)) {
8320
- fs3.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
10130
+ if (fs6.existsSync(metaPath)) {
10131
+ fs6.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
8321
10132
  }
8322
10133
  success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
8323
10134
  } catch (err) {
@@ -8326,46 +10137,46 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
8326
10137
  });
8327
10138
  }
8328
10139
  function resolvePluginChoices(marketplacePath) {
8329
- const marketplaceJsonPath = path8.join(marketplacePath, ".claude-plugin", "marketplace.json");
8330
- if (fs3.existsSync(marketplaceJsonPath)) {
10140
+ const marketplaceJsonPath = path11.join(marketplacePath, ".claude-plugin", "marketplace.json");
10141
+ if (fs6.existsSync(marketplaceJsonPath)) {
8331
10142
  try {
8332
- const meta = JSON.parse(fs3.readFileSync(marketplaceJsonPath, "utf8"));
10143
+ const meta = JSON.parse(fs6.readFileSync(marketplaceJsonPath, "utf8"));
8333
10144
  if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
8334
10145
  return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
8335
10146
  }
8336
10147
  } catch {
8337
10148
  }
8338
10149
  }
8339
- const pluginsDir = path8.join(marketplacePath, "plugins");
8340
- if (fs3.existsSync(pluginsDir)) {
8341
- return fs3.readdirSync(pluginsDir).filter((name) => fs3.statSync(path8.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
10150
+ const pluginsDir = path11.join(marketplacePath, "plugins");
10151
+ if (fs6.existsSync(pluginsDir)) {
10152
+ return fs6.readdirSync(pluginsDir).filter((name) => fs6.statSync(path11.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
8342
10153
  }
8343
10154
  return [];
8344
10155
  }
8345
10156
  function resolveSkillsSrc(marketplacePath, selectedPlugin) {
8346
- const pluginsBase = path8.resolve(marketplacePath, "plugins");
8347
- const skillsSrc = path8.resolve(pluginsBase, selectedPlugin, "skills");
8348
- if (!skillsSrc.startsWith(pluginsBase + path8.sep)) {
10157
+ const pluginsBase = path11.resolve(marketplacePath, "plugins");
10158
+ const skillsSrc = path11.resolve(pluginsBase, selectedPlugin, "skills");
10159
+ if (!skillsSrc.startsWith(pluginsBase + path11.sep)) {
8349
10160
  throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
8350
10161
  }
8351
10162
  return skillsSrc;
8352
10163
  }
8353
10164
  function copyPluginSkills(skillsSrc, targetDir, meta) {
8354
- fs3.mkdirSync(targetDir, { recursive: true });
8355
- const resolvedTarget = path8.resolve(targetDir);
8356
- const skillNames = fs3.readdirSync(skillsSrc).filter(
8357
- (name) => fs3.statSync(path8.join(skillsSrc, name)).isDirectory()
10165
+ fs6.mkdirSync(targetDir, { recursive: true });
10166
+ const resolvedTarget = path11.resolve(targetDir);
10167
+ const skillNames = fs6.readdirSync(skillsSrc).filter(
10168
+ (name) => fs6.statSync(path11.join(skillsSrc, name)).isDirectory()
8358
10169
  );
8359
10170
  const installed = [];
8360
10171
  const failed = [];
8361
10172
  for (const skillName of skillNames) {
8362
- const dest = path8.resolve(targetDir, skillName);
8363
- if (!dest.startsWith(resolvedTarget + path8.sep)) {
10173
+ const dest = path11.resolve(targetDir, skillName);
10174
+ if (!dest.startsWith(resolvedTarget + path11.sep)) {
8364
10175
  failed.push(skillName);
8365
10176
  continue;
8366
10177
  }
8367
- fs3.rmSync(dest, { recursive: true, force: true });
8368
- fs3.cpSync(path8.join(skillsSrc, skillName), dest, { recursive: true });
10178
+ fs6.rmSync(dest, { recursive: true, force: true });
10179
+ fs6.cpSync(path11.join(skillsSrc, skillName), dest, { recursive: true });
8369
10180
  installed.push(skillName);
8370
10181
  }
8371
10182
  if (meta) {
@@ -8382,9 +10193,10 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
8382
10193
 
8383
10194
  // src/services/config/commands.ts
8384
10195
  import chalk from "chalk";
8385
- import fs4 from "fs";
10196
+ import fs7 from "fs";
8386
10197
  function registerConfigCommands(program2) {
8387
10198
  const config = program2.command("config").description("Manage pncli configuration");
10199
+ registerKeychainCommands(config, program2);
8388
10200
  config.command("init").description("Setup wizard for service credentials (interactive)").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
8389
10201
  const start = Date.now();
8390
10202
  try {
@@ -8496,8 +10308,8 @@ function registerConfigCommands(program2) {
8496
10308
  if (cfg.ado.baseUrl) {
8497
10309
  try {
8498
10310
  const collection = cfg.defaults.ado?.collection;
8499
- const path11 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
8500
- await http.ado(path11);
10311
+ const path14 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
10312
+ await http.ado(path14);
8501
10313
  results.ado = { ok: true, message: "connected" };
8502
10314
  } catch (err) {
8503
10315
  results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -8790,7 +10602,7 @@ async function initGlobalConfig(start) {
8790
10602
  message: "Jira base URL (e.g. jira.imagile.dev):",
8791
10603
  default: ""
8792
10604
  });
8793
- const jiraApiToken = await password({
10605
+ const jiraApiToken = await password2({
8794
10606
  message: "Jira personal access token:"
8795
10607
  });
8796
10608
  process.stderr.write("\n\u2500\u2500 Bitbucket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
@@ -8798,7 +10610,7 @@ async function initGlobalConfig(start) {
8798
10610
  message: "Bitbucket Server base URL (e.g. bitbucket.imagile.dev):",
8799
10611
  default: ""
8800
10612
  });
8801
- const bitbucketPat = await password({
10613
+ const bitbucketPat = await password2({
8802
10614
  message: "Bitbucket personal access token:"
8803
10615
  });
8804
10616
  process.stderr.write("\n\u2500\u2500 GitHub \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
@@ -8813,7 +10625,7 @@ async function initGlobalConfig(start) {
8813
10625
  message: "GitHub API base URL\n GitHub.com: api.github.com\n GitHub Enterprise: <host>/api/v3\n URL: ",
8814
10626
  default: ""
8815
10627
  });
8816
- githubToken = await password({
10628
+ githubToken = await password2({
8817
10629
  message: "GitHub personal access token (classic or fine-grained):"
8818
10630
  });
8819
10631
  if (githubBaseUrl && githubToken) {
@@ -8840,7 +10652,7 @@ async function initGlobalConfig(start) {
8840
10652
  message: "Confluence base URL (e.g. confluence.imagile.dev):",
8841
10653
  default: ""
8842
10654
  });
8843
- const confluenceApiToken = await password({
10655
+ const confluenceApiToken = await password2({
8844
10656
  message: "Confluence personal access token:"
8845
10657
  });
8846
10658
  process.stderr.write("\n\u2500\u2500 Artifactory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
@@ -8858,7 +10670,7 @@ async function initGlobalConfig(start) {
8858
10670
  message: "Artifactory base URL (e.g. artifactory.imagile.dev):",
8859
10671
  default: ""
8860
10672
  });
8861
- artifactoryToken = await password({
10673
+ artifactoryToken = await password2({
8862
10674
  message: "Artifactory API token:"
8863
10675
  });
8864
10676
  process.stderr.write("\nConfigure which ecosystems you use (skip any that don't apply):\n");
@@ -8887,7 +10699,7 @@ async function initGlobalConfig(start) {
8887
10699
  message: "SonarQube Server base URL (e.g. sonar.imagile.dev):",
8888
10700
  default: ""
8889
10701
  });
8890
- sonarToken = await password({
10702
+ sonarToken = await password2({
8891
10703
  message: "SonarQube personal access token:"
8892
10704
  });
8893
10705
  }
@@ -8905,7 +10717,7 @@ async function initGlobalConfig(start) {
8905
10717
  default: false
8906
10718
  });
8907
10719
  if (useSdeConnectionString) {
8908
- const sdeConnection = await password({
10720
+ const sdeConnection = await password2({
8909
10721
  message: "SDElements connection string (api-token@hostname, e.g. mytoken@myorg.sdelements.com):"
8910
10722
  });
8911
10723
  const atIdx = sdeConnection.indexOf("@");
@@ -8918,7 +10730,7 @@ async function initGlobalConfig(start) {
8918
10730
  message: "SDElements hostname\n Cloud-hosted: imagile.sdelements.com\n On-premise: sde.imagile.dev\n Host: ",
8919
10731
  default: ""
8920
10732
  });
8921
- sdeToken = await password({
10733
+ sdeToken = await password2({
8922
10734
  message: "SDElements API token:"
8923
10735
  });
8924
10736
  }
@@ -8940,7 +10752,7 @@ async function initGlobalConfig(start) {
8940
10752
  message: "Azure DevOps Server base URL (e.g. tfs.imagile.dev or devops.imagile.dev/tfs):",
8941
10753
  default: ""
8942
10754
  });
8943
- adoPat = await password({
10755
+ adoPat = await password2({
8944
10756
  message: "Azure DevOps personal access token:"
8945
10757
  });
8946
10758
  adoCollection = await input2({
@@ -9016,7 +10828,7 @@ async function initGlobalConfig(start) {
9016
10828
  message: "Jenkins username:",
9017
10829
  default: ""
9018
10830
  });
9019
- jenkinsApiToken = await password({
10831
+ jenkinsApiToken = await password2({
9020
10832
  message: "Jenkins API token:"
9021
10833
  });
9022
10834
  }
@@ -9039,7 +10851,7 @@ async function initGlobalConfig(start) {
9039
10851
  message: "Checkmarx One tenant name (IAM realm, e.g. imagile):",
9040
10852
  default: ""
9041
10853
  });
9042
- checkmarxApiKey = await password({
10854
+ checkmarxApiKey = await password2({
9043
10855
  message: "Checkmarx One API key (leave blank to use an OAuth client):"
9044
10856
  });
9045
10857
  if (!checkmarxApiKey) {
@@ -9047,7 +10859,7 @@ async function initGlobalConfig(start) {
9047
10859
  message: "Checkmarx One OAuth client ID:",
9048
10860
  default: ""
9049
10861
  });
9050
- checkmarxClientSecret = await password({
10862
+ checkmarxClientSecret = await password2({
9051
10863
  message: "Checkmarx One OAuth client secret:",
9052
10864
  validate: (v) => v.length > 0 || "Client secret cannot be blank"
9053
10865
  });
@@ -9092,7 +10904,7 @@ async function initGlobalConfig(start) {
9092
10904
  message: "User Code (from User Token):",
9093
10905
  default: ""
9094
10906
  });
9095
- sonatypeIqPasscode = await password({
10907
+ sonatypeIqPasscode = await password2({
9096
10908
  message: "Passcode (from User Token):"
9097
10909
  });
9098
10910
  if (sonatypeIqBaseUrl && sonatypeIqUserCode && sonatypeIqPasscode) {
@@ -9140,10 +10952,10 @@ async function initGlobalConfig(start) {
9140
10952
  message: "Username (email):",
9141
10953
  default: ""
9142
10954
  });
9143
- contrastApiKey = await password({
10955
+ contrastApiKey = await password2({
9144
10956
  message: "API key:"
9145
10957
  });
9146
- contrastServiceKey = await password({
10958
+ contrastServiceKey = await password2({
9147
10959
  message: "Service key:"
9148
10960
  });
9149
10961
  const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
@@ -9182,7 +10994,7 @@ async function initGlobalConfig(start) {
9182
10994
  message: "OpenShift API server URL (e.g. https://api.cluster.imagile.dev:6443):",
9183
10995
  default: ""
9184
10996
  });
9185
- openShiftToken = await password({
10997
+ openShiftToken = await password2({
9186
10998
  message: "Service account token:"
9187
10999
  });
9188
11000
  if (openShiftBaseUrl && openShiftToken) {
@@ -9218,7 +11030,7 @@ async function initGlobalConfig(start) {
9218
11030
  message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
9219
11031
  default: ""
9220
11032
  });
9221
- dynatraceApiToken = await password({
11033
+ dynatraceApiToken = await password2({
9222
11034
  message: "Environment API token (entities.read and problems.read):"
9223
11035
  });
9224
11036
  const configureTraces = await confirm({
@@ -9230,7 +11042,7 @@ async function initGlobalConfig(start) {
9230
11042
  message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
9231
11043
  default: ""
9232
11044
  });
9233
- dynatracePlatformToken = await password({
11045
+ dynatracePlatformToken = await password2({
9234
11046
  message: "Platform token with Grail span read permissions:"
9235
11047
  });
9236
11048
  }
@@ -9269,7 +11081,7 @@ async function initGlobalConfig(start) {
9269
11081
  message: "LogScale base URL (e.g. https://logscale.imagile.dev):",
9270
11082
  default: ""
9271
11083
  });
9272
- logscaleToken = await password({
11084
+ logscaleToken = await password2({
9273
11085
  message: "LogScale personal access token:"
9274
11086
  });
9275
11087
  if (logscaleBaseUrl && logscaleToken) {
@@ -9303,7 +11115,7 @@ async function initGlobalConfig(start) {
9303
11115
  message: "Split.IO Admin API base URL (e.g. https://api.split.io):",
9304
11116
  default: ""
9305
11117
  });
9306
- splitioAdminApiKey = await password({
11118
+ splitioAdminApiKey = await password2({
9307
11119
  message: "Split.IO Admin API key:"
9308
11120
  });
9309
11121
  if (splitioBaseUrl && splitioAdminApiKey) {
@@ -9333,7 +11145,7 @@ async function initGlobalConfig(start) {
9333
11145
  let figmaToken = "";
9334
11146
  if (useFigma) {
9335
11147
  process.stderr.write(" Generate a personal access token in Figma under Account Settings \u2192 Personal access tokens.\n");
9336
- figmaToken = await password({
11148
+ figmaToken = await password2({
9337
11149
  message: "Figma personal access token:"
9338
11150
  });
9339
11151
  if (figmaToken) {
@@ -9370,7 +11182,7 @@ async function initGlobalConfig(start) {
9370
11182
  message: "Alation base URL (e.g. https://alation.imagile.dev):",
9371
11183
  validate: (v) => v.trim().length > 0 || "Required"
9372
11184
  });
9373
- alationRefreshToken = await password({
11185
+ alationRefreshToken = await password2({
9374
11186
  message: "Alation refresh token:"
9375
11187
  });
9376
11188
  alationUserId = await input2({
@@ -9426,6 +11238,11 @@ async function initGlobalConfig(start) {
9426
11238
  const existingOpenshift = existingGlobal?.openshift;
9427
11239
  writeGlobalConfig({
9428
11240
  jenkinsInstances: existingInstances,
11241
+ // Not prompted for here, so carry them over: dropping gitAuth strands `git-auth disable`
11242
+ // (it restores from the recorded backups), and marketplaces/skillsTargets hold tokens and paths.
11243
+ ...existingGlobal?.marketplaces ? { marketplaces: existingGlobal.marketplaces } : {},
11244
+ ...existingGlobal?.skillsTargets ? { skillsTargets: existingGlobal.skillsTargets } : {},
11245
+ ...existingGlobal?.gitAuth ? { gitAuth: existingGlobal.gitAuth } : {},
9429
11246
  user: {
9430
11247
  email: userEmail || void 0,
9431
11248
  userId: userId || void 0
@@ -9628,7 +11445,7 @@ async function initRepoConfig(start) {
9628
11445
  process.exitCode = ExitCode.SUCCESS;
9629
11446
  return;
9630
11447
  }
9631
- if (fs4.existsSync(".pncli.json")) {
11448
+ if (fs7.existsSync(".pncli.json")) {
9632
11449
  const overwrite = await confirm({
9633
11450
  message: ".pncli.json already exists. Overwrite?",
9634
11451
  default: false
@@ -9662,14 +11479,172 @@ async function initRepoConfig(start) {
9662
11479
  }
9663
11480
 
9664
11481
  // src/services/doctor/commands.ts
9665
- import fs5 from "fs";
9666
- import path9 from "path";
11482
+ import fs9 from "fs";
11483
+ import path12 from "path";
11484
+
11485
+ // src/services/doctor/git-auth-checks.ts
11486
+ import fs8 from "fs";
11487
+ var TOKEN_EXPIRY_WARNING_DAYS = 14;
11488
+ function checkKeychain(rawGlobalConfig, backend = getKeychainBackend(), configPath) {
11489
+ const refs = findKeychainRefs(rawGlobalConfig);
11490
+ const plaintext = findPlaintextSecrets(rawGlobalConfig).map((s) => s.path.join("."));
11491
+ let unresolved = [];
11492
+ if (refs.length > 0) {
11493
+ resolveKeychainRefs(rawGlobalConfig, backend);
11494
+ unresolved = getUnresolvedKeychainRefs();
11495
+ }
11496
+ const available = refs.length > 0 ? backend.available() : null;
11497
+ const backupPath = configPath ? `${configPath}.pre-keychain.bak` : null;
11498
+ return {
11499
+ backend: backend.name,
11500
+ store: backend.store,
11501
+ available,
11502
+ references: refs.length,
11503
+ unresolved,
11504
+ plaintextSecrets: plaintext,
11505
+ plaintextBackup: backupPath && fs8.existsSync(backupPath) ? backupPath : null,
11506
+ hint: plaintext.length > 0 && backend.name !== "none" ? `${plaintext.length} secret(s) are stored in plaintext config. Move them into the ${backend.store} with: pncli config keychain migrate` : null
11507
+ };
11508
+ }
11509
+ async function checkGitAuth(rawGlobalConfig, cfg, offline, fetchFn = fetch, git = defaultGitRunner) {
11510
+ const reports = [];
11511
+ const inspected = /* @__PURE__ */ new Map();
11512
+ const byName = new Map(allMarketplaces(rawGlobalConfig).map((m) => [m.name ?? m.repoUrl, m]));
11513
+ for (const local of describeAllGitAuth(rawGlobalConfig, cfg, git)) {
11514
+ let access = null;
11515
+ let token = null;
11516
+ const m = local.marketplace ? byName.get(local.marketplace) : void 0;
11517
+ if (!offline && m?.repoUrl && !local.credentialError) {
11518
+ let auth = null;
11519
+ try {
11520
+ auth = resolveMarketplaceAuth(m, cfg);
11521
+ } catch {
11522
+ }
11523
+ access = lsRemote(m.repoUrl, auth, git, { keepHelpers: !auth });
11524
+ if (auth && auth.provider === "github") {
11525
+ const apiBase = apiBaseForHost(local.host, cfg.github?.baseUrl);
11526
+ const key = `${apiBase}\0${auth.password}`;
11527
+ if (!inspected.has(key)) inspected.set(key, await inspectGitHubToken(apiBase, auth.password, fetchFn));
11528
+ token = inspected.get(key);
11529
+ }
11530
+ }
11531
+ reports.push({ ...local, access, token });
11532
+ }
11533
+ return reports;
11534
+ }
11535
+ function buildKeychainProblems(report) {
11536
+ const problems = report.unresolved.map((u) => ({
11537
+ area: "keychain",
11538
+ message: `${u.path} points at keychain entry "${u.account}", which could not be read (${u.reason})`,
11539
+ fix: `Store it again with: pncli config keychain set ${u.path} (or move it back to plaintext: pncli config keychain migrate --to config ${u.path})`
11540
+ }));
11541
+ if (report.plaintextBackup) {
11542
+ problems.push({
11543
+ area: "keychain",
11544
+ message: `A plaintext copy of your pre-migration config (every secret in it) is still on disk: ${report.plaintextBackup}`,
11545
+ fix: "Once `pncli config check` passes, delete that file."
11546
+ });
11547
+ }
11548
+ return problems;
11549
+ }
11550
+ function rotateFix(source) {
11551
+ if (source?.startsWith("marketplace:")) {
11552
+ const name = source.slice("marketplace:".length);
11553
+ return `Regenerate it, then: pncli skills marketplace update ${name} --token <new-token> [--keychain]`;
11554
+ }
11555
+ const key = source === "bitbucket.pat" || source === "ado.pat" ? source : "github.token";
11556
+ return `Regenerate it, then: pncli config keychain set ${key} (or: pncli config set ${key} <new-token>)`;
11557
+ }
11558
+ function subject(r) {
11559
+ return r.marketplace ? `marketplace "${r.marketplace}"` : r.scope;
11560
+ }
11561
+ function enableFix(r) {
11562
+ return r.marketplace ? `pncli skills git-auth enable --marketplace ${r.marketplace}` : `pncli skills git-auth enable --host ${r.host}`;
11563
+ }
11564
+ function accessFix(r) {
11565
+ if (!r.pncliTokenSource) return `If the repo is private, give it a token: pncli skills marketplace update ${r.marketplace} --token <token>`;
11566
+ if (r.provider === "bitbucket" && r.username === "x-token-auth") {
11567
+ return `Bitbucket Data Center personal access tokens are usually sent with your username: pncli skills marketplace update ${r.marketplace} --username <your-bitbucket-username>. If that is not it, the token itself: ${rotateFix(r.pncliTokenSource)}`;
11568
+ }
11569
+ if (r.provider === "ado") return `Check the PAT has Code (Read) for this collection and has not expired. ${rotateFix(r.pncliTokenSource)}`;
11570
+ return `Check the token can read this repo and has not expired. ${rotateFix(r.pncliTokenSource)}`;
11571
+ }
11572
+ function buildGitAuthProblems(reports) {
11573
+ const problems = [];
11574
+ for (const r of reports) {
11575
+ const t = r.token;
11576
+ if (r.credentialError) {
11577
+ problems.push({ area: "git-auth", message: r.credentialError, fix: `Re-store it: pncli config keychain set marketplaces.${r.marketplace}.token` });
11578
+ continue;
11579
+ }
11580
+ if (r.access && !r.access.ok) {
11581
+ problems.push({
11582
+ area: "git-auth",
11583
+ message: `git cannot read ${subject(r)} (${r.repoUrl}) with the credential pncli has (${r.pncliTokenSource ?? "none"}): ${r.access.error}`,
11584
+ fix: accessFix(r)
11585
+ });
11586
+ }
11587
+ if (r.clonesWithEmbeddedToken.length > 0) {
11588
+ problems.push({
11589
+ area: "git-auth",
11590
+ message: `The clone of ${subject(r)} stores a token in plaintext in .git/config (${r.clonesWithEmbeddedToken.join(", ")})`,
11591
+ fix: `Run: ${enableFix(r)} (it rewrites the remote without the token; the next sync does too)`
11592
+ });
11593
+ }
11594
+ if (r.pncliTokenSource && r.mode === "none") {
11595
+ problems.push({
11596
+ area: "git-auth",
11597
+ message: `git itself has no credential for ${subject(r)}: agent hosts that clone marketplaces themselves (e.g. Claude Code's /plugin marketplace add) will fail if it is private`,
11598
+ fix: `Run: ${enableFix(r)}`
11599
+ });
11600
+ }
11601
+ if (r.mode === "keychain" && r.matchesPncliToken === false) {
11602
+ problems.push({
11603
+ area: "git-auth",
11604
+ message: `The token git has in the OS keychain for ${subject(r)} differs from the one pncli uses \u2014 usually a rotation that was not re-stored`,
11605
+ fix: `Re-store it: ${enableFix(r)} --mode keychain (or switch to helper mode, which never goes stale)`
11606
+ });
11607
+ }
11608
+ if (!t) continue;
11609
+ const who = `The GitHub token for ${subject(r)}`;
11610
+ if (t.ssoAuthorizationRequired) {
11611
+ problems.push({
11612
+ area: "git-auth",
11613
+ message: `${who} is not authorized for the organization's SAML SSO`,
11614
+ fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize for the org"
11615
+ });
11616
+ } else if (t.status === 401) {
11617
+ problems.push({
11618
+ area: "git-auth",
11619
+ message: `${who} was rejected (HTTP 401) \u2014 expired or revoked (source: ${r.pncliTokenSource})`,
11620
+ fix: rotateFix(r.pncliTokenSource)
11621
+ });
11622
+ }
11623
+ if (t.missingRepoScope) {
11624
+ problems.push({
11625
+ area: "git-auth",
11626
+ message: `${who} is a classic token without the "repo" scope (has: ${t.scopes?.join(", ") || "none"}), so a private marketplace will not clone`,
11627
+ fix: 'Edit the token in GitHub \u2192 Settings \u2192 Developer settings \u2192 Tokens (classic) and tick "repo"'
11628
+ });
11629
+ }
11630
+ if (t.expiresInDays !== null && t.expiresInDays <= TOKEN_EXPIRY_WARNING_DAYS) {
11631
+ problems.push({
11632
+ area: "git-auth",
11633
+ message: t.expiresInDays < 0 ? `${who} expired on ${t.expiresAt}` : `${who} expires in ${t.expiresInDays} day(s) (${t.expiresAt})`,
11634
+ fix: `${rotateFix(r.pncliTokenSource)} \u2014 helper mode picks up the new token automatically`
11635
+ });
11636
+ }
11637
+ }
11638
+ return problems;
11639
+ }
11640
+
11641
+ // src/services/doctor/commands.ts
9667
11642
  function checkConfigFile(filePath) {
9668
- if (!fs5.existsSync(filePath)) {
11643
+ if (!fs9.existsSync(filePath)) {
9669
11644
  return { path: filePath, exists: false, valid: false, message: "not present" };
9670
11645
  }
9671
11646
  try {
9672
- JSON.parse(fs5.readFileSync(filePath, "utf8"));
11647
+ JSON.parse(fs9.readFileSync(filePath, "utf8"));
9673
11648
  return { path: filePath, exists: true, valid: true, message: "ok" };
9674
11649
  } catch (err) {
9675
11650
  return {
@@ -9750,13 +11725,13 @@ function buildProblems(globalFile, repoFile, credentials, skillLocations, market
9750
11725
  return problems;
9751
11726
  }
9752
11727
  function registerDoctorCommands(program2) {
9753
- program2.command("doctor").description("Diagnose pncli setup: config files, credentials, agent skills, and skills marketplaces").option("--offline", "Skip credential checks (no network calls)").action(async (cmdOpts) => {
11728
+ program2.command("doctor").description("Diagnose pncli setup: config files, credentials, keychain references, git auth for marketplace hosts, agent skills, and marketplaces").option("--offline", "Skip credential checks (no network calls)").action(async (cmdOpts) => {
9754
11729
  const start = Date.now();
9755
11730
  try {
9756
11731
  const opts = program2.optsWithGlobals();
9757
11732
  const globalFile = checkConfigFile(opts.config ?? getGlobalConfigPath());
9758
11733
  const repoRoot = findGitRoot() ?? process.cwd();
9759
- const repoFile = checkConfigFile(path9.join(repoRoot, ".pncli.json"));
11734
+ const repoFile = checkConfigFile(path12.join(repoRoot, ".pncli.json"));
9760
11735
  let credentials = null;
9761
11736
  let credentialsSkipped = cmdOpts.offline ? "--offline" : null;
9762
11737
  if (!cmdOpts.offline) {
@@ -9781,9 +11756,33 @@ function registerDoctorCommands(program2) {
9781
11756
  name: m.name ?? m.repoUrl ?? "(unnamed)",
9782
11757
  repoUrl: m.repoUrl ?? null,
9783
11758
  localPath: m.localPath ?? null,
9784
- cloneExists: !!m.localPath && fs5.existsSync(m.localPath)
11759
+ cloneExists: !!m.localPath && fs9.existsSync(m.localPath)
9785
11760
  }));
9786
- const problems = buildProblems(globalFile, repoFile, credentials, skillLocations, marketplaces);
11761
+ const keychain = (() => {
11762
+ try {
11763
+ return checkKeychain(globalConfig, void 0, opts.config ?? getGlobalConfigPath());
11764
+ } catch (err) {
11765
+ return { error: err instanceof Error ? err.message : String(err) };
11766
+ }
11767
+ })();
11768
+ let gitAuth = [];
11769
+ try {
11770
+ const providerCfg = (() => {
11771
+ try {
11772
+ return loadConfig({ configPath: opts.config });
11773
+ } catch {
11774
+ return {};
11775
+ }
11776
+ })();
11777
+ gitAuth = await checkGitAuth(globalConfig, providerCfg, !!cmdOpts.offline);
11778
+ } catch (err) {
11779
+ gitAuth = { error: err instanceof Error ? err.message : String(err) };
11780
+ }
11781
+ const problems = [
11782
+ ...buildProblems(globalFile, repoFile, credentials, skillLocations, marketplaces),
11783
+ ..."error" in keychain ? [] : buildKeychainProblems(keychain),
11784
+ ...Array.isArray(gitAuth) ? buildGitAuthProblems(gitAuth) : []
11785
+ ];
9787
11786
  if (credentials) {
9788
11787
  const statuses = Object.values(credentials).map((r) => r.status);
9789
11788
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
@@ -9806,7 +11805,10 @@ function registerDoctorCommands(program2) {
9806
11805
  registered: marketplaces,
9807
11806
  hint: marketplaces.length === 0 ? "No org marketplaces registered. Add one with: pncli skills marketplace add <git-url> --all-agents" : "Refresh with: pncli skills marketplace sync --marketplace all --all-agents"
9808
11807
  },
9809
- problems
11808
+ keychain,
11809
+ gitAuth,
11810
+ problems,
11811
+ hint: problems.length === 0 ? null : "New to skills and marketplaces? pncli skills guide troubleshooting"
9810
11812
  }, "doctor", "check", start);
9811
11813
  } catch (err) {
9812
11814
  fail(err, "doctor", "check", start);
@@ -10195,8 +12197,8 @@ function registerAdoProjectCommands(ado) {
10195
12197
  }
10196
12198
 
10197
12199
  // src/services/ado/commands/work.ts
10198
- import fs6 from "fs";
10199
- import path10 from "path";
12200
+ import fs10 from "fs";
12201
+ import path13 from "path";
10200
12202
 
10201
12203
  // src/services/ado/input-schema.ts
10202
12204
  var ADO_WORK_INPUT_FILE_SCHEMA = {
@@ -10437,11 +12439,11 @@ function registerAdoWorkCommands(ado) {
10437
12439
  const attachments = await workClient.listAttachments(collection, parseInt(opts.id, 10));
10438
12440
  const attachment = attachments.find((a) => a.id === opts.attachmentId);
10439
12441
  if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
10440
- const outDir = opts.dir ?? path10.join(process.cwd(), ".pncli");
10441
- fs6.mkdirSync(outDir, { recursive: true });
10442
- const outPath = path10.join(outDir, path10.basename(attachment.name));
12442
+ const outDir = opts.dir ?? path13.join(process.cwd(), ".pncli");
12443
+ fs10.mkdirSync(outDir, { recursive: true });
12444
+ const outPath = path13.join(outDir, path13.basename(attachment.name));
10443
12445
  const buffer = await workClient.downloadAttachment(attachment.url);
10444
- fs6.writeFileSync(outPath, buffer);
12446
+ fs10.writeFileSync(outPath, buffer);
10445
12447
  success({ saved: outPath, name: attachment.name, size: buffer.length }, "ado", "work-download-attachment", start);
10446
12448
  } catch (err) {
10447
12449
  fail(err, "ado", "work-download-attachment", start);
@@ -10475,10 +12477,10 @@ function registerAdoWorkCommands(ado) {
10475
12477
  const types = await discoverTypes(workClient, collection, project);
10476
12478
  if (opts.save) {
10477
12479
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
10478
- const fs9 = await import("fs");
10479
- const existing = JSON.parse(fs9.readFileSync(globalConfigPath, "utf8") || "{}");
12480
+ const fs13 = await import("fs");
12481
+ const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
10480
12482
  existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
10481
- fs9.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
12483
+ fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10482
12484
  warn(`Saved ${types.length} types to ${globalConfigPath}`);
10483
12485
  }
10484
12486
  success(types, "ado", "work-types", start);
@@ -10507,11 +12509,11 @@ function registerAdoWorkCommands(ado) {
10507
12509
  ) : allFields;
10508
12510
  if (opts.save) {
10509
12511
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
10510
- const fs9 = await import("fs");
10511
- const existing = JSON.parse(fs9.readFileSync(globalConfigPath, "utf8") || "{}");
12512
+ const fs13 = await import("fs");
12513
+ const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
10512
12514
  const aliases = buildDefaultAliases(allFields);
10513
12515
  existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
10514
- fs9.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
12516
+ fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10515
12517
  warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
10516
12518
  }
10517
12519
  success(fields, "ado", "work-fields", start);
@@ -10808,7 +12810,7 @@ function registerAdoRepoCommands(ado) {
10808
12810
  const start = Date.now();
10809
12811
  try {
10810
12812
  const globalOpts = ado.optsWithGlobals();
10811
- const { loadConfig: lc } = await import("./config-Y6IE6GY7.js");
12813
+ const { loadConfig: lc } = await import("./config-OH2O5E3P.js");
10812
12814
  const config = lc({ configPath: globalOpts.config });
10813
12815
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
10814
12816
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
@@ -11109,6 +13111,10 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
11109
13111
  }
11110
13112
  throw new PncliError(`Timed out waiting for build #${buildNumber} to complete after ${timeoutMs / 1e3}s`, 1);
11111
13113
  }
13114
+ function rawJenkinsInstances(configPath) {
13115
+ const stored = loadJsonFile(getGlobalConfigPath(configPath))?.jenkinsInstances;
13116
+ return Array.isArray(stored) ? stored : [];
13117
+ }
11112
13118
  function registerJenkinsCommands(program2) {
11113
13119
  const jenkins = program2.command("jenkins").description("Jenkins operations").option("--instance <name>", "Named Jenkins instance from global config (overrides default jenkins config)");
11114
13120
  const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
@@ -11132,15 +13138,15 @@ function registerJenkinsCommands(program2) {
11132
13138
  try {
11133
13139
  const opts = jenkins.optsWithGlobals();
11134
13140
  const configPath = opts.config;
11135
- const existing = loadConfig({ configPath }).jenkinsInstances;
13141
+ const existing = rawJenkinsInstances(configPath);
11136
13142
  const idx = existing.findIndex((i) => i.name === o.name);
11137
13143
  if (idx >= 0 && !o.force) {
11138
13144
  throw new PncliError(`Jenkins instance "${o.name}" already exists. Pass --force to overwrite it, or run: pncli jenkins instance remove --name ${o.name}`, 1);
11139
13145
  }
11140
13146
  let apiToken = o.apiToken;
11141
13147
  if (!apiToken && process.stdin.isTTY) {
11142
- const { default: password2 } = await import("@inquirer/password");
11143
- apiToken = await password2({ message: `API token for "${o.name}" (leave blank to skip):` });
13148
+ const { default: password3 } = await import("@inquirer/password");
13149
+ apiToken = await password3({ message: `API token for "${o.name}" (leave blank to skip):` });
11144
13150
  }
11145
13151
  const entry = {
11146
13152
  name: o.name,
@@ -11160,7 +13166,7 @@ function registerJenkinsCommands(program2) {
11160
13166
  try {
11161
13167
  const opts = jenkins.optsWithGlobals();
11162
13168
  const configPath = opts.config;
11163
- const existing = loadConfig({ configPath }).jenkinsInstances;
13169
+ const existing = rawJenkinsInstances(configPath);
11164
13170
  if (!existing.some((i) => i.name === o.name)) {
11165
13171
  throw new PncliError(`Jenkins instance "${o.name}" not found. Run: pncli jenkins instance list`, 1);
11166
13172
  }
@@ -11282,18 +13288,18 @@ var ArtifactoryClient = class {
11282
13288
  }
11283
13289
  });
11284
13290
  }
11285
- async getStorageInfo(repo, path11) {
11286
- const cleanPath = path11.startsWith("/") ? path11.slice(1) : path11;
13291
+ async getStorageInfo(repo, path14) {
13292
+ const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
11287
13293
  return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`);
11288
13294
  }
11289
- async getProperties(repo, path11) {
11290
- const cleanPath = path11.startsWith("/") ? path11.slice(1) : path11;
13295
+ async getProperties(repo, path14) {
13296
+ const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
11291
13297
  return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
11292
13298
  params: { properties: "" }
11293
13299
  });
11294
13300
  }
11295
- async setProperties(repo, path11, properties, opts = {}) {
11296
- const cleanPath = path11.startsWith("/") ? path11.slice(1) : path11;
13301
+ async setProperties(repo, path14, properties, opts = {}) {
13302
+ const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
11297
13303
  const propString = Object.entries(properties).map(([k, v]) => `${k}=${v}`).join(";");
11298
13304
  await this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
11299
13305
  method: "PUT",
@@ -11375,27 +13381,27 @@ function registerArtifactoryCommands(program2) {
11375
13381
  fail(err, "artifactory", "repos", start);
11376
13382
  }
11377
13383
  });
11378
- art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path11) => {
13384
+ art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path14) => {
11379
13385
  const start = Date.now();
11380
13386
  try {
11381
13387
  const client = getClient8(program2);
11382
- const data = await client.getStorageInfo(repo, path11);
13388
+ const data = await client.getStorageInfo(repo, path14);
11383
13389
  success(data, "artifactory", "artifact-info", start);
11384
13390
  } catch (err) {
11385
13391
  fail(err, "artifactory", "artifact-info", start);
11386
13392
  }
11387
13393
  });
11388
- art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path11) => {
13394
+ art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path14) => {
11389
13395
  const start = Date.now();
11390
13396
  try {
11391
13397
  const client = getClient8(program2);
11392
- const data = await client.getProperties(repo, path11);
13398
+ const data = await client.getProperties(repo, path14);
11393
13399
  success(data, "artifactory", "properties-get", start);
11394
13400
  } catch (err) {
11395
13401
  fail(err, "artifactory", "properties-get", start);
11396
13402
  }
11397
13403
  });
11398
- art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo, path11, keyvals, opts) => {
13404
+ art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo, path14, keyvals, opts) => {
11399
13405
  const start = Date.now();
11400
13406
  try {
11401
13407
  if (!keyvals.length) throw new PncliError("Provide at least one key=value pair");
@@ -11406,8 +13412,8 @@ function registerArtifactoryCommands(program2) {
11406
13412
  properties[kv.slice(0, eq)] = kv.slice(eq + 1);
11407
13413
  }
11408
13414
  const client = getClient8(program2);
11409
- await client.setProperties(repo, path11, properties, { recursive: opts.recursive });
11410
- success({ repo, path: path11, properties }, "artifactory", "properties-set", start);
13415
+ await client.setProperties(repo, path14, properties, { recursive: opts.recursive });
13416
+ success({ repo, path: path14, properties }, "artifactory", "properties-set", start);
11411
13417
  } catch (err) {
11412
13418
  fail(err, "artifactory", "properties-set", start);
11413
13419
  }
@@ -11725,8 +13731,8 @@ function registerSonatypeIqCommands(program2) {
11725
13731
  const startTime = Date.now();
11726
13732
  try {
11727
13733
  const http = getHttpClient(program2);
11728
- const path11 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
11729
- const data = await http.sonatypeiq(path11);
13734
+ const path14 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
13735
+ const data = await http.sonatypeiq(path14);
11730
13736
  success(data, "sonatypeiq", "policies list", startTime);
11731
13737
  } catch (err) {
11732
13738
  fail(err, "sonatypeiq", "policies list", startTime);
@@ -12421,7 +14427,7 @@ function registerLogscaleCommands(program2) {
12421
14427
  }
12422
14428
 
12423
14429
  // src/services/splitio/commands.ts
12424
- import fs7 from "fs";
14430
+ import fs11 from "fs";
12425
14431
  import confirm2 from "@inquirer/confirm";
12426
14432
  function getHttp4(program2) {
12427
14433
  const opts = program2.optsWithGlobals();
@@ -12433,9 +14439,9 @@ function getHttp4(program2) {
12433
14439
  function loadInputFile(filePath) {
12434
14440
  let raw;
12435
14441
  if (filePath === "-") {
12436
- raw = fs7.readFileSync(process.stdin.fd, "utf8");
14442
+ raw = fs11.readFileSync(process.stdin.fd, "utf8");
12437
14443
  } else {
12438
- raw = fs7.readFileSync(filePath, "utf8");
14444
+ raw = fs11.readFileSync(filePath, "utf8");
12439
14445
  }
12440
14446
  try {
12441
14447
  return JSON.parse(raw);
@@ -12967,7 +14973,7 @@ function registerFigmaCommands(program2) {
12967
14973
  }
12968
14974
 
12969
14975
  // src/services/alation/commands.ts
12970
- import fs8 from "fs";
14976
+ import fs12 from "fs";
12971
14977
  var MAX_PAGE = 1e3;
12972
14978
  function parseIntOption(value, flag) {
12973
14979
  if (value === void 0) return void 0;
@@ -13080,8 +15086,8 @@ function getHttp6(program2) {
13080
15086
  Boolean(opts.dryRun)
13081
15087
  );
13082
15088
  }
13083
- async function getOne(http, path11, id, what) {
13084
- const rows = await http.alation(path11, { params: { id, limit: 1 } });
15089
+ async function getOne(http, path14, id, what) {
15090
+ const rows = await http.alation(path14, { params: { id, limit: 1 } });
13085
15091
  const row = Array.isArray(rows) ? rows.find((r) => r.id === id) ?? rows[0] : void 0;
13086
15092
  if (!row) throw new PncliError(`${what} ${id} not found`, 404);
13087
15093
  return row;
@@ -13131,7 +15137,7 @@ function registerAlationCommands(program2) {
13131
15137
  const opts = program2.optsWithGlobals();
13132
15138
  const config = loadConfig({ configPath: opts.config });
13133
15139
  if (opts.dryRun) {
13134
- fs8.writeSync(process.stderr.fd, `DRY RUN: POST ${config.alation.baseUrl ?? "<alation.baseUrl>"}/integration/v1/createAPIAccessToken/ (token exchange skipped)
15140
+ fs12.writeSync(process.stderr.fd, `DRY RUN: POST ${config.alation.baseUrl ?? "<alation.baseUrl>"}/integration/v1/createAPIAccessToken/ (token exchange skipped)
13135
15141
  `);
13136
15142
  process.exitCode = ExitCode.SUCCESS;
13137
15143
  throw new PncliError("dry-run", 0);
@@ -13438,8 +15444,8 @@ Services:
13438
15444
  splitio Split.IO (feature flags, Change Requests)
13439
15445
  figma Figma (files, comments, version history)
13440
15446
  alation Alation (data sources, schemas, tables, columns, search, Document Hubs)
13441
- config Manage pncli configuration
13442
- doctor Diagnose pncli setup (config files, credentials, agent skills, marketplaces)
15447
+ config Manage pncli configuration (and OS keychain storage for credentials)
15448
+ doctor Diagnose pncli setup (config, credentials, keychain, git auth, agent skills, marketplaces)
13443
15449
  skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)
13444
15450
  jwt JWT token utilities (decode header and payload)
13445
15451
 
@@ -13448,7 +15454,12 @@ Agent skills and org plugins:
13448
15454
  pncli skills marketplace add <git-url> --all-agents Org plugins (skills + AGENTS.md/CLAUDE.md) from a git marketplace
13449
15455
  pncli skills marketplace sync Pull and refresh everything installed from marketplaces
13450
15456
  pncli skills marketplace manage Interactive: toggle plugins, add/remove marketplaces
15457
+ pncli skills git-auth enable Let git and agent hosts authenticate to private marketplace hosts
15458
+ pncli skills guide How skills management fits together
13451
15459
  pncli skills --help Every install location and command
15460
+
15461
+ Credentials:
15462
+ pncli config keychain migrate Move plaintext tokens into the OS keychain (--to config reverses it)
13452
15463
  `);
13453
15464
  program.parseAsync(process.argv).catch((err) => {
13454
15465
  if (process.exitCode !== void 0) return;