@kolatts/pncli 5.2.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]);
@@ -587,30 +605,30 @@ var BitbucketClient = class {
587
605
  );
588
606
  }
589
607
  async createPR(opts) {
590
- return this.http.bitbucket(
591
- `${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests`,
592
- {
593
- method: "POST",
594
- body: {
595
- title: opts.title,
596
- description: opts.description,
597
- fromRef: {
598
- id: `refs/heads/${opts.source}`,
599
- repository: {
600
- slug: opts.repo,
601
- project: { key: opts.project }
602
- }
603
- },
604
- toRef: {
605
- id: `refs/heads/${opts.target}`,
606
- repository: {
607
- slug: opts.repo,
608
- project: { key: opts.project }
609
- }
610
- },
611
- reviewers: (opts.reviewers ?? []).map((name) => ({ user: { name } }))
608
+ const body = {
609
+ title: opts.title,
610
+ description: opts.description,
611
+ fromRef: {
612
+ id: `refs/heads/${opts.source}`,
613
+ repository: {
614
+ slug: opts.repo,
615
+ project: { key: opts.project }
616
+ }
617
+ },
618
+ toRef: {
619
+ id: `refs/heads/${opts.target}`,
620
+ repository: {
621
+ slug: opts.repo,
622
+ project: { key: opts.project }
612
623
  }
613
624
  }
625
+ };
626
+ if (opts.reviewers && opts.reviewers.length > 0) {
627
+ body.reviewers = opts.reviewers.map((name) => ({ user: { name } }));
628
+ }
629
+ return this.http.bitbucket(
630
+ `${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests`,
631
+ { method: "POST", body }
614
632
  );
615
633
  }
616
634
  async updatePR(opts) {
@@ -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
  }
@@ -1861,7 +2956,7 @@ function registerBitbucketCommands(program2) {
1861
2956
  const { client, project, repo } = getClient2(bb, { project: opts.project, repo: opts.repo });
1862
2957
  const config = loadConfig({ configPath: bb.optsWithGlobals().config });
1863
2958
  const target = opts.target ?? config.defaults.bitbucket?.targetBranch ?? "main";
1864
- const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : [];
2959
+ const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : void 0;
1865
2960
  const data = await client.createPR({ project, repo, title: opts.title, source: opts.source, target, description: opts.description, reviewers });
1866
2961
  success(data, "bitbucket", "create-pr", start);
1867
2962
  } catch (err) {
@@ -1885,7 +2980,7 @@ function registerBitbucketCommands(program2) {
1885
2980
  throw new PncliError("--repos must contain at least one repository slug", 1);
1886
2981
  }
1887
2982
  const title = opts.title ?? `Promote ${opts.from} to ${opts.to}`;
1888
- const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : [];
2983
+ const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : void 0;
1889
2984
  const results = [];
1890
2985
  let created = 0;
1891
2986
  let nothingToPromote = 0;
@@ -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);
@@ -6487,35 +7582,295 @@ async function runCredentialChecks(cfg, http) {
6487
7582
  results.figma = { status: "error", message: "baseUrl not configured" };
6488
7583
  } else {
6489
7584
  try {
6490
- await http.figma("/v1/me", { timeoutMs: 1e4 });
6491
- results.figma = { status: "valid", message: "ok" };
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);
6492
7787
  } catch (err) {
6493
- results.figma = categorize(err);
7788
+ fail(err, "config", "keychain-set", start);
6494
7789
  }
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 {
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();
6503
7793
  try {
6504
- await validateAlationAccessToken(cfg);
6505
- results.alation = { 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);
6506
7816
  } catch (err) {
6507
- results.alation = categorize(err);
7817
+ fail(err, "config", "keychain-remove", start);
6508
7818
  }
6509
- }
6510
- return results;
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();
7822
+ try {
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);
7862
+ } catch (err) {
7863
+ fail(err, "config", "keychain-migrate", start);
7864
+ }
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(", ")}`);
@@ -7404,15 +9086,18 @@ function installAllPlugins(m, resolvedPath, targets) {
7404
9086
  }
7405
9087
  return targets.map((t) => installMarketplaceToTarget(m, resolvedPath, pluginChoices, "all", t, { updated: true, force: true, installedOnly: false }));
7406
9088
  }
9089
+ function isBareMarketplaceSync(plugin, opts) {
9090
+ return plugin === void 0 && !opts.marketplace && !opts.force;
9091
+ }
7407
9092
  function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
7408
9093
  const marketplaceName = marketplaceLabel(m);
7409
9094
  try {
7410
9095
  const marketplacePath = m.localPath;
7411
- if (!marketplacePath || !fs3.existsSync(marketplacePath)) {
9096
+ if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
7412
9097
  warn(`Marketplace "${marketplaceName}" local path not found \u2014 skipping.`);
7413
9098
  return { marketplace: marketplaceName, skipped: true, message: "Local path not found." };
7414
9099
  }
7415
- const { updated } = pullMarketplace(marketplacePath, m.repoUrl, m.token, marketplaceName);
9100
+ const { updated } = pullMarketplace(marketplacePath, m, marketplaceName);
7416
9101
  const pluginChoices = resolvePluginChoices(marketplacePath);
7417
9102
  if (pluginChoices.length === 0) {
7418
9103
  warn(`No plugins found in marketplace "${marketplaceName}" \u2014 skipping.`);
@@ -7431,19 +9116,40 @@ function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
7431
9116
  return { marketplace: marketplaceName, skipped: true, error: message };
7432
9117
  }
7433
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
+ }
7434
9133
  function performMarketplaceAdd(url, localPath, opts) {
7435
- const resolvedPath = path8.resolve(localPath ?? defaultMarketplacePath(url));
9134
+ const resolvedPath = path11.resolve(localPath ?? defaultMarketplacePath(url));
7436
9135
  const marketplaceName = opts.name ?? repoNameFromUrl(url);
7437
- cloneOrReuseMarketplace(url, resolvedPath, opts, marketplaceName);
9136
+ const provider = parseProviderOption(opts.provider);
9137
+ cloneOrReuseMarketplace(url, resolvedPath, { ...opts, provider }, marketplaceName);
7438
9138
  const configPath = getGlobalConfigPath();
7439
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);
7440
9143
  const entry = {
7441
9144
  name: marketplaceName,
7442
9145
  repoUrl: url,
7443
9146
  localPath: resolvedPath,
7444
- ...opts.token ? { token: opts.token } : {}
9147
+ ...opts.token ? { token: storeInKeychain ? keychainRef(account) : opts.token } : {},
9148
+ ...opts.username ? { username: opts.username } : {},
9149
+ ...provider ? { provider } : {}
7445
9150
  };
7446
9151
  upsertMarketplace(all, entry);
9152
+ if (storeInKeychain) getKeychainBackend().set(account, opts.token);
7447
9153
  saveMarketplaces(configPath, existing, all);
7448
9154
  const targets = resolveInstallTargets(opts);
7449
9155
  const targetResults = installAllPlugins(entry, resolvedPath, targets);
@@ -7454,6 +9160,8 @@ function performMarketplaceAdd(url, localPath, opts) {
7454
9160
  localPath: resolvedPath,
7455
9161
  branch: opts.branch ?? null,
7456
9162
  tokenConfigured: !!opts.token,
9163
+ tokenStorage: !opts.token ? null : opts.keychain ? "keychain" : "config",
9164
+ provider: detectProvider(url, safeProviderConfig(), provider),
7457
9165
  // One target keeps the flat shape earlier versions emitted; several nest under `targets`.
7458
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) },
7459
9167
  ...instructions ? { instructions } : {}
@@ -7468,12 +9176,22 @@ function performMarketplaceRemove(name) {
7468
9176
  }
7469
9177
  const removed = all.splice(idx, 1)[0];
7470
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
+ }
7471
9188
  return {
7472
9189
  removed: {
7473
9190
  name: marketplaceLabel(removed),
7474
9191
  repoUrl: removed.repoUrl,
7475
9192
  localPath: removed.localPath
7476
9193
  },
9194
+ keychainEntryDeleted,
7477
9195
  remaining: all.length
7478
9196
  };
7479
9197
  }
@@ -7490,8 +9208,8 @@ function listBundledSkillDirs() {
7490
9208
  let skillDirs = [];
7491
9209
  if (bundledDir !== "") {
7492
9210
  try {
7493
- skillDirs = fs3.readdirSync(bundledDir).filter(
7494
- (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"))
7495
9213
  );
7496
9214
  } catch {
7497
9215
  }
@@ -7502,30 +9220,30 @@ function listBundledSkillDirs() {
7502
9220
  return skillDirs;
7503
9221
  }
7504
9222
  function installBundledSkillsTo(targetDir, skillDirs) {
7505
- const resolvedTarget = path8.resolve(targetDir);
9223
+ const resolvedTarget = path11.resolve(targetDir);
7506
9224
  const bundledDir = getBundledSkillsDir();
7507
9225
  const installed = [];
7508
9226
  const failed = [];
7509
9227
  for (const skillName of skillDirs) {
7510
- const existingDir = path8.resolve(targetDir, skillName);
7511
- if (!existingDir.startsWith(resolvedTarget + path8.sep)) continue;
7512
- if (fs3.existsSync(existingDir)) {
7513
- 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 });
7514
9232
  }
7515
9233
  }
7516
9234
  log(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
7517
9235
  for (const skillName of skillDirs) {
7518
- const skillDir = path8.resolve(targetDir, skillName);
7519
- if (!skillDir.startsWith(resolvedTarget + path8.sep)) {
9236
+ const skillDir = path11.resolve(targetDir, skillName);
9237
+ if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
7520
9238
  failed.push(skillName);
7521
9239
  continue;
7522
9240
  }
7523
9241
  try {
7524
- const mdFiles = fs3.readdirSync(path8.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
7525
- 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 });
7526
9244
  for (const mdFile of mdFiles) {
7527
- const content = fs3.readFileSync(path8.join(bundledDir, skillName, mdFile), "utf8");
7528
- 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");
7529
9247
  }
7530
9248
  installed.push(skillName);
7531
9249
  } catch {
@@ -7560,6 +9278,12 @@ Org plugins (skills and AGENTS.md/CLAUDE.md from a git-hosted marketplace):
7560
9278
 
7561
9279
  Check what is installed:
7562
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>)
7563
9287
  `);
7564
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) => {
7565
9289
  const start = Date.now();
@@ -7584,7 +9308,7 @@ Check what is installed:
7584
9308
  return;
7585
9309
  }
7586
9310
  const agentName = resolveAgentName(opts);
7587
- 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);
7588
9312
  const { installed, failed } = installBundledSkillsTo(targetDir, skillDirs);
7589
9313
  let tracked = false;
7590
9314
  if (opts.target && installed.length > 0) {
@@ -7613,12 +9337,12 @@ Check what is installed:
7613
9337
  try {
7614
9338
  let targetDir;
7615
9339
  if (opts.target) {
7616
- targetDir = path8.resolve(opts.target);
9340
+ targetDir = path11.resolve(opts.target);
7617
9341
  } else {
7618
9342
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
7619
9343
  targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
7620
9344
  }
7621
- if (!fs3.existsSync(targetDir)) {
9345
+ if (!fs6.existsSync(targetDir)) {
7622
9346
  success({
7623
9347
  skills: [],
7624
9348
  total: 0,
@@ -7629,13 +9353,13 @@ Check what is installed:
7629
9353
  return;
7630
9354
  }
7631
9355
  const meta = readInstalledMeta(targetDir);
7632
- const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
9356
+ const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
7633
9357
  if (name.startsWith(".")) return false;
7634
- const skillPath = path8.join(targetDir, name, "SKILL.md");
7635
- return fs3.existsSync(skillPath);
9358
+ const skillPath = path11.join(targetDir, name, "SKILL.md");
9359
+ return fs6.existsSync(skillPath);
7636
9360
  });
7637
9361
  const skillsList = skillDirs.map((name) => {
7638
- const content = fs3.readFileSync(path8.join(targetDir, name, "SKILL.md"), "utf8");
9362
+ const content = fs6.readFileSync(path11.join(targetDir, name, "SKILL.md"), "utf8");
7639
9363
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7640
9364
  const data = {};
7641
9365
  const metadata = {};
@@ -7688,24 +9412,24 @@ Check what is installed:
7688
9412
  try {
7689
9413
  let targetDir;
7690
9414
  if (opts.target) {
7691
- targetDir = path8.resolve(opts.target);
9415
+ targetDir = path11.resolve(opts.target);
7692
9416
  } else {
7693
9417
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
7694
9418
  targetDir = resolveScopedPath(agentConfig, opts.scope === "project" ? "project" : "user");
7695
9419
  }
7696
- const resolvedTarget = path8.resolve(targetDir);
7697
- const skillDir = path8.resolve(targetDir, name);
7698
- 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)) {
7699
9423
  throw new Error(`Invalid skill name: "${name}"`);
7700
9424
  }
7701
- if (!fs3.existsSync(skillDir)) {
9425
+ if (!fs6.existsSync(skillDir)) {
7702
9426
  throw new Error(`Skill "${name}" not found at ${skillDir}`);
7703
9427
  }
7704
9428
  const meta = readInstalledMeta(targetDir);
7705
9429
  const record = meta.skills[name];
7706
- fs3.rmSync(skillDir, { recursive: true, force: true });
9430
+ fs6.rmSync(skillDir, { recursive: true, force: true });
7707
9431
  delete meta.skills[name];
7708
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9432
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
7709
9433
  success({
7710
9434
  uninstalled: name,
7711
9435
  target: targetDir,
@@ -7776,7 +9500,7 @@ Check what is installed:
7776
9500
  const start = Date.now();
7777
9501
  try {
7778
9502
  const configPath = getGlobalConfigPath();
7779
- const resolved = path8.resolve(dir);
9503
+ const resolved = path11.resolve(dir);
7780
9504
  const forgotten = forgetCustomTarget(configPath, resolved);
7781
9505
  if (!forgotten) {
7782
9506
  warn(`${resolved} is not a tracked custom target \u2014 nothing to forget.`);
@@ -7790,6 +9514,44 @@ Check what is installed:
7790
9514
  fail(err, "skills", "forget-target", start);
7791
9515
  }
7792
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
+ });
7793
9555
  const marketplace = skills.command("marketplace").description("Org plugins from git-hosted marketplaces: add, sync, enable/disable, and shipped AGENTS.md/CLAUDE.md");
7794
9556
  marketplace.addHelpText("after", `
7795
9557
  Workflow:
@@ -7807,7 +9569,7 @@ Shipped instructions:
7807
9569
 
7808
9570
  Plugin skills always install at user scope. --agent picks the host (default: ${DEFAULT_AGENT}); --all-agents covers all of them.
7809
9571
  `);
7810
- 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) => {
7811
9573
  const marketplaceIdx = process.argv.indexOf("marketplace");
7812
9574
  const invokedAs = marketplaceIdx !== -1 && process.argv[marketplaceIdx + 1] === "setup" ? "marketplace-setup" : "marketplace-add";
7813
9575
  return marketplaceAddAction(url, localPath, opts, invokedAs);
@@ -7817,13 +9579,17 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7817
9579
  try {
7818
9580
  const configPath = getGlobalConfigPath();
7819
9581
  const all = loadMarketplaces(configPath);
9582
+ const providerCfg = safeProviderConfig();
7820
9583
  success({
7821
9584
  marketplaces: all.map((m) => ({
7822
9585
  name: marketplaceLabel(m),
7823
9586
  repoUrl: m.repoUrl,
7824
9587
  localPath: m.localPath,
7825
9588
  tokenConfigured: !!m.token,
7826
- 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
7827
9593
  })),
7828
9594
  total: all.length
7829
9595
  }, "skills", "marketplace-list", start);
@@ -7831,6 +9597,54 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7831
9597
  fail(err, "skills", "marketplace-list", start);
7832
9598
  }
7833
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
+ });
7834
9648
  marketplace.command("plugins").description("List the plugins available in a registered marketplace").argument("<name>", "Marketplace name (or repo URL) to inspect").action((name) => {
7835
9649
  const start = Date.now();
7836
9650
  try {
@@ -7840,7 +9654,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7840
9654
  if (!found) {
7841
9655
  throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
7842
9656
  }
7843
- if (!found.localPath || !fs3.existsSync(found.localPath)) {
9657
+ if (!found.localPath || !fs6.existsSync(found.localPath)) {
7844
9658
  throw new Error(`Marketplace "${marketplaceLabel(found)}" local path not found at ${found.localPath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
7845
9659
  }
7846
9660
  const plugins = resolvePluginChoices(found.localPath);
@@ -7861,61 +9675,62 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7861
9675
  fail(err, "skills", "marketplace-remove", start);
7862
9676
  }
7863
9677
  });
7864
- marketplace.command("sync").description("Pull latest marketplace content and install plugin skills (interactive picker when no plugin is given)").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--marketplace <name>", 'Marketplace name to sync, or "all" to sync every registered marketplace (skips interactive selection)').option("--agent <agent>", `Target agent host: ${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("--force", 'Reinstall even if a marketplace has no new changes (applies to single-plugin and "all" installs alike)').option("--installed-only", "Only sync plugins that are already installed \u2014 skip plugins newly added to the marketplace").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action(async (plugin, opts) => {
9678
+ marketplace.command("sync").description("With no arguments, refreshes every already-installed plugin from every marketplace (same as --marketplace all --installed-only). Pass --force, a plugin, or --marketplace to get the interactive picker instead.").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--marketplace <name>", 'Marketplace name to sync, or "all" to sync every registered marketplace (skips interactive selection)').option("--agent <agent>", `Target agent host: ${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("--force", 'Reinstall even if a marketplace has no new changes (applies to single-plugin and "all" installs alike)').option("--installed-only", "Only sync plugins that are already installed \u2014 skip plugins newly added to the marketplace").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action(async (plugin, opts) => {
7865
9679
  const start = Date.now();
7866
9680
  try {
7867
9681
  const configPath = getGlobalConfigPath();
7868
- const allMarketplaces = loadMarketplaces(configPath);
7869
- if (allMarketplaces.length === 0) {
9682
+ const allMarketplaces2 = loadMarketplaces(configPath);
9683
+ if (allMarketplaces2.length === 0) {
7870
9684
  throw new Error("No marketplaces configured. Run: pncli skills marketplace add <url>");
7871
9685
  }
9686
+ const bareSync = isBareMarketplaceSync(plugin, opts);
7872
9687
  const targets = resolveInstallTargets(opts);
7873
- const syncOpts = { force: opts.force ?? false, installedOnly: opts.installedOnly ?? false, instructions: opts.instructions !== false };
9688
+ const syncOpts = { force: opts.force ?? false, installedOnly: bareSync ? true : opts.installedOnly ?? false, instructions: opts.instructions !== false };
7874
9689
  const targetSummary = targets.length === 1 ? { target: targets[0].target } : { targets: targets.map((t) => t.target) };
7875
- if (opts.marketplace === "all") {
7876
- const results = allMarketplaces.map((m) => syncMarketplacePlugins(m, targets, plugin ?? "all", syncOpts));
9690
+ if (opts.marketplace === "all" || bareSync) {
9691
+ const results = allMarketplaces2.map((m) => syncMarketplacePlugins(m, targets, plugin ?? "all", syncOpts));
7877
9692
  success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
7878
9693
  return;
7879
9694
  }
7880
9695
  let selectedMarketplace;
7881
9696
  let selectedPlugin = plugin;
7882
- const canGoBack = !opts.marketplace && allMarketplaces.length > 1;
9697
+ const canGoBack = !opts.marketplace && allMarketplaces2.length > 1;
7883
9698
  if (opts.marketplace) {
7884
- 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);
7885
9700
  if (!found) {
7886
9701
  throw new Error(`Marketplace "${opts.marketplace}" not found. Run: pncli skills marketplace list`);
7887
9702
  }
7888
9703
  selectedMarketplace = found;
7889
- } else if (allMarketplaces.length === 1) {
7890
- selectedMarketplace = allMarketplaces[0];
9704
+ } else if (allMarketplaces2.length === 1) {
9705
+ selectedMarketplace = allMarketplaces2[0];
7891
9706
  }
7892
9707
  for (; ; ) {
7893
9708
  if (!selectedMarketplace) {
7894
- 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.`);
7895
9710
  const chosen = await select({
7896
9711
  message: "Select a marketplace to sync:",
7897
9712
  choices: [
7898
9713
  { value: ALL_MARKETPLACES, name: "All marketplaces \u2014 sync every plugin from every marketplace" },
7899
9714
  // Index-based values: labels alone aren't guaranteed unique (two marketplaces can share a derived name).
7900
- ...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 ?? ""}` }))
7901
9716
  ]
7902
9717
  });
7903
9718
  if (chosen === ALL_MARKETPLACES) {
7904
- const results = allMarketplaces.map((m2) => syncMarketplacePlugins(m2, targets, selectedPlugin ?? "all", syncOpts));
9719
+ const results = allMarketplaces2.map((m2) => syncMarketplacePlugins(m2, targets, selectedPlugin ?? "all", syncOpts));
7905
9720
  success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
7906
9721
  return;
7907
9722
  }
7908
- selectedMarketplace = allMarketplaces[Number(chosen)];
9723
+ selectedMarketplace = allMarketplaces2[Number(chosen)];
7909
9724
  if (!selectedMarketplace) {
7910
9725
  throw new Error(`Marketplace "${chosen}" not found.`);
7911
9726
  }
7912
9727
  }
7913
9728
  const marketplaceName = marketplaceLabel(selectedMarketplace);
7914
9729
  const marketplacePath = selectedMarketplace.localPath;
7915
- if (!marketplacePath || !fs3.existsSync(marketplacePath)) {
9730
+ if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
7916
9731
  throw new Error(`Marketplace "${marketplaceName}" local path not found at ${marketplacePath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
7917
9732
  }
7918
- const { updated } = pullMarketplace(marketplacePath, selectedMarketplace.repoUrl, selectedMarketplace.token, marketplaceName);
9733
+ const { updated } = pullMarketplace(marketplacePath, selectedMarketplace, marketplaceName);
7919
9734
  const pluginChoices = resolvePluginChoices(marketplacePath);
7920
9735
  if (pluginChoices.length === 0) {
7921
9736
  throw new Error(`No plugins found in marketplace "${marketplaceName}". Check the marketplace repository structure.`);
@@ -7969,7 +9784,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7969
9784
  const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
7970
9785
  const agents = Object.keys(AGENT_PATHS);
7971
9786
  const marketplaces = selected.map((m) => {
7972
- 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) : [];
7973
9788
  return {
7974
9789
  marketplace: marketplaceLabel(m),
7975
9790
  localPath: m.localPath ?? null,
@@ -7995,7 +9810,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
7995
9810
  const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
7996
9811
  const results = selected.map((m) => {
7997
9812
  const name = marketplaceLabel(m);
7998
- if (!m.localPath || !fs3.existsSync(m.localPath)) {
9813
+ if (!m.localPath || !fs6.existsSync(m.localPath)) {
7999
9814
  return { marketplace: name, skipped: true, message: "Local path not found. Run: pncli skills marketplace add <url>" };
8000
9815
  }
8001
9816
  if (findMarketplaceInstructions(m.localPath).length === 0) {
@@ -8033,31 +9848,31 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8033
9848
  try {
8034
9849
  let targetDir;
8035
9850
  if (opts.target) {
8036
- targetDir = path8.resolve(opts.target);
9851
+ targetDir = path11.resolve(opts.target);
8037
9852
  } else {
8038
9853
  const agentConfig = resolveAgentPaths(resolveAgentName(opts));
8039
9854
  targetDir = resolveScopedPath(agentConfig, opts.scope ?? "user");
8040
9855
  }
8041
- const resolvedTarget = path8.resolve(targetDir);
8042
- if (!fs3.existsSync(targetDir)) {
9856
+ const resolvedTarget = path11.resolve(targetDir);
9857
+ if (!fs6.existsSync(targetDir)) {
8043
9858
  success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
8044
9859
  return;
8045
9860
  }
8046
9861
  const meta = readInstalledMeta(targetDir);
8047
9862
  const removed = [];
8048
9863
  const skipped = [];
8049
- const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
9864
+ const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
8050
9865
  if (name.startsWith(".")) return false;
8051
- const p = path8.join(targetDir, name);
9866
+ const p = path11.join(targetDir, name);
8052
9867
  try {
8053
- return fs3.statSync(p).isDirectory();
9868
+ return fs6.statSync(p).isDirectory();
8054
9869
  } catch {
8055
9870
  return false;
8056
9871
  }
8057
9872
  });
8058
9873
  for (const skillName of skillDirs) {
8059
- const skillDir = path8.resolve(targetDir, skillName);
8060
- if (!skillDir.startsWith(resolvedTarget + path8.sep)) continue;
9874
+ const skillDir = path11.resolve(targetDir, skillName);
9875
+ if (!skillDir.startsWith(resolvedTarget + path11.sep)) continue;
8061
9876
  let record = meta.skills[skillName] ?? null;
8062
9877
  if (!record) {
8063
9878
  const perSkill = readSkillOrigin(skillDir);
@@ -8082,12 +9897,12 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8082
9897
  skipped.push(skillName);
8083
9898
  continue;
8084
9899
  }
8085
- fs3.rmSync(skillDir, { recursive: true, force: true });
9900
+ fs6.rmSync(skillDir, { recursive: true, force: true });
8086
9901
  delete meta.skills[skillName];
8087
9902
  removed.push(skillName);
8088
9903
  }
8089
9904
  if (removed.length > 0) {
8090
- fs3.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9905
+ fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8091
9906
  }
8092
9907
  success({
8093
9908
  removed,
@@ -8105,7 +9920,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8105
9920
  const start = Date.now();
8106
9921
  try {
8107
9922
  const targetDir = resolveTargetDir(opts);
8108
- if (!fs3.existsSync(targetDir)) {
9923
+ if (!fs6.existsSync(targetDir)) {
8109
9924
  success({ disabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-disable", start);
8110
9925
  return;
8111
9926
  }
@@ -8117,7 +9932,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8117
9932
  total: result.disabled.length,
8118
9933
  target: targetDir,
8119
9934
  plugin,
8120
- stash: path8.join(targetDir, DISABLED_SUBDIR),
9935
+ stash: path11.join(targetDir, DISABLED_SUBDIR),
8121
9936
  ...opts.marketplace ? { marketplace: opts.marketplace } : {}
8122
9937
  }, "skills", "marketplace-disable", start);
8123
9938
  } catch (err) {
@@ -8128,7 +9943,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8128
9943
  const start = Date.now();
8129
9944
  try {
8130
9945
  const targetDir = resolveTargetDir(opts);
8131
- if (!fs3.existsSync(targetDir)) {
9946
+ if (!fs6.existsSync(targetDir)) {
8132
9947
  success({ enabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-enable", start);
8133
9948
  return;
8134
9949
  }
@@ -8167,9 +9982,9 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8167
9982
  const appliedInstructions = /* @__PURE__ */ new Map();
8168
9983
  const instructionAgents = opts.target ? [] : [resolveAgentName(opts)];
8169
9984
  for (; ; ) {
8170
- const states = fs3.existsSync(targetDir) ? listPluginStates(targetDir) : [];
9985
+ const states = fs6.existsSync(targetDir) ? listPluginStates(targetDir) : [];
8171
9986
  const registered = loadMarketplaces(getGlobalConfigPath());
8172
- 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);
8173
9988
  const action = await select({
8174
9989
  message: `Manage skills marketplaces (target: ${targetDir}):`,
8175
9990
  choices: [
@@ -8283,15 +10098,15 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
8283
10098
  const start = Date.now();
8284
10099
  try {
8285
10100
  const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
8286
- if (!fs3.existsSync(userDir)) {
10101
+ if (!fs6.existsSync(userDir)) {
8287
10102
  success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
8288
10103
  return;
8289
10104
  }
8290
- const skillDirs = fs3.readdirSync(userDir).filter((name) => {
10105
+ const skillDirs = fs6.readdirSync(userDir).filter((name) => {
8291
10106
  if (name.startsWith(".")) return false;
8292
- const p = path8.join(userDir, name);
10107
+ const p = path11.join(userDir, name);
8293
10108
  try {
8294
- return fs3.statSync(p).isDirectory();
10109
+ return fs6.statSync(p).isDirectory();
8295
10110
  } catch {
8296
10111
  return false;
8297
10112
  }
@@ -8307,13 +10122,13 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
8307
10122
  );
8308
10123
  }
8309
10124
  for (const name of skillDirs) {
8310
- const skillDir = path8.resolve(userDir, name);
8311
- if (!skillDir.startsWith(path8.resolve(userDir) + path8.sep)) continue;
8312
- 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 });
8313
10128
  }
8314
10129
  const metaPath = getInstalledMetaPath(userDir);
8315
- if (fs3.existsSync(metaPath)) {
8316
- 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");
8317
10132
  }
8318
10133
  success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
8319
10134
  } catch (err) {
@@ -8322,46 +10137,46 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
8322
10137
  });
8323
10138
  }
8324
10139
  function resolvePluginChoices(marketplacePath) {
8325
- const marketplaceJsonPath = path8.join(marketplacePath, ".claude-plugin", "marketplace.json");
8326
- if (fs3.existsSync(marketplaceJsonPath)) {
10140
+ const marketplaceJsonPath = path11.join(marketplacePath, ".claude-plugin", "marketplace.json");
10141
+ if (fs6.existsSync(marketplaceJsonPath)) {
8327
10142
  try {
8328
- const meta = JSON.parse(fs3.readFileSync(marketplaceJsonPath, "utf8"));
10143
+ const meta = JSON.parse(fs6.readFileSync(marketplaceJsonPath, "utf8"));
8329
10144
  if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
8330
10145
  return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
8331
10146
  }
8332
10147
  } catch {
8333
10148
  }
8334
10149
  }
8335
- const pluginsDir = path8.join(marketplacePath, "plugins");
8336
- if (fs3.existsSync(pluginsDir)) {
8337
- 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: "" }));
8338
10153
  }
8339
10154
  return [];
8340
10155
  }
8341
10156
  function resolveSkillsSrc(marketplacePath, selectedPlugin) {
8342
- const pluginsBase = path8.resolve(marketplacePath, "plugins");
8343
- const skillsSrc = path8.resolve(pluginsBase, selectedPlugin, "skills");
8344
- 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)) {
8345
10160
  throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
8346
10161
  }
8347
10162
  return skillsSrc;
8348
10163
  }
8349
10164
  function copyPluginSkills(skillsSrc, targetDir, meta) {
8350
- fs3.mkdirSync(targetDir, { recursive: true });
8351
- const resolvedTarget = path8.resolve(targetDir);
8352
- const skillNames = fs3.readdirSync(skillsSrc).filter(
8353
- (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()
8354
10169
  );
8355
10170
  const installed = [];
8356
10171
  const failed = [];
8357
10172
  for (const skillName of skillNames) {
8358
- const dest = path8.resolve(targetDir, skillName);
8359
- if (!dest.startsWith(resolvedTarget + path8.sep)) {
10173
+ const dest = path11.resolve(targetDir, skillName);
10174
+ if (!dest.startsWith(resolvedTarget + path11.sep)) {
8360
10175
  failed.push(skillName);
8361
10176
  continue;
8362
10177
  }
8363
- fs3.rmSync(dest, { recursive: true, force: true });
8364
- 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 });
8365
10180
  installed.push(skillName);
8366
10181
  }
8367
10182
  if (meta) {
@@ -8378,9 +10193,10 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
8378
10193
 
8379
10194
  // src/services/config/commands.ts
8380
10195
  import chalk from "chalk";
8381
- import fs4 from "fs";
10196
+ import fs7 from "fs";
8382
10197
  function registerConfigCommands(program2) {
8383
10198
  const config = program2.command("config").description("Manage pncli configuration");
10199
+ registerKeychainCommands(config, program2);
8384
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) => {
8385
10201
  const start = Date.now();
8386
10202
  try {
@@ -8492,8 +10308,8 @@ function registerConfigCommands(program2) {
8492
10308
  if (cfg.ado.baseUrl) {
8493
10309
  try {
8494
10310
  const collection = cfg.defaults.ado?.collection;
8495
- const path11 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
8496
- 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);
8497
10313
  results.ado = { ok: true, message: "connected" };
8498
10314
  } catch (err) {
8499
10315
  results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -8786,7 +10602,7 @@ async function initGlobalConfig(start) {
8786
10602
  message: "Jira base URL (e.g. jira.imagile.dev):",
8787
10603
  default: ""
8788
10604
  });
8789
- const jiraApiToken = await password({
10605
+ const jiraApiToken = await password2({
8790
10606
  message: "Jira personal access token:"
8791
10607
  });
8792
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");
@@ -8794,7 +10610,7 @@ async function initGlobalConfig(start) {
8794
10610
  message: "Bitbucket Server base URL (e.g. bitbucket.imagile.dev):",
8795
10611
  default: ""
8796
10612
  });
8797
- const bitbucketPat = await password({
10613
+ const bitbucketPat = await password2({
8798
10614
  message: "Bitbucket personal access token:"
8799
10615
  });
8800
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");
@@ -8809,7 +10625,7 @@ async function initGlobalConfig(start) {
8809
10625
  message: "GitHub API base URL\n GitHub.com: api.github.com\n GitHub Enterprise: <host>/api/v3\n URL: ",
8810
10626
  default: ""
8811
10627
  });
8812
- githubToken = await password({
10628
+ githubToken = await password2({
8813
10629
  message: "GitHub personal access token (classic or fine-grained):"
8814
10630
  });
8815
10631
  if (githubBaseUrl && githubToken) {
@@ -8836,7 +10652,7 @@ async function initGlobalConfig(start) {
8836
10652
  message: "Confluence base URL (e.g. confluence.imagile.dev):",
8837
10653
  default: ""
8838
10654
  });
8839
- const confluenceApiToken = await password({
10655
+ const confluenceApiToken = await password2({
8840
10656
  message: "Confluence personal access token:"
8841
10657
  });
8842
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");
@@ -8854,7 +10670,7 @@ async function initGlobalConfig(start) {
8854
10670
  message: "Artifactory base URL (e.g. artifactory.imagile.dev):",
8855
10671
  default: ""
8856
10672
  });
8857
- artifactoryToken = await password({
10673
+ artifactoryToken = await password2({
8858
10674
  message: "Artifactory API token:"
8859
10675
  });
8860
10676
  process.stderr.write("\nConfigure which ecosystems you use (skip any that don't apply):\n");
@@ -8883,7 +10699,7 @@ async function initGlobalConfig(start) {
8883
10699
  message: "SonarQube Server base URL (e.g. sonar.imagile.dev):",
8884
10700
  default: ""
8885
10701
  });
8886
- sonarToken = await password({
10702
+ sonarToken = await password2({
8887
10703
  message: "SonarQube personal access token:"
8888
10704
  });
8889
10705
  }
@@ -8901,7 +10717,7 @@ async function initGlobalConfig(start) {
8901
10717
  default: false
8902
10718
  });
8903
10719
  if (useSdeConnectionString) {
8904
- const sdeConnection = await password({
10720
+ const sdeConnection = await password2({
8905
10721
  message: "SDElements connection string (api-token@hostname, e.g. mytoken@myorg.sdelements.com):"
8906
10722
  });
8907
10723
  const atIdx = sdeConnection.indexOf("@");
@@ -8914,7 +10730,7 @@ async function initGlobalConfig(start) {
8914
10730
  message: "SDElements hostname\n Cloud-hosted: imagile.sdelements.com\n On-premise: sde.imagile.dev\n Host: ",
8915
10731
  default: ""
8916
10732
  });
8917
- sdeToken = await password({
10733
+ sdeToken = await password2({
8918
10734
  message: "SDElements API token:"
8919
10735
  });
8920
10736
  }
@@ -8936,7 +10752,7 @@ async function initGlobalConfig(start) {
8936
10752
  message: "Azure DevOps Server base URL (e.g. tfs.imagile.dev or devops.imagile.dev/tfs):",
8937
10753
  default: ""
8938
10754
  });
8939
- adoPat = await password({
10755
+ adoPat = await password2({
8940
10756
  message: "Azure DevOps personal access token:"
8941
10757
  });
8942
10758
  adoCollection = await input2({
@@ -9012,7 +10828,7 @@ async function initGlobalConfig(start) {
9012
10828
  message: "Jenkins username:",
9013
10829
  default: ""
9014
10830
  });
9015
- jenkinsApiToken = await password({
10831
+ jenkinsApiToken = await password2({
9016
10832
  message: "Jenkins API token:"
9017
10833
  });
9018
10834
  }
@@ -9035,7 +10851,7 @@ async function initGlobalConfig(start) {
9035
10851
  message: "Checkmarx One tenant name (IAM realm, e.g. imagile):",
9036
10852
  default: ""
9037
10853
  });
9038
- checkmarxApiKey = await password({
10854
+ checkmarxApiKey = await password2({
9039
10855
  message: "Checkmarx One API key (leave blank to use an OAuth client):"
9040
10856
  });
9041
10857
  if (!checkmarxApiKey) {
@@ -9043,7 +10859,7 @@ async function initGlobalConfig(start) {
9043
10859
  message: "Checkmarx One OAuth client ID:",
9044
10860
  default: ""
9045
10861
  });
9046
- checkmarxClientSecret = await password({
10862
+ checkmarxClientSecret = await password2({
9047
10863
  message: "Checkmarx One OAuth client secret:",
9048
10864
  validate: (v) => v.length > 0 || "Client secret cannot be blank"
9049
10865
  });
@@ -9088,7 +10904,7 @@ async function initGlobalConfig(start) {
9088
10904
  message: "User Code (from User Token):",
9089
10905
  default: ""
9090
10906
  });
9091
- sonatypeIqPasscode = await password({
10907
+ sonatypeIqPasscode = await password2({
9092
10908
  message: "Passcode (from User Token):"
9093
10909
  });
9094
10910
  if (sonatypeIqBaseUrl && sonatypeIqUserCode && sonatypeIqPasscode) {
@@ -9136,10 +10952,10 @@ async function initGlobalConfig(start) {
9136
10952
  message: "Username (email):",
9137
10953
  default: ""
9138
10954
  });
9139
- contrastApiKey = await password({
10955
+ contrastApiKey = await password2({
9140
10956
  message: "API key:"
9141
10957
  });
9142
- contrastServiceKey = await password({
10958
+ contrastServiceKey = await password2({
9143
10959
  message: "Service key:"
9144
10960
  });
9145
10961
  const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
@@ -9178,7 +10994,7 @@ async function initGlobalConfig(start) {
9178
10994
  message: "OpenShift API server URL (e.g. https://api.cluster.imagile.dev:6443):",
9179
10995
  default: ""
9180
10996
  });
9181
- openShiftToken = await password({
10997
+ openShiftToken = await password2({
9182
10998
  message: "Service account token:"
9183
10999
  });
9184
11000
  if (openShiftBaseUrl && openShiftToken) {
@@ -9214,7 +11030,7 @@ async function initGlobalConfig(start) {
9214
11030
  message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
9215
11031
  default: ""
9216
11032
  });
9217
- dynatraceApiToken = await password({
11033
+ dynatraceApiToken = await password2({
9218
11034
  message: "Environment API token (entities.read and problems.read):"
9219
11035
  });
9220
11036
  const configureTraces = await confirm({
@@ -9226,7 +11042,7 @@ async function initGlobalConfig(start) {
9226
11042
  message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
9227
11043
  default: ""
9228
11044
  });
9229
- dynatracePlatformToken = await password({
11045
+ dynatracePlatformToken = await password2({
9230
11046
  message: "Platform token with Grail span read permissions:"
9231
11047
  });
9232
11048
  }
@@ -9265,7 +11081,7 @@ async function initGlobalConfig(start) {
9265
11081
  message: "LogScale base URL (e.g. https://logscale.imagile.dev):",
9266
11082
  default: ""
9267
11083
  });
9268
- logscaleToken = await password({
11084
+ logscaleToken = await password2({
9269
11085
  message: "LogScale personal access token:"
9270
11086
  });
9271
11087
  if (logscaleBaseUrl && logscaleToken) {
@@ -9299,7 +11115,7 @@ async function initGlobalConfig(start) {
9299
11115
  message: "Split.IO Admin API base URL (e.g. https://api.split.io):",
9300
11116
  default: ""
9301
11117
  });
9302
- splitioAdminApiKey = await password({
11118
+ splitioAdminApiKey = await password2({
9303
11119
  message: "Split.IO Admin API key:"
9304
11120
  });
9305
11121
  if (splitioBaseUrl && splitioAdminApiKey) {
@@ -9329,7 +11145,7 @@ async function initGlobalConfig(start) {
9329
11145
  let figmaToken = "";
9330
11146
  if (useFigma) {
9331
11147
  process.stderr.write(" Generate a personal access token in Figma under Account Settings \u2192 Personal access tokens.\n");
9332
- figmaToken = await password({
11148
+ figmaToken = await password2({
9333
11149
  message: "Figma personal access token:"
9334
11150
  });
9335
11151
  if (figmaToken) {
@@ -9366,7 +11182,7 @@ async function initGlobalConfig(start) {
9366
11182
  message: "Alation base URL (e.g. https://alation.imagile.dev):",
9367
11183
  validate: (v) => v.trim().length > 0 || "Required"
9368
11184
  });
9369
- alationRefreshToken = await password({
11185
+ alationRefreshToken = await password2({
9370
11186
  message: "Alation refresh token:"
9371
11187
  });
9372
11188
  alationUserId = await input2({
@@ -9422,6 +11238,11 @@ async function initGlobalConfig(start) {
9422
11238
  const existingOpenshift = existingGlobal?.openshift;
9423
11239
  writeGlobalConfig({
9424
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 } : {},
9425
11246
  user: {
9426
11247
  email: userEmail || void 0,
9427
11248
  userId: userId || void 0
@@ -9624,7 +11445,7 @@ async function initRepoConfig(start) {
9624
11445
  process.exitCode = ExitCode.SUCCESS;
9625
11446
  return;
9626
11447
  }
9627
- if (fs4.existsSync(".pncli.json")) {
11448
+ if (fs7.existsSync(".pncli.json")) {
9628
11449
  const overwrite = await confirm({
9629
11450
  message: ".pncli.json already exists. Overwrite?",
9630
11451
  default: false
@@ -9658,14 +11479,172 @@ async function initRepoConfig(start) {
9658
11479
  }
9659
11480
 
9660
11481
  // src/services/doctor/commands.ts
9661
- import fs5 from "fs";
9662
- 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
9663
11642
  function checkConfigFile(filePath) {
9664
- if (!fs5.existsSync(filePath)) {
11643
+ if (!fs9.existsSync(filePath)) {
9665
11644
  return { path: filePath, exists: false, valid: false, message: "not present" };
9666
11645
  }
9667
11646
  try {
9668
- JSON.parse(fs5.readFileSync(filePath, "utf8"));
11647
+ JSON.parse(fs9.readFileSync(filePath, "utf8"));
9669
11648
  return { path: filePath, exists: true, valid: true, message: "ok" };
9670
11649
  } catch (err) {
9671
11650
  return {
@@ -9746,13 +11725,13 @@ function buildProblems(globalFile, repoFile, credentials, skillLocations, market
9746
11725
  return problems;
9747
11726
  }
9748
11727
  function registerDoctorCommands(program2) {
9749
- 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) => {
9750
11729
  const start = Date.now();
9751
11730
  try {
9752
11731
  const opts = program2.optsWithGlobals();
9753
11732
  const globalFile = checkConfigFile(opts.config ?? getGlobalConfigPath());
9754
11733
  const repoRoot = findGitRoot() ?? process.cwd();
9755
- const repoFile = checkConfigFile(path9.join(repoRoot, ".pncli.json"));
11734
+ const repoFile = checkConfigFile(path12.join(repoRoot, ".pncli.json"));
9756
11735
  let credentials = null;
9757
11736
  let credentialsSkipped = cmdOpts.offline ? "--offline" : null;
9758
11737
  if (!cmdOpts.offline) {
@@ -9777,9 +11756,33 @@ function registerDoctorCommands(program2) {
9777
11756
  name: m.name ?? m.repoUrl ?? "(unnamed)",
9778
11757
  repoUrl: m.repoUrl ?? null,
9779
11758
  localPath: m.localPath ?? null,
9780
- cloneExists: !!m.localPath && fs5.existsSync(m.localPath)
11759
+ cloneExists: !!m.localPath && fs9.existsSync(m.localPath)
9781
11760
  }));
9782
- 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
+ ];
9783
11786
  if (credentials) {
9784
11787
  const statuses = Object.values(credentials).map((r) => r.status);
9785
11788
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
@@ -9802,7 +11805,10 @@ function registerDoctorCommands(program2) {
9802
11805
  registered: marketplaces,
9803
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"
9804
11807
  },
9805
- problems
11808
+ keychain,
11809
+ gitAuth,
11810
+ problems,
11811
+ hint: problems.length === 0 ? null : "New to skills and marketplaces? pncli skills guide troubleshooting"
9806
11812
  }, "doctor", "check", start);
9807
11813
  } catch (err) {
9808
11814
  fail(err, "doctor", "check", start);
@@ -10191,8 +12197,8 @@ function registerAdoProjectCommands(ado) {
10191
12197
  }
10192
12198
 
10193
12199
  // src/services/ado/commands/work.ts
10194
- import fs6 from "fs";
10195
- import path10 from "path";
12200
+ import fs10 from "fs";
12201
+ import path13 from "path";
10196
12202
 
10197
12203
  // src/services/ado/input-schema.ts
10198
12204
  var ADO_WORK_INPUT_FILE_SCHEMA = {
@@ -10433,11 +12439,11 @@ function registerAdoWorkCommands(ado) {
10433
12439
  const attachments = await workClient.listAttachments(collection, parseInt(opts.id, 10));
10434
12440
  const attachment = attachments.find((a) => a.id === opts.attachmentId);
10435
12441
  if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
10436
- const outDir = opts.dir ?? path10.join(process.cwd(), ".pncli");
10437
- fs6.mkdirSync(outDir, { recursive: true });
10438
- 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));
10439
12445
  const buffer = await workClient.downloadAttachment(attachment.url);
10440
- fs6.writeFileSync(outPath, buffer);
12446
+ fs10.writeFileSync(outPath, buffer);
10441
12447
  success({ saved: outPath, name: attachment.name, size: buffer.length }, "ado", "work-download-attachment", start);
10442
12448
  } catch (err) {
10443
12449
  fail(err, "ado", "work-download-attachment", start);
@@ -10471,10 +12477,10 @@ function registerAdoWorkCommands(ado) {
10471
12477
  const types = await discoverTypes(workClient, collection, project);
10472
12478
  if (opts.save) {
10473
12479
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
10474
- const fs9 = await import("fs");
10475
- const existing = JSON.parse(fs9.readFileSync(globalConfigPath, "utf8") || "{}");
12480
+ const fs13 = await import("fs");
12481
+ const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
10476
12482
  existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
10477
- fs9.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
12483
+ fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10478
12484
  warn(`Saved ${types.length} types to ${globalConfigPath}`);
10479
12485
  }
10480
12486
  success(types, "ado", "work-types", start);
@@ -10503,11 +12509,11 @@ function registerAdoWorkCommands(ado) {
10503
12509
  ) : allFields;
10504
12510
  if (opts.save) {
10505
12511
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
10506
- const fs9 = await import("fs");
10507
- const existing = JSON.parse(fs9.readFileSync(globalConfigPath, "utf8") || "{}");
12512
+ const fs13 = await import("fs");
12513
+ const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
10508
12514
  const aliases = buildDefaultAliases(allFields);
10509
12515
  existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
10510
- fs9.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
12516
+ fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
10511
12517
  warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
10512
12518
  }
10513
12519
  success(fields, "ado", "work-fields", start);
@@ -10804,7 +12810,7 @@ function registerAdoRepoCommands(ado) {
10804
12810
  const start = Date.now();
10805
12811
  try {
10806
12812
  const globalOpts = ado.optsWithGlobals();
10807
- const { loadConfig: lc } = await import("./config-Y6IE6GY7.js");
12813
+ const { loadConfig: lc } = await import("./config-OH2O5E3P.js");
10808
12814
  const config = lc({ configPath: globalOpts.config });
10809
12815
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
10810
12816
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
@@ -11105,6 +13111,10 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
11105
13111
  }
11106
13112
  throw new PncliError(`Timed out waiting for build #${buildNumber} to complete after ${timeoutMs / 1e3}s`, 1);
11107
13113
  }
13114
+ function rawJenkinsInstances(configPath) {
13115
+ const stored = loadJsonFile(getGlobalConfigPath(configPath))?.jenkinsInstances;
13116
+ return Array.isArray(stored) ? stored : [];
13117
+ }
11108
13118
  function registerJenkinsCommands(program2) {
11109
13119
  const jenkins = program2.command("jenkins").description("Jenkins operations").option("--instance <name>", "Named Jenkins instance from global config (overrides default jenkins config)");
11110
13120
  const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
@@ -11128,15 +13138,15 @@ function registerJenkinsCommands(program2) {
11128
13138
  try {
11129
13139
  const opts = jenkins.optsWithGlobals();
11130
13140
  const configPath = opts.config;
11131
- const existing = loadConfig({ configPath }).jenkinsInstances;
13141
+ const existing = rawJenkinsInstances(configPath);
11132
13142
  const idx = existing.findIndex((i) => i.name === o.name);
11133
13143
  if (idx >= 0 && !o.force) {
11134
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);
11135
13145
  }
11136
13146
  let apiToken = o.apiToken;
11137
13147
  if (!apiToken && process.stdin.isTTY) {
11138
- const { default: password2 } = await import("@inquirer/password");
11139
- 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):` });
11140
13150
  }
11141
13151
  const entry = {
11142
13152
  name: o.name,
@@ -11156,7 +13166,7 @@ function registerJenkinsCommands(program2) {
11156
13166
  try {
11157
13167
  const opts = jenkins.optsWithGlobals();
11158
13168
  const configPath = opts.config;
11159
- const existing = loadConfig({ configPath }).jenkinsInstances;
13169
+ const existing = rawJenkinsInstances(configPath);
11160
13170
  if (!existing.some((i) => i.name === o.name)) {
11161
13171
  throw new PncliError(`Jenkins instance "${o.name}" not found. Run: pncli jenkins instance list`, 1);
11162
13172
  }
@@ -11278,18 +13288,18 @@ var ArtifactoryClient = class {
11278
13288
  }
11279
13289
  });
11280
13290
  }
11281
- async getStorageInfo(repo, path11) {
11282
- const cleanPath = path11.startsWith("/") ? path11.slice(1) : path11;
13291
+ async getStorageInfo(repo, path14) {
13292
+ const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
11283
13293
  return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`);
11284
13294
  }
11285
- async getProperties(repo, path11) {
11286
- const cleanPath = path11.startsWith("/") ? path11.slice(1) : path11;
13295
+ async getProperties(repo, path14) {
13296
+ const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
11287
13297
  return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
11288
13298
  params: { properties: "" }
11289
13299
  });
11290
13300
  }
11291
- async setProperties(repo, path11, properties, opts = {}) {
11292
- const cleanPath = path11.startsWith("/") ? path11.slice(1) : path11;
13301
+ async setProperties(repo, path14, properties, opts = {}) {
13302
+ const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
11293
13303
  const propString = Object.entries(properties).map(([k, v]) => `${k}=${v}`).join(";");
11294
13304
  await this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
11295
13305
  method: "PUT",
@@ -11371,27 +13381,27 @@ function registerArtifactoryCommands(program2) {
11371
13381
  fail(err, "artifactory", "repos", start);
11372
13382
  }
11373
13383
  });
11374
- 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) => {
11375
13385
  const start = Date.now();
11376
13386
  try {
11377
13387
  const client = getClient8(program2);
11378
- const data = await client.getStorageInfo(repo, path11);
13388
+ const data = await client.getStorageInfo(repo, path14);
11379
13389
  success(data, "artifactory", "artifact-info", start);
11380
13390
  } catch (err) {
11381
13391
  fail(err, "artifactory", "artifact-info", start);
11382
13392
  }
11383
13393
  });
11384
- 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) => {
11385
13395
  const start = Date.now();
11386
13396
  try {
11387
13397
  const client = getClient8(program2);
11388
- const data = await client.getProperties(repo, path11);
13398
+ const data = await client.getProperties(repo, path14);
11389
13399
  success(data, "artifactory", "properties-get", start);
11390
13400
  } catch (err) {
11391
13401
  fail(err, "artifactory", "properties-get", start);
11392
13402
  }
11393
13403
  });
11394
- 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) => {
11395
13405
  const start = Date.now();
11396
13406
  try {
11397
13407
  if (!keyvals.length) throw new PncliError("Provide at least one key=value pair");
@@ -11402,8 +13412,8 @@ function registerArtifactoryCommands(program2) {
11402
13412
  properties[kv.slice(0, eq)] = kv.slice(eq + 1);
11403
13413
  }
11404
13414
  const client = getClient8(program2);
11405
- await client.setProperties(repo, path11, properties, { recursive: opts.recursive });
11406
- 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);
11407
13417
  } catch (err) {
11408
13418
  fail(err, "artifactory", "properties-set", start);
11409
13419
  }
@@ -11721,8 +13731,8 @@ function registerSonatypeIqCommands(program2) {
11721
13731
  const startTime = Date.now();
11722
13732
  try {
11723
13733
  const http = getHttpClient(program2);
11724
- const path11 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
11725
- 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);
11726
13736
  success(data, "sonatypeiq", "policies list", startTime);
11727
13737
  } catch (err) {
11728
13738
  fail(err, "sonatypeiq", "policies list", startTime);
@@ -12417,7 +14427,7 @@ function registerLogscaleCommands(program2) {
12417
14427
  }
12418
14428
 
12419
14429
  // src/services/splitio/commands.ts
12420
- import fs7 from "fs";
14430
+ import fs11 from "fs";
12421
14431
  import confirm2 from "@inquirer/confirm";
12422
14432
  function getHttp4(program2) {
12423
14433
  const opts = program2.optsWithGlobals();
@@ -12429,9 +14439,9 @@ function getHttp4(program2) {
12429
14439
  function loadInputFile(filePath) {
12430
14440
  let raw;
12431
14441
  if (filePath === "-") {
12432
- raw = fs7.readFileSync(process.stdin.fd, "utf8");
14442
+ raw = fs11.readFileSync(process.stdin.fd, "utf8");
12433
14443
  } else {
12434
- raw = fs7.readFileSync(filePath, "utf8");
14444
+ raw = fs11.readFileSync(filePath, "utf8");
12435
14445
  }
12436
14446
  try {
12437
14447
  return JSON.parse(raw);
@@ -12482,7 +14492,7 @@ function registerSplitioCommands(program2) {
12482
14492
  try {
12483
14493
  const http = getHttp4(program2);
12484
14494
  const data = await http.splitio(
12485
- `/internal/api/v2/workspaces/${encodeURIComponent(opts.workspace)}/environments`
14495
+ `/internal/api/v2/environments/ws/${encodeURIComponent(opts.workspace)}`
12486
14496
  );
12487
14497
  success(
12488
14498
  { count: data.objects.length, environments: data.objects },
@@ -12500,12 +14510,14 @@ function registerSplitioCommands(program2) {
12500
14510
  try {
12501
14511
  const http = getHttp4(program2);
12502
14512
  const params = {
12503
- wsId: opts.workspace,
12504
14513
  limit: parseInt(opts.limit, 10),
12505
14514
  offset: parseInt(opts.offset, 10)
12506
14515
  };
12507
14516
  if (opts.flagSet) params["flagSetName"] = opts.flagSet;
12508
- const data = await http.splitio("/internal/api/v2/splits", { params });
14517
+ const data = await http.splitio(
14518
+ `/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}`,
14519
+ { params }
14520
+ );
12509
14521
  success(
12510
14522
  { count: data.objects.length, totalCount: data.totalCount, flags: data.objects },
12511
14523
  "splitio",
@@ -12522,14 +14534,12 @@ function registerSplitioCommands(program2) {
12522
14534
  const http = getHttp4(program2);
12523
14535
  if (opts.environment) {
12524
14536
  const data = await http.splitio(
12525
- `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/environments/${encodeURIComponent(opts.environment)}`,
12526
- { params: { wsId: opts.workspace } }
14537
+ `/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}/environment/${encodeURIComponent(opts.environment)}`
12527
14538
  );
12528
14539
  success(data, "splitio", "flags get", start);
12529
14540
  } else {
12530
14541
  const data = await http.splitio(
12531
- `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}`,
12532
- { params: { wsId: opts.workspace } }
14542
+ `/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}`
12533
14543
  );
12534
14544
  success(data, "splitio", "flags get", start);
12535
14545
  }
@@ -12554,7 +14564,7 @@ function registerSplitioCommands(program2) {
12554
14564
  },
12555
14565
  workspace: { id: opts.workspace }
12556
14566
  };
12557
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14567
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12558
14568
  method: "POST",
12559
14569
  body
12560
14570
  });
@@ -12571,8 +14581,7 @@ function registerSplitioCommands(program2) {
12571
14581
  }
12572
14582
  const http = getHttp4(program2);
12573
14583
  const current = await http.splitio(
12574
- `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/environments/${encodeURIComponent(opts.environment)}`,
12575
- { params: { wsId: opts.workspace } }
14584
+ `/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}/environment/${encodeURIComponent(opts.environment)}`
12576
14585
  );
12577
14586
  const rules = current.rules ?? [];
12578
14587
  const idx = parseInt(opts.ruleIndex, 10);
@@ -12596,7 +14605,7 @@ function registerSplitioCommands(program2) {
12596
14605
  },
12597
14606
  workspace: { id: opts.workspace }
12598
14607
  };
12599
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14608
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12600
14609
  method: "POST",
12601
14610
  body
12602
14611
  });
@@ -12610,8 +14619,7 @@ function registerSplitioCommands(program2) {
12610
14619
  try {
12611
14620
  const http = getHttp4(program2);
12612
14621
  const current = await http.splitio(
12613
- `/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/environments/${encodeURIComponent(opts.environment)}`,
12614
- { params: { wsId: opts.workspace } }
14622
+ `/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}/environment/${encodeURIComponent(opts.environment)}`
12615
14623
  );
12616
14624
  await confirmWrite(opts, `SET_DEFAULT ${opts.treatment} on ${opts.flag} in ${opts.environment}`);
12617
14625
  const body = {
@@ -12627,7 +14635,7 @@ function registerSplitioCommands(program2) {
12627
14635
  },
12628
14636
  workspace: { id: opts.workspace }
12629
14637
  };
12630
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14638
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12631
14639
  method: "POST",
12632
14640
  body
12633
14641
  });
@@ -12649,7 +14657,7 @@ function registerSplitioCommands(program2) {
12649
14657
  split: { name: opts.flag, environment: { id: opts.environment } },
12650
14658
  workspace: { id: opts.workspace }
12651
14659
  };
12652
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14660
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12653
14661
  method: "POST",
12654
14662
  body
12655
14663
  });
@@ -12677,7 +14685,7 @@ function registerSplitioCommands(program2) {
12677
14685
  split: { name: entry.flag, environment: { id: opts.environment } },
12678
14686
  workspace: { id: opts.workspace }
12679
14687
  };
12680
- const cr = await http.splitio("/internal/api/v2/changeRequests", {
14688
+ const cr = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12681
14689
  method: "POST",
12682
14690
  body
12683
14691
  });
@@ -12706,7 +14714,7 @@ function registerSplitioCommands(program2) {
12706
14714
  split: { name: opts.flag, environment: { id: opts.environment } },
12707
14715
  workspace: { id: opts.workspace }
12708
14716
  };
12709
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14717
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12710
14718
  method: "POST",
12711
14719
  body
12712
14720
  });
@@ -12726,7 +14734,7 @@ function registerSplitioCommands(program2) {
12726
14734
  split: { name: opts.flag, environment: { id: opts.environment } },
12727
14735
  workspace: { id: opts.workspace }
12728
14736
  };
12729
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14737
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12730
14738
  method: "POST",
12731
14739
  body
12732
14740
  });
@@ -12746,7 +14754,7 @@ function registerSplitioCommands(program2) {
12746
14754
  split: { name: opts.flag },
12747
14755
  workspace: { id: opts.workspace }
12748
14756
  };
12749
- const data = await http.splitio("/internal/api/v2/changeRequests", {
14757
+ const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
12750
14758
  method: "POST",
12751
14759
  body
12752
14760
  });
@@ -12761,14 +14769,13 @@ function registerSplitioCommands(program2) {
12761
14769
  try {
12762
14770
  const http = getHttp4(program2);
12763
14771
  const params = {
12764
- wsId: opts.workspace,
12765
14772
  limit: parseInt(opts.limit, 10),
12766
14773
  offset: parseInt(opts.offset, 10)
12767
14774
  };
12768
14775
  if (opts.environment) params["environmentId"] = opts.environment;
12769
14776
  if (opts.status) params["status"] = opts.status;
12770
14777
  const data = await http.splitio(
12771
- "/internal/api/v2/changeRequests",
14778
+ `/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`,
12772
14779
  { params }
12773
14780
  );
12774
14781
  success(
@@ -12966,7 +14973,7 @@ function registerFigmaCommands(program2) {
12966
14973
  }
12967
14974
 
12968
14975
  // src/services/alation/commands.ts
12969
- import fs8 from "fs";
14976
+ import fs12 from "fs";
12970
14977
  var MAX_PAGE = 1e3;
12971
14978
  function parseIntOption(value, flag) {
12972
14979
  if (value === void 0) return void 0;
@@ -13079,8 +15086,8 @@ function getHttp6(program2) {
13079
15086
  Boolean(opts.dryRun)
13080
15087
  );
13081
15088
  }
13082
- async function getOne(http, path11, id, what) {
13083
- 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 } });
13084
15091
  const row = Array.isArray(rows) ? rows.find((r) => r.id === id) ?? rows[0] : void 0;
13085
15092
  if (!row) throw new PncliError(`${what} ${id} not found`, 404);
13086
15093
  return row;
@@ -13130,7 +15137,7 @@ function registerAlationCommands(program2) {
13130
15137
  const opts = program2.optsWithGlobals();
13131
15138
  const config = loadConfig({ configPath: opts.config });
13132
15139
  if (opts.dryRun) {
13133
- 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)
13134
15141
  `);
13135
15142
  process.exitCode = ExitCode.SUCCESS;
13136
15143
  throw new PncliError("dry-run", 0);
@@ -13437,17 +15444,22 @@ Services:
13437
15444
  splitio Split.IO (feature flags, Change Requests)
13438
15445
  figma Figma (files, comments, version history)
13439
15446
  alation Alation (data sources, schemas, tables, columns, search, Document Hubs)
13440
- config Manage pncli configuration
13441
- 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)
13442
15449
  skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)
13443
15450
  jwt JWT token utilities (decode header and payload)
13444
15451
 
13445
15452
  Agent skills and org plugins:
13446
15453
  pncli skills install --all-agents Bundled pncli skills into this repo, every agent host
13447
15454
  pncli skills marketplace add <git-url> --all-agents Org plugins (skills + AGENTS.md/CLAUDE.md) from a git marketplace
13448
- pncli skills marketplace sync --marketplace all Pull and refresh everything installed from marketplaces
15455
+ pncli skills marketplace sync Pull and refresh everything installed from marketplaces
13449
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
13450
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)
13451
15463
  `);
13452
15464
  program.parseAsync(process.argv).catch((err) => {
13453
15465
  if (process.exitCode !== void 0) return;