@kolatts/pncli 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunk-L6OY2546.js +984 -0
- package/dist/chunk-L6OY2546.js.map +1 -0
- package/dist/cli.js +2396 -385
- package/dist/cli.js.map +1 -1
- package/dist/{config-Y6IE6GY7.js → config-OH2O5E3P.js} +4 -2
- package/package.json +1 -1
- package/skills/pncli/SKILL.md +7 -2
- package/skills/pncli/ado.md +4 -0
- package/skills/pncli/bitbucket.md +4 -0
- package/skills/pncli/github.md +37 -0
- package/skills/pncli/jira.md +45 -1
- package/skills/pncli/marketplace.md +53 -1
- package/skills/pncli/skills-guide.md +169 -0
- package/dist/chunk-QHJ2HM5J.js +0 -466
- package/dist/chunk-QHJ2HM5J.js.map +0 -1
- /package/dist/{config-Y6IE6GY7.js.map → config-OH2O5E3P.js.map} +0 -0
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-
|
|
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
|
|
468
|
+
let path14 = remoteUrl;
|
|
451
469
|
let remoteHostname = "";
|
|
452
|
-
const sshProtoMatch =
|
|
470
|
+
const sshProtoMatch = path14.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
|
|
453
471
|
if (sshProtoMatch) {
|
|
454
472
|
remoteHostname = sshProtoMatch[1].toLowerCase();
|
|
455
|
-
|
|
473
|
+
path14 = sshProtoMatch[2];
|
|
456
474
|
} else {
|
|
457
|
-
const gitAtMatch =
|
|
475
|
+
const gitAtMatch = path14.match(/^git@([^:]+)(?::\d+)?:(.*)/);
|
|
458
476
|
if (gitAtMatch) {
|
|
459
477
|
remoteHostname = gitAtMatch[1].toLowerCase();
|
|
460
|
-
|
|
478
|
+
path14 = "/" + gitAtMatch[2];
|
|
461
479
|
} else {
|
|
462
|
-
const httpsMatch =
|
|
480
|
+
const httpsMatch = path14.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
|
|
463
481
|
if (httpsMatch) {
|
|
464
482
|
remoteHostname = httpsMatch[1].toLowerCase();
|
|
465
|
-
|
|
483
|
+
path14 = httpsMatch[2];
|
|
466
484
|
}
|
|
467
485
|
}
|
|
468
486
|
}
|
|
469
487
|
if (!remoteHostname || remoteHostname !== baseHostname) return null;
|
|
470
|
-
|
|
471
|
-
const gitIdx =
|
|
472
|
-
const sshIdx =
|
|
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(
|
|
493
|
+
const repo = decodeURIComponent(path14.slice(markerIdx + 6));
|
|
476
494
|
if (!repo) return null;
|
|
477
|
-
const before =
|
|
495
|
+
const before = path14.slice(1, markerIdx);
|
|
478
496
|
const parts = before.split("/").filter(Boolean);
|
|
479
497
|
if (parts.length < 2) return null;
|
|
480
498
|
const project = decodeURIComponent(parts[parts.length - 1]);
|
|
@@ -817,7 +835,936 @@ var BitbucketClient = class {
|
|
|
817
835
|
}
|
|
818
836
|
};
|
|
819
837
|
|
|
838
|
+
// src/services/git/credentials.ts
|
|
839
|
+
import fs from "fs";
|
|
840
|
+
import os from "os";
|
|
841
|
+
import path from "path";
|
|
842
|
+
|
|
843
|
+
// src/services/skills/git-auth.ts
|
|
844
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
845
|
+
var HELPER_COMMAND = "!pncli skills git-credential";
|
|
846
|
+
var HELPER_NAME_PATTERN = /^[^'\r\n]+$/;
|
|
847
|
+
function helperCommandFor(marketplaceName, configPath) {
|
|
848
|
+
let command = HELPER_COMMAND;
|
|
849
|
+
if (configPath !== void 0) {
|
|
850
|
+
if (!HELPER_NAME_PATTERN.test(configPath)) {
|
|
851
|
+
throw new Error(`Config path ${JSON.stringify(configPath)} cannot be used in a git credential helper (no quotes or newlines).`);
|
|
852
|
+
}
|
|
853
|
+
command += ` --config '${configPath}'`;
|
|
854
|
+
}
|
|
855
|
+
if (marketplaceName === void 0) return command;
|
|
856
|
+
if (!HELPER_NAME_PATTERN.test(marketplaceName)) {
|
|
857
|
+
throw new Error(`Marketplace name ${JSON.stringify(marketplaceName)} cannot be used in a git credential helper (no quotes or newlines). Re-add it with a simpler --name.`);
|
|
858
|
+
}
|
|
859
|
+
return `${command} --marketplace '${marketplaceName}'`;
|
|
860
|
+
}
|
|
861
|
+
function isPncliHelperValue(value) {
|
|
862
|
+
return value.trim().startsWith(HELPER_COMMAND);
|
|
863
|
+
}
|
|
864
|
+
function helperConfigKey(scope) {
|
|
865
|
+
return `credential.${scope}.helper`;
|
|
866
|
+
}
|
|
867
|
+
function useHttpPathKey(scope) {
|
|
868
|
+
return `credential.${scope}.useHttpPath`;
|
|
869
|
+
}
|
|
870
|
+
function gitUsernameForHost(host) {
|
|
871
|
+
return host === "github.com" ? "x-access-token" : "x-token-auth";
|
|
872
|
+
}
|
|
873
|
+
function gitHostFromApiBaseUrl(baseUrl) {
|
|
874
|
+
if (!baseUrl) return null;
|
|
875
|
+
try {
|
|
876
|
+
const host = new URL(baseUrl).host.toLowerCase();
|
|
877
|
+
return host === "api.github.com" ? "github.com" : host;
|
|
878
|
+
} catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function apiBaseForHost(host, githubBaseUrl) {
|
|
883
|
+
if (host === "github.com") return "https://api.github.com";
|
|
884
|
+
if (githubBaseUrl && gitHostFromApiBaseUrl(githubBaseUrl) === host) return githubBaseUrl.replace(/\/+$/, "");
|
|
885
|
+
return `https://${host}/api/v3`;
|
|
886
|
+
}
|
|
887
|
+
function hostOf(url) {
|
|
888
|
+
if (!url) return null;
|
|
889
|
+
try {
|
|
890
|
+
const u = new URL(url);
|
|
891
|
+
return u.protocol === "https:" ? u.host.toLowerCase() : null;
|
|
892
|
+
} catch {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function httpHostOf(url) {
|
|
897
|
+
if (!url) return null;
|
|
898
|
+
try {
|
|
899
|
+
const u = new URL(url);
|
|
900
|
+
return u.protocol === "https:" || u.protocol === "http:" ? u.host.toLowerCase() : null;
|
|
901
|
+
} catch {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function hostnameOf(url) {
|
|
906
|
+
if (!url) return null;
|
|
907
|
+
try {
|
|
908
|
+
return new URL(url).hostname.toLowerCase();
|
|
909
|
+
} catch {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function normalizeRepoPath(p) {
|
|
914
|
+
return p.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
|
|
915
|
+
}
|
|
916
|
+
function classifyGitHubToken(token) {
|
|
917
|
+
if (token.startsWith("ghp_")) return "classic";
|
|
918
|
+
if (token.startsWith("github_pat_")) return "fine-grained";
|
|
919
|
+
if (token.startsWith("gho_")) return "oauth";
|
|
920
|
+
if (token.startsWith("ghu_") || token.startsWith("ghs_")) return "github-app";
|
|
921
|
+
if (/^[0-9a-f]{40}$/i.test(token)) return "legacy-hex";
|
|
922
|
+
return "unknown";
|
|
923
|
+
}
|
|
924
|
+
function allMarketplaces(globalConfig) {
|
|
925
|
+
return [...globalConfig.marketplaces ?? [], ...globalConfig.marketplace ? [globalConfig.marketplace] : []];
|
|
926
|
+
}
|
|
927
|
+
var PROVIDERS = ["github", "bitbucket", "ado", "git"];
|
|
928
|
+
function detectProvider(repoUrl, cfg, explicit) {
|
|
929
|
+
if (explicit && PROVIDERS.includes(explicit)) return explicit;
|
|
930
|
+
const hostname = hostnameOf(repoUrl);
|
|
931
|
+
if (!hostname) return "git";
|
|
932
|
+
const path14 = (() => {
|
|
933
|
+
try {
|
|
934
|
+
return new URL(repoUrl).pathname;
|
|
935
|
+
} catch {
|
|
936
|
+
return "";
|
|
937
|
+
}
|
|
938
|
+
})();
|
|
939
|
+
const githubHost = hostnameOf(cfg.github?.baseUrl);
|
|
940
|
+
if (hostname === "github.com" || githubHost && (hostname === githubHost || githubHost === `api.${hostname}`)) return "github";
|
|
941
|
+
if (hostname === hostnameOf(cfg.ado?.baseUrl) || /\/_git\//.test(path14) || hostname === "dev.azure.com" || hostname.endsWith(".visualstudio.com")) return "ado";
|
|
942
|
+
if (hostname === hostnameOf(cfg.bitbucket?.baseUrl) || /(^|\/)scm\//.test(path14) || hostname === "bitbucket.org") return "bitbucket";
|
|
943
|
+
return "git";
|
|
944
|
+
}
|
|
945
|
+
function providerFallbackToken(provider, repoUrl, cfg) {
|
|
946
|
+
const hostname = hostnameOf(repoUrl);
|
|
947
|
+
if (!hostname) return null;
|
|
948
|
+
if (provider === "github") {
|
|
949
|
+
const githubHost = hostnameOf(cfg.github?.baseUrl);
|
|
950
|
+
const isGitHubHost2 = hostname === "github.com" || !!githubHost && githubHost === hostname;
|
|
951
|
+
return isGitHubHost2 && cfg.github?.token ? { token: cfg.github.token, source: "github.token" } : null;
|
|
952
|
+
}
|
|
953
|
+
if (provider === "bitbucket") {
|
|
954
|
+
return hostname === hostnameOf(cfg.bitbucket?.baseUrl) && cfg.bitbucket?.pat ? { token: cfg.bitbucket.pat, source: "bitbucket.pat" } : null;
|
|
955
|
+
}
|
|
956
|
+
if (provider === "ado") {
|
|
957
|
+
return hostname === hostnameOf(cfg.ado?.baseUrl) && cfg.ado?.pat ? { token: cfg.ado.pat, source: "ado.pat" } : null;
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
var UnresolvedMarketplaceTokenError = class extends Error {
|
|
962
|
+
};
|
|
963
|
+
function marketplaceLabelOf(m) {
|
|
964
|
+
return m.name ?? m.repoUrl ?? "(unnamed)";
|
|
965
|
+
}
|
|
966
|
+
function resolveMarketplaceAuth(m, cfg, resolveSecret = resolveSecretValue) {
|
|
967
|
+
const host = httpHostOf(m.repoUrl);
|
|
968
|
+
if (!host) return null;
|
|
969
|
+
const provider = detectProvider(m.repoUrl, cfg, m.provider);
|
|
970
|
+
const username = m.username || gitUsernameForHost(host);
|
|
971
|
+
if (m.token) {
|
|
972
|
+
const secret = resolveSecret(m.token);
|
|
973
|
+
if (!secret && isKeychainRef(m.token)) {
|
|
974
|
+
throw new UnresolvedMarketplaceTokenError(`The token for marketplace "${marketplaceLabelOf(m)}" is stored in the OS keychain as "${m.token}", but it could not be read. Check with: pncli config keychain status`);
|
|
975
|
+
}
|
|
976
|
+
if (secret) return { username, password: secret, source: `marketplace:${marketplaceLabelOf(m)}`, provider };
|
|
977
|
+
}
|
|
978
|
+
const fallback = providerFallbackToken(provider, m.repoUrl, cfg);
|
|
979
|
+
return fallback ? { username, password: fallback.token, source: fallback.source, provider } : null;
|
|
980
|
+
}
|
|
981
|
+
function parseCredentialRequest(input3) {
|
|
982
|
+
const out = {};
|
|
983
|
+
for (const line of input3.split(/\r?\n/)) {
|
|
984
|
+
if (line === "") break;
|
|
985
|
+
const eq = line.indexOf("=");
|
|
986
|
+
if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
function resolveCredential(request, globalConfig, cfg, resolveSecret = resolveSecretValue, marketplaceName) {
|
|
991
|
+
if (request.protocol !== "https" || !request.host) return null;
|
|
992
|
+
const host = request.host.toLowerCase().replace(/:443$/, "");
|
|
993
|
+
const marketplaces = allMarketplaces(globalConfig).filter((m) => hostOf(m.repoUrl) === host);
|
|
994
|
+
if (marketplaceName !== void 0) {
|
|
995
|
+
const m = marketplaces.find((x) => x.name === marketplaceName || x.repoUrl === marketplaceName);
|
|
996
|
+
if (!m) return null;
|
|
997
|
+
const auth = resolveMarketplaceAuth(m, cfg, resolveSecret);
|
|
998
|
+
return auth ? { username: auth.username, password: auth.password, source: auth.source } : null;
|
|
999
|
+
}
|
|
1000
|
+
if (request.path) {
|
|
1001
|
+
const wanted = normalizeRepoPath(request.path);
|
|
1002
|
+
const exact = marketplaces.find((m) => normalizeRepoPath(new URL(m.repoUrl).pathname) === wanted);
|
|
1003
|
+
if (exact) {
|
|
1004
|
+
const auth = resolveMarketplaceAuth(exact, cfg, resolveSecret);
|
|
1005
|
+
if (auth) return { username: auth.username, password: auth.password, source: auth.source };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const url = `https://${host}/`;
|
|
1009
|
+
for (const provider of ["github", "bitbucket", "ado"]) {
|
|
1010
|
+
const fallback = providerFallbackToken(provider, url, cfg);
|
|
1011
|
+
if (fallback) return { username: gitUsernameForHost(host), password: fallback.token, source: fallback.source };
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
function formatCredentialAnswer(answer) {
|
|
1016
|
+
return `username=${answer.username}
|
|
1017
|
+
password=${answer.password}
|
|
1018
|
+
`;
|
|
1019
|
+
}
|
|
1020
|
+
function parseScope(scope) {
|
|
1021
|
+
const u = new URL(scope);
|
|
1022
|
+
const path14 = u.pathname.replace(/^\/+/, "");
|
|
1023
|
+
return { scope, host: u.host.toLowerCase(), path: path14 || null };
|
|
1024
|
+
}
|
|
1025
|
+
function hostScope(host) {
|
|
1026
|
+
return `https://${host}`;
|
|
1027
|
+
}
|
|
1028
|
+
function marketplaceScopes(repoUrl, provider) {
|
|
1029
|
+
const u = new URL(repoUrl);
|
|
1030
|
+
u.username = "";
|
|
1031
|
+
u.password = "";
|
|
1032
|
+
u.search = "";
|
|
1033
|
+
u.hash = "";
|
|
1034
|
+
const base = `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
|
|
1035
|
+
if (provider === "ado") return [base];
|
|
1036
|
+
const toggled = /\.git$/i.test(base) ? base.replace(/\.git$/i, "") : `${base}.git`;
|
|
1037
|
+
return [base, toggled];
|
|
1038
|
+
}
|
|
1039
|
+
var defaultGitRunner = (args, opts) => {
|
|
1040
|
+
const r = spawnSync("git", args, {
|
|
1041
|
+
input: opts?.input,
|
|
1042
|
+
encoding: "utf8",
|
|
1043
|
+
windowsHide: true,
|
|
1044
|
+
timeout: opts?.timeoutMs ?? 3e4,
|
|
1045
|
+
env: { ...process.env, ...opts?.env }
|
|
1046
|
+
});
|
|
1047
|
+
if (r.error) throw r.error;
|
|
1048
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1049
|
+
};
|
|
1050
|
+
var NON_INTERACTIVE_ENV = {
|
|
1051
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1052
|
+
GCM_INTERACTIVE: "never",
|
|
1053
|
+
GIT_ASKPASS: "",
|
|
1054
|
+
SSH_ASKPASS: ""
|
|
1055
|
+
};
|
|
1056
|
+
function readHelperEntries(scope, git = defaultGitRunner) {
|
|
1057
|
+
const r = git(["config", "--global", "--get-all", helperConfigKey(scope)]);
|
|
1058
|
+
return r.status === 0 ? r.stdout.split(/\r?\n/).filter((l, i, a) => i < a.length - 1 || l !== "") : [];
|
|
1059
|
+
}
|
|
1060
|
+
function readGlobalHelpers(git = defaultGitRunner) {
|
|
1061
|
+
const r = git(["config", "--get-all", "credential.helper"]);
|
|
1062
|
+
return r.status === 0 ? r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : [];
|
|
1063
|
+
}
|
|
1064
|
+
function readUseHttpPath(scope, git) {
|
|
1065
|
+
const r = git(["config", "--global", "--get", useHttpPathKey(scope)]);
|
|
1066
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
1067
|
+
}
|
|
1068
|
+
function afterLastReset(entries) {
|
|
1069
|
+
const own = entries.filter((e) => !isPncliHelperValue(e));
|
|
1070
|
+
return own.slice(own.lastIndexOf("") + 1).filter((e) => e.trim() !== "");
|
|
1071
|
+
}
|
|
1072
|
+
function effectiveFallbackHelpers(scopeEntries, genericHelpers, hostEntries = []) {
|
|
1073
|
+
for (const entries of [scopeEntries, hostEntries]) {
|
|
1074
|
+
if (entries.some((e) => !isPncliHelperValue(e))) return afterLastReset(entries);
|
|
1075
|
+
}
|
|
1076
|
+
return genericHelpers.filter((h) => h.trim() !== "" && !isPncliHelperValue(h));
|
|
1077
|
+
}
|
|
1078
|
+
function enableHelper(scope, helperValue, git = defaultGitRunner) {
|
|
1079
|
+
const { host, path: path14 } = parseScope(scope);
|
|
1080
|
+
const previousHelpers = readHelperEntries(scope, git).filter((e) => !isPncliHelperValue(e));
|
|
1081
|
+
const previousUseHttpPath = readUseHttpPath(scope, git);
|
|
1082
|
+
const hostEntries = path14 ? readHelperEntries(hostScope(host), git) : [];
|
|
1083
|
+
const fallbacks = effectiveFallbackHelpers(previousHelpers, readGlobalHelpers(git), hostEntries);
|
|
1084
|
+
git(["config", "--global", "--unset-all", helperConfigKey(scope)]);
|
|
1085
|
+
for (const value of ["", helperValue, ...fallbacks]) {
|
|
1086
|
+
const r = git(["config", "--global", "--add", helperConfigKey(scope), value]);
|
|
1087
|
+
if (r.status !== 0) throw new Error(`git config failed: ${r.stderr.trim()}`);
|
|
1088
|
+
}
|
|
1089
|
+
return { previousHelpers, previousUseHttpPath };
|
|
1090
|
+
}
|
|
1091
|
+
function restoreUseHttpPath(scope, previous, git) {
|
|
1092
|
+
if (previous === null) git(["config", "--global", "--unset", useHttpPathKey(scope)]);
|
|
1093
|
+
else git(["config", "--global", useHttpPathKey(scope), previous]);
|
|
1094
|
+
}
|
|
1095
|
+
function disableHelper(scope, backup, git = defaultGitRunner) {
|
|
1096
|
+
const current = readHelperEntries(scope, git);
|
|
1097
|
+
if (!current.some(isPncliHelperValue)) return false;
|
|
1098
|
+
const restore = backup ? backup.previousHelpers : current.slice(current.findIndex(isPncliHelperValue) + 1);
|
|
1099
|
+
git(["config", "--global", "--unset-all", helperConfigKey(scope)]);
|
|
1100
|
+
for (const value of restore) git(["config", "--global", "--add", helperConfigKey(scope), value]);
|
|
1101
|
+
if (backup) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
function inlineCredentialArgs(username, token) {
|
|
1105
|
+
const helper = `credential.helper=!f() { test "$1" = get || exit 0; printf '%s\\n' "username=$PNCLI_GIT_USERNAME" "password=$PNCLI_GIT_PASSWORD"; }; f`;
|
|
1106
|
+
return {
|
|
1107
|
+
args: ["-c", "credential.helper=", "-c", helper],
|
|
1108
|
+
env: { PNCLI_GIT_USERNAME: username, PNCLI_GIT_PASSWORD: token }
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function credentialInput(host, fields = {}) {
|
|
1112
|
+
return Object.entries({ protocol: "https", host, ...fields }).map(([k, v]) => `${k}=${v}`).join("\n") + "\n\n";
|
|
1113
|
+
}
|
|
1114
|
+
function keychainStore(scope, username, token, git = defaultGitRunner) {
|
|
1115
|
+
const helpers = readGlobalHelpers(git);
|
|
1116
|
+
if (helpers.length === 0) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
"git has no credential.helper configured, so there is no OS keychain to store into. Configure one first \u2014 Windows: `git config --global credential.helper manager` (ships with Git for Windows); macOS: `git config --global credential.helper osxkeychain`; Linux: git-credential-libsecret. Or use --mode helper, which needs no keychain."
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const { host, path: path14 } = parseScope(scope);
|
|
1122
|
+
const backup = { previousHelpers: [], previousUseHttpPath: readUseHttpPath(scope, git) };
|
|
1123
|
+
if (path14) git(["config", "--global", useHttpPathKey(scope), "true"]);
|
|
1124
|
+
const r = git(["credential", "approve"], { input: credentialInput(host, { ...path14 ? { path: path14 } : {}, username, password: token }), env: NON_INTERACTIVE_ENV });
|
|
1125
|
+
if (r.status !== 0) {
|
|
1126
|
+
if (path14) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
|
|
1127
|
+
throw new Error(`git credential approve failed: ${r.stderr.trim()}`);
|
|
1128
|
+
}
|
|
1129
|
+
return { helpers, plaintextStore: helpers.some((h) => /^store\b/.test(h)), backup };
|
|
1130
|
+
}
|
|
1131
|
+
function keychainErase(scope, username, git = defaultGitRunner) {
|
|
1132
|
+
const { host, path: path14 } = parseScope(scope);
|
|
1133
|
+
git(["credential", "reject"], { input: credentialInput(host, { ...path14 ? { path: path14 } : {}, username }), env: NON_INTERACTIVE_ENV });
|
|
1134
|
+
}
|
|
1135
|
+
function disableScope(scope, record, forgetKeychain, git = defaultGitRunner) {
|
|
1136
|
+
const backup = record ? { previousHelpers: record.previousHelpers ?? [], previousUseHttpPath: record.previousUseHttpPath ?? null } : void 0;
|
|
1137
|
+
const helperRemoved = disableHelper(scope, record?.mode === "helper" ? backup : void 0, git);
|
|
1138
|
+
let keychainErased = false;
|
|
1139
|
+
if (record?.mode === "keychain") {
|
|
1140
|
+
if (forgetKeychain) {
|
|
1141
|
+
keychainErase(scope, record.username ?? gitUsernameForHost(parseScope(scope).host), git);
|
|
1142
|
+
keychainErased = true;
|
|
1143
|
+
}
|
|
1144
|
+
keychainRestoreScope(scope, backup, git);
|
|
1145
|
+
}
|
|
1146
|
+
return { helperRemoved, keychainErased };
|
|
1147
|
+
}
|
|
1148
|
+
function keychainRestoreScope(scope, backup, git = defaultGitRunner) {
|
|
1149
|
+
if (parseScope(scope).path) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
|
|
1150
|
+
}
|
|
1151
|
+
function credentialFill(host, path14, git = defaultGitRunner) {
|
|
1152
|
+
try {
|
|
1153
|
+
const r = git(["credential", "fill"], { input: credentialInput(host, path14 ? { path: path14 } : {}), env: NON_INTERACTIVE_ENV });
|
|
1154
|
+
if (r.status !== 0) return null;
|
|
1155
|
+
const parsed = parseCredentialRequest(r.stdout);
|
|
1156
|
+
return parsed.password ? { username: parsed.username ?? "", password: parsed.password } : null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function lsRemote(repoUrl, auth, git = defaultGitRunner, opts = {}) {
|
|
1162
|
+
const inline = auth ? inlineCredentialArgs(auth.username, auth.password) : { args: opts.keepHelpers ? [] : ["-c", "credential.helper="], env: {} };
|
|
1163
|
+
try {
|
|
1164
|
+
const r = git([...inline.args, "ls-remote", "--heads", repoUrl], { env: { ...NON_INTERACTIVE_ENV, ...inline.env }, timeoutMs: 2e4 });
|
|
1165
|
+
if (r.status === 0) return { ok: true, error: null };
|
|
1166
|
+
const scrub2 = (s) => auth ? s.split(auth.password).join("***") : s;
|
|
1167
|
+
return { ok: false, error: scrub2(r.stderr.trim().split(/\r?\n/).slice(-2).join(" ")) || `git exited with ${r.status}` };
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function originHasCredentials(repoPath) {
|
|
1173
|
+
try {
|
|
1174
|
+
const url = execFileSync2("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: "pipe" }).trim();
|
|
1175
|
+
const u = new URL(url);
|
|
1176
|
+
return !!u.password;
|
|
1177
|
+
} catch {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async function inspectGitHubToken(apiBase, token, fetchFn = fetch, now = /* @__PURE__ */ new Date()) {
|
|
1182
|
+
const kind = classifyGitHubToken(token);
|
|
1183
|
+
const base = {
|
|
1184
|
+
kind,
|
|
1185
|
+
status: null,
|
|
1186
|
+
valid: false,
|
|
1187
|
+
login: null,
|
|
1188
|
+
scopes: null,
|
|
1189
|
+
missingRepoScope: false,
|
|
1190
|
+
expiresAt: null,
|
|
1191
|
+
expiresInDays: null,
|
|
1192
|
+
ssoAuthorizationRequired: false,
|
|
1193
|
+
error: null
|
|
1194
|
+
};
|
|
1195
|
+
let res;
|
|
1196
|
+
try {
|
|
1197
|
+
res = await fetchFn(`${apiBase.replace(/\/+$/, "")}/user`, {
|
|
1198
|
+
headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "pncli" },
|
|
1199
|
+
signal: AbortSignal.timeout(1e4)
|
|
1200
|
+
});
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
return { ...base, error: err instanceof Error ? err.message : String(err) };
|
|
1203
|
+
}
|
|
1204
|
+
const scopesHeader = res.headers.get("x-oauth-scopes");
|
|
1205
|
+
const scopes = scopesHeader === null ? null : scopesHeader.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1206
|
+
const expiryHeader = res.headers.get("github-authentication-token-expiration");
|
|
1207
|
+
let expiresAt = null;
|
|
1208
|
+
let expiresInDays = null;
|
|
1209
|
+
if (expiryHeader) {
|
|
1210
|
+
const t = Date.parse(expiryHeader.replace(/ UTC$/, "Z").replace(" ", "T").replace(" ", ""));
|
|
1211
|
+
if (!Number.isNaN(t)) {
|
|
1212
|
+
expiresAt = new Date(t).toISOString();
|
|
1213
|
+
expiresInDays = Math.floor((t - now.getTime()) / 864e5);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
let login = null;
|
|
1217
|
+
if (res.ok) {
|
|
1218
|
+
try {
|
|
1219
|
+
login = (await res.json()).login ?? null;
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const classicLike = kind === "classic" || kind === "legacy-hex";
|
|
1224
|
+
return {
|
|
1225
|
+
...base,
|
|
1226
|
+
status: res.status,
|
|
1227
|
+
valid: res.ok,
|
|
1228
|
+
login,
|
|
1229
|
+
scopes,
|
|
1230
|
+
missingRepoScope: res.ok && classicLike && scopes !== null && !scopes.includes("repo"),
|
|
1231
|
+
expiresAt,
|
|
1232
|
+
expiresInDays,
|
|
1233
|
+
ssoAuthorizationRequired: res.status === 403 && res.headers.has("x-github-sso"),
|
|
1234
|
+
error: res.ok ? null : `GET /user returned HTTP ${res.status}`
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/services/git/credentials.ts
|
|
1239
|
+
function fingerprint(token) {
|
|
1240
|
+
const prefix = /^(github_pat_|gh[pousr]_)/.exec(token)?.[1] ?? "";
|
|
1241
|
+
return token.length <= 8 ? "\u2026" : `${prefix}\u2026${token.slice(-4)}`;
|
|
1242
|
+
}
|
|
1243
|
+
function looksLikeToken(value) {
|
|
1244
|
+
return /^(github_pat_|gh[pousr]_)/.test(value) || /^[0-9a-f]{40}$/i.test(value) || value.length >= 30 && !/[@\s]/.test(value);
|
|
1245
|
+
}
|
|
1246
|
+
function tokenFromUrl(url) {
|
|
1247
|
+
try {
|
|
1248
|
+
const u = new URL(url);
|
|
1249
|
+
const user = decodeURIComponent(u.username);
|
|
1250
|
+
const pass = decodeURIComponent(u.password);
|
|
1251
|
+
if (pass) return { username: user, token: pass };
|
|
1252
|
+
if (user && looksLikeToken(user)) return { username: "", token: user };
|
|
1253
|
+
return null;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function redactUrl(url) {
|
|
1259
|
+
try {
|
|
1260
|
+
const u = new URL(url);
|
|
1261
|
+
if (!u.username && !u.password) return url;
|
|
1262
|
+
const found = tokenFromUrl(url);
|
|
1263
|
+
const shown = found ? found.username ? `${found.username}:${fingerprint(found.token)}` : fingerprint(found.token) : "***";
|
|
1264
|
+
return `${u.protocol}//${shown}@${u.host}${u.pathname}${u.search}`;
|
|
1265
|
+
} catch {
|
|
1266
|
+
return url.replace(/\/\/[^/@]+@/, "//***@");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function redactSecrets(text) {
|
|
1270
|
+
return text.replace(/(github_pat_|gh[pousr]_)[A-Za-z0-9_]{8,}/g, (m) => fingerprint(m)).replace(/\b[0-9a-f]{40}\b/gi, (m) => fingerprint(m)).replace(/(password=)([^\s;'"]+)/gi, (m, k, v) => v.includes("\u2026") ? m : `${k}${fingerprint(v)}`).replace(/(\/\/)([^/@\s:]+):([^/@\s]+)@/g, (_m, s, u, p) => `${s}${u}:${fingerprint(p)}@`);
|
|
1271
|
+
}
|
|
1272
|
+
function scrub(text, secret) {
|
|
1273
|
+
return secret ? text.split(secret).join(fingerprint(secret)) : text;
|
|
1274
|
+
}
|
|
1275
|
+
function parseInsteadOf(output) {
|
|
1276
|
+
const mappings = [];
|
|
1277
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1278
|
+
const m = /^url\.(.+)\.insteadof (.+)$/i.exec(line.trim());
|
|
1279
|
+
if (m) mappings.push({ from: m[2], to: redactUrl(m[1]), rawTo: m[1] });
|
|
1280
|
+
}
|
|
1281
|
+
return mappings;
|
|
1282
|
+
}
|
|
1283
|
+
function matchMapping(url, mappings) {
|
|
1284
|
+
let best = null;
|
|
1285
|
+
for (const m of mappings) {
|
|
1286
|
+
if (url.startsWith(m.from) && (!best || m.from.length > best.from.length)) best = m;
|
|
1287
|
+
}
|
|
1288
|
+
return best;
|
|
1289
|
+
}
|
|
1290
|
+
function parseScopedHelpers(output) {
|
|
1291
|
+
const out = [];
|
|
1292
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1293
|
+
const m = /^credential\.(.+)\.helper(?: (.*))?$/i.exec(line.trim());
|
|
1294
|
+
if (m) out.push({ scope: m[1], helper: m[2] ?? "" });
|
|
1295
|
+
}
|
|
1296
|
+
return out;
|
|
1297
|
+
}
|
|
1298
|
+
function githubRepoSlug(url) {
|
|
1299
|
+
try {
|
|
1300
|
+
const parts = new URL(url).pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
|
|
1301
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async function checkGitHubRepoAccess(apiBase, slug, token, fetchFn = fetch) {
|
|
1307
|
+
try {
|
|
1308
|
+
const res = await fetchFn(`${apiBase.replace(/\/+$/, "")}/repos/${slug}`, {
|
|
1309
|
+
headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "pncli" },
|
|
1310
|
+
signal: AbortSignal.timeout(1e4)
|
|
1311
|
+
});
|
|
1312
|
+
return {
|
|
1313
|
+
status: res.status,
|
|
1314
|
+
canRead: res.ok,
|
|
1315
|
+
ssoAuthorizationRequired: res.status === 403 && res.headers.has("x-github-sso"),
|
|
1316
|
+
error: res.ok ? null : `GET /repos/${slug} returned HTTP ${res.status}`
|
|
1317
|
+
};
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
return { status: null, canRead: false, ssoAuthorizationRequired: false, error: err instanceof Error ? err.message : String(err) };
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function gitIn(cwd, git) {
|
|
1323
|
+
return cwd ? (args, opts) => git(["-C", cwd, ...args], opts) : git;
|
|
1324
|
+
}
|
|
1325
|
+
async function inspectRemoteAuth(remote, url, cwd, cfg, deps = {}) {
|
|
1326
|
+
const baseGit = deps.git ?? defaultGitRunner;
|
|
1327
|
+
const git = gitIn(cwd, baseGit);
|
|
1328
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1329
|
+
const effRaw = (() => {
|
|
1330
|
+
const r = git(["ls-remote", "--get-url", url]);
|
|
1331
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : url;
|
|
1332
|
+
})();
|
|
1333
|
+
const mappingRaw = matchMapping(url, parseInsteadOf(git(["config", "--get-regexp", "^url\\..*\\.insteadof$"]).stdout));
|
|
1334
|
+
let host = null;
|
|
1335
|
+
let repoPath = null;
|
|
1336
|
+
try {
|
|
1337
|
+
const u = new URL(effRaw);
|
|
1338
|
+
if (u.protocol === "https:" || u.protocol === "http:") {
|
|
1339
|
+
host = u.host.toLowerCase();
|
|
1340
|
+
repoPath = u.pathname.replace(/^\/+/, "");
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
const plainEffective = (() => {
|
|
1345
|
+
try {
|
|
1346
|
+
const u = new URL(effRaw);
|
|
1347
|
+
u.username = "";
|
|
1348
|
+
u.password = "";
|
|
1349
|
+
return u.toString();
|
|
1350
|
+
} catch {
|
|
1351
|
+
return effRaw;
|
|
1352
|
+
}
|
|
1353
|
+
})();
|
|
1354
|
+
const provider = detectProvider(plainEffective, cfg);
|
|
1355
|
+
let source = "none";
|
|
1356
|
+
let credential = null;
|
|
1357
|
+
let helper = null;
|
|
1358
|
+
const embedded = tokenFromUrl(effRaw);
|
|
1359
|
+
if (embedded) {
|
|
1360
|
+
credential = embedded;
|
|
1361
|
+
source = mappingRaw && tokenFromUrl(mappingRaw.rawTo) ? "insteadOf" : "url";
|
|
1362
|
+
} else if (host) {
|
|
1363
|
+
const filled = credentialFill(host, repoPath ?? void 0, git);
|
|
1364
|
+
if (filled) {
|
|
1365
|
+
credential = { username: filled.username, token: filled.password };
|
|
1366
|
+
source = "helper";
|
|
1367
|
+
const h = git(["config", "--get-urlmatch", "credential.helper", plainEffective]);
|
|
1368
|
+
helper = h.status === 0 ? redactSecrets(h.stdout.trim().split(/\r?\n/).pop() ?? "") || null : null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const report = {
|
|
1372
|
+
remote,
|
|
1373
|
+
url: redactUrl(url),
|
|
1374
|
+
effectiveUrl: redactUrl(effRaw),
|
|
1375
|
+
host,
|
|
1376
|
+
provider,
|
|
1377
|
+
mapping: mappingRaw ? { from: mappingRaw.from, to: mappingRaw.to } : null,
|
|
1378
|
+
credentialSource: source,
|
|
1379
|
+
helper,
|
|
1380
|
+
helperKind: helper ? classifyHelper(helper) : null,
|
|
1381
|
+
credentialStore: helper && classifyHelper(helper) === "git-credential-manager" ? gcmCredentialStore(git) : null,
|
|
1382
|
+
token: credential ? { kind: provider === "github" ? classifyGitHubToken(credential.token) : "unknown", fingerprint: fingerprint(credential.token), username: credential.username || null } : null,
|
|
1383
|
+
access: null,
|
|
1384
|
+
github: null,
|
|
1385
|
+
problems: []
|
|
1386
|
+
};
|
|
1387
|
+
if (host && !deps.offline) {
|
|
1388
|
+
const auth = source === "helper" && credential ? { username: credential.username, password: credential.token } : null;
|
|
1389
|
+
const r = lsRemote(remote && cwd ? remote : source === "helper" ? plainEffective : url, auth, git);
|
|
1390
|
+
report.access = { ok: r.ok, error: r.error ? scrub(r.error, credential?.token ?? null) : null };
|
|
1391
|
+
if (provider === "github" && credential) {
|
|
1392
|
+
const apiBase = apiBaseForHost(host, cfg.github?.baseUrl);
|
|
1393
|
+
const key = `${apiBase}\0${credential.token}`;
|
|
1394
|
+
if (!deps.cache?.has(key)) deps.cache?.set(key, await inspectGitHubToken(apiBase, credential.token, fetchFn));
|
|
1395
|
+
const tokenInfo = deps.cache?.get(key) ?? await inspectGitHubToken(apiBase, credential.token, fetchFn);
|
|
1396
|
+
const slug = githubRepoSlug(plainEffective);
|
|
1397
|
+
const repo = slug && tokenInfo.valid ? await checkGitHubRepoAccess(apiBase, slug, credential.token, fetchFn) : null;
|
|
1398
|
+
report.github = { token: tokenInfo, repo };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
report.problems = buildRemoteProblems(report, url);
|
|
1402
|
+
return report;
|
|
1403
|
+
}
|
|
1404
|
+
function buildRemoteProblems(r, originalUrl) {
|
|
1405
|
+
const problems = [];
|
|
1406
|
+
const where = r.credentialSource === "insteadOf" && r.mapping ? `the insteadOf mapping for ${r.mapping.from}` : r.credentialSource === "url" ? "the remote URL" : r.credentialSource === "helper" ? `the credential helper (${r.helper ?? "unknown"})` : "nothing";
|
|
1407
|
+
const org = (() => {
|
|
1408
|
+
try {
|
|
1409
|
+
const u = new URL(originalUrl);
|
|
1410
|
+
return `${u.protocol}//${u.host}/${u.pathname.split("/").filter(Boolean)[0] ?? ""}/`;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return originalUrl;
|
|
1413
|
+
}
|
|
1414
|
+
})();
|
|
1415
|
+
if (!r.host) return problems;
|
|
1416
|
+
if (r.credentialSource === "url") {
|
|
1417
|
+
problems.push({
|
|
1418
|
+
message: "The token is embedded in the remote URL, so it sits in plaintext in this clone's .git/config",
|
|
1419
|
+
fix: `Move it to a mapping or helper and reset the remote: git remote set-url ${r.remote ?? "origin"} ${r.url.replace(/\/\/[^@]+@/, "//")}`
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (r.access && !r.access.ok) {
|
|
1423
|
+
if (r.credentialSource === "none") {
|
|
1424
|
+
problems.push({
|
|
1425
|
+
message: `No credential is mapped for ${r.effectiveUrl} \u2014 git reached it anonymously and was refused (${r.access.error})`,
|
|
1426
|
+
fix: r.provider === "github" ? `Map a token for the org: git config --global url."https://<classic-PAT>@${r.host}/${org.split("/").slice(3).join("/")}".insteadOf "${org}" \u2014 or let pncli answer with github.token: pncli skills git-auth enable --host ${r.host}` : `Give git a credential for ${r.host} (a credential helper, or pncli skills git-auth enable --host ${r.host})`
|
|
1427
|
+
});
|
|
1428
|
+
} else if (!r.github) {
|
|
1429
|
+
problems.push({
|
|
1430
|
+
message: `The credential from ${where} (${r.token?.fingerprint}) cannot read ${r.effectiveUrl}: ${r.access.error}`,
|
|
1431
|
+
fix: "Check the token has not expired and its account can read this repo; replace it where it is mapped"
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const g = r.github;
|
|
1436
|
+
if (g) {
|
|
1437
|
+
const t = g.token;
|
|
1438
|
+
const who = `The token from ${where} (${r.token?.fingerprint}${t.login ? `, ${t.login}` : ""})`;
|
|
1439
|
+
if (t.status === 401) problems.push({ message: `${who} was rejected \u2014 expired or revoked`, fix: "Generate a new token and replace it where it is mapped" });
|
|
1440
|
+
if (t.ssoAuthorizationRequired || g.repo?.ssoAuthorizationRequired) {
|
|
1441
|
+
problems.push({ message: `${who} is not authorized for the organization's SAML SSO`, fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize for the org" });
|
|
1442
|
+
}
|
|
1443
|
+
if (t.missingRepoScope) problems.push({ message: `${who} is a classic token without the "repo" scope, so it cannot read private repositories`, fix: 'Edit the token (Settings \u2192 Developer settings \u2192 Tokens (classic)) and tick "repo"' });
|
|
1444
|
+
if (g.repo && !g.repo.canRead && !g.repo.ssoAuthorizationRequired && t.valid) {
|
|
1445
|
+
problems.push({
|
|
1446
|
+
message: `${who} is valid but cannot see ${githubRepoSlug(r.effectiveUrl.replace(/\/\/[^@]+@/, "//"))} (HTTP ${g.repo.status})`,
|
|
1447
|
+
fix: r.token?.kind === "fine-grained" ? "Add this repository to the fine-grained token's repository access, or map a different token for it" : "The token's account needs access to this repo (or the org may block classic tokens); map a token from an account that has access"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
if (t.expiresInDays !== null && t.expiresInDays <= 14) {
|
|
1451
|
+
problems.push({ message: t.expiresInDays < 0 ? `${who} expired on ${t.expiresAt}` : `${who} expires in ${t.expiresInDays} day(s)`, fix: "Regenerate it and replace it where it is mapped" });
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return problems;
|
|
1455
|
+
}
|
|
1456
|
+
function listRemotes(cwd, git = defaultGitRunner) {
|
|
1457
|
+
const r = git(["-C", cwd, "config", "--get-regexp", "^remote\\..*\\.url$"]);
|
|
1458
|
+
if (r.status !== 0) return [];
|
|
1459
|
+
return r.stdout.split(/\r?\n/).map((l) => /^remote\.(.+)\.url (.+)$/.exec(l.trim())).filter((m) => !!m).map((m) => [m[1], m[2]]);
|
|
1460
|
+
}
|
|
1461
|
+
function findClones(root, depth) {
|
|
1462
|
+
const found = [];
|
|
1463
|
+
const walk = (dir, level) => {
|
|
1464
|
+
if (fs.existsSync(path.join(dir, ".git"))) {
|
|
1465
|
+
found.push(dir);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (level >= depth) return;
|
|
1469
|
+
let entries;
|
|
1470
|
+
try {
|
|
1471
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1472
|
+
} catch {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
for (const e of entries) {
|
|
1476
|
+
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") walk(path.join(dir, e.name), level + 1);
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
walk(path.resolve(root), 0);
|
|
1480
|
+
return found;
|
|
1481
|
+
}
|
|
1482
|
+
async function listMappings(cwd, cfg, deps = {}) {
|
|
1483
|
+
const git = gitIn(cwd, deps.git ?? defaultGitRunner);
|
|
1484
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1485
|
+
const reports = [];
|
|
1486
|
+
for (const m of parseInsteadOf(git(["config", "--get-regexp", "^url\\..*\\.insteadof$"]).stdout)) {
|
|
1487
|
+
const embedded = tokenFromUrl(m.rawTo);
|
|
1488
|
+
let host = null;
|
|
1489
|
+
try {
|
|
1490
|
+
host = new URL(m.rawTo).host.toLowerCase();
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
const provider = detectProvider(m.rawTo.replace(/\/\/[^@]+@/, "//"), cfg);
|
|
1494
|
+
let github = null;
|
|
1495
|
+
if (embedded && host && provider === "github" && !deps.offline) {
|
|
1496
|
+
github = await inspectGitHubToken(apiBaseForHost(host, cfg.github?.baseUrl), embedded.token, fetchFn);
|
|
1497
|
+
}
|
|
1498
|
+
reports.push({
|
|
1499
|
+
kind: "insteadOf",
|
|
1500
|
+
from: m.from,
|
|
1501
|
+
to: m.to,
|
|
1502
|
+
token: embedded ? { kind: provider === "github" ? classifyGitHubToken(embedded.token) : "unknown", fingerprint: fingerprint(embedded.token) } : null,
|
|
1503
|
+
github
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
for (const h of parseScopedHelpers(git(["config", "--get-regexp", "^credential\\..*\\.helper$"]).stdout)) {
|
|
1507
|
+
reports.push({ kind: "helper", from: h.scope, to: redactSecrets(h.helper), token: null, github: null });
|
|
1508
|
+
}
|
|
1509
|
+
return reports;
|
|
1510
|
+
}
|
|
1511
|
+
function classifyHelper(helper) {
|
|
1512
|
+
const h = helper.trim().replace(/^!/, "").replace(/['"]/g, "").replace(/\\/g, "/");
|
|
1513
|
+
const exe = h.split(/\s+(?=[a-z-]+(\s|$))/i)[0].split("/").pop().replace(/\.exe$/i, "").toLowerCase();
|
|
1514
|
+
if (/^manager(-core)?(\s|$)/.test(h) || /git-credential-manager/i.test(h)) return "git-credential-manager";
|
|
1515
|
+
if (/^wincred(\s|$)/.test(h) || exe === "git-credential-wincred") return "wincred";
|
|
1516
|
+
if (/^osxkeychain(\s|$)/.test(h) || exe === "git-credential-osxkeychain") return "osxkeychain";
|
|
1517
|
+
if (/libsecret/i.test(h)) return "libsecret";
|
|
1518
|
+
if (/^store(\s|$)/.test(h)) return "store";
|
|
1519
|
+
if (/^cache(\s|$)/.test(h)) return "cache";
|
|
1520
|
+
if (/(^|\/)gh(\.exe)?\s+auth\s+git-credential/i.test(h)) return "gh";
|
|
1521
|
+
if (/(^|\/)pncli(\.cmd)?\s+skills\s+git-credential/i.test(h)) return "pncli";
|
|
1522
|
+
return "other";
|
|
1523
|
+
}
|
|
1524
|
+
function gcmCredentialStore(git, platform = process.platform, env = process.env) {
|
|
1525
|
+
const r = git(["config", "--get", "credential.credentialStore"]);
|
|
1526
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
1527
|
+
if (env.GCM_CREDENTIAL_STORE) return env.GCM_CREDENTIAL_STORE;
|
|
1528
|
+
return platform === "win32" ? "wincredman" : platform === "darwin" ? "keychain" : "unset";
|
|
1529
|
+
}
|
|
1530
|
+
function parseGitTarget(target) {
|
|
1531
|
+
const m = /^git:(https?:\/\/.+)$/i.exec(target);
|
|
1532
|
+
if (!m) return null;
|
|
1533
|
+
try {
|
|
1534
|
+
const u = new URL(m[1]);
|
|
1535
|
+
const p = u.pathname.replace(/^\/+|\/+$/g, "");
|
|
1536
|
+
return { host: u.host.toLowerCase(), path: p || null, username: u.username ? decodeURIComponent(u.username) : null };
|
|
1537
|
+
} catch {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function parseGitCredentialsFile(content) {
|
|
1542
|
+
const out = [];
|
|
1543
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1544
|
+
const t = line.trim();
|
|
1545
|
+
if (!t) continue;
|
|
1546
|
+
try {
|
|
1547
|
+
const u = new URL(t);
|
|
1548
|
+
const found = tokenFromUrl(t);
|
|
1549
|
+
if (!found) continue;
|
|
1550
|
+
const p = u.pathname.replace(/^\/+|\/+$/g, "");
|
|
1551
|
+
out.push({ host: u.host.toLowerCase(), path: p || null, username: found.username || null, secret: found.token });
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1557
|
+
function parseMacKeychainDump(output) {
|
|
1558
|
+
const out = [];
|
|
1559
|
+
for (const block of output.split(/^keychain: /m)) {
|
|
1560
|
+
if (!/^class: "inet"/m.test(block)) continue;
|
|
1561
|
+
const attr = (name) => new RegExp(`"${name}"<[^>]+>="([^"]*)"`).exec(block)?.[1] ?? null;
|
|
1562
|
+
const protocol = attr("ptcl");
|
|
1563
|
+
if (protocol && protocol !== "htps" && protocol !== "http") continue;
|
|
1564
|
+
const host = attr("srvr");
|
|
1565
|
+
if (!host) continue;
|
|
1566
|
+
const p = attr("path");
|
|
1567
|
+
out.push({ host: host.toLowerCase(), path: p ? p.replace(/^\/+/, "") || null : null, username: attr("acct") });
|
|
1568
|
+
}
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
function isGitHubHost(host, cfg) {
|
|
1572
|
+
return detectProvider(`https://${host}/`, cfg) === "github";
|
|
1573
|
+
}
|
|
1574
|
+
async function listStoredCredentials(hosts, cfg, deps = {}) {
|
|
1575
|
+
const git = deps.git ?? defaultGitRunner;
|
|
1576
|
+
const platform = deps.platform ?? process.platform;
|
|
1577
|
+
const env = deps.env ?? process.env;
|
|
1578
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1579
|
+
const run = deps.run ?? defaultRunner;
|
|
1580
|
+
const cache = deps.cache ?? /* @__PURE__ */ new Map();
|
|
1581
|
+
const readFile2 = deps.readFile ?? ((p) => {
|
|
1582
|
+
try {
|
|
1583
|
+
return fs.readFileSync(p, "utf8");
|
|
1584
|
+
} catch {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
const home = deps.homedir ?? os.homedir();
|
|
1589
|
+
const helperOut = git(["config", "--get-all", "credential.helper"]);
|
|
1590
|
+
const rawHelpers = helperOut.status === 0 ? helperOut.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : [];
|
|
1591
|
+
const kinds = [...new Set(rawHelpers.map(classifyHelper))];
|
|
1592
|
+
const configured = rawHelpers.map(redactSecrets);
|
|
1593
|
+
const report = {
|
|
1594
|
+
helpers: { configured, kinds, gcmCredentialStore: kinds.includes("git-credential-manager") ? gcmCredentialStore(git, platform, env) : null },
|
|
1595
|
+
stores: [],
|
|
1596
|
+
probes: [],
|
|
1597
|
+
problems: []
|
|
1598
|
+
};
|
|
1599
|
+
const tokenInfo = (secret, host) => ({ kind: isGitHubHost(host, cfg) ? classifyGitHubToken(secret) : "unknown", fingerprint: fingerprint(secret) });
|
|
1600
|
+
if (platform === "win32") {
|
|
1601
|
+
try {
|
|
1602
|
+
const entries = [];
|
|
1603
|
+
for (const it of windowsListCredentials("git:*", run)) {
|
|
1604
|
+
const t = parseGitTarget(it.target);
|
|
1605
|
+
if (!t) continue;
|
|
1606
|
+
entries.push({
|
|
1607
|
+
store: "windows-credential-manager",
|
|
1608
|
+
host: t.host,
|
|
1609
|
+
path: t.path,
|
|
1610
|
+
username: it.user || t.username,
|
|
1611
|
+
token: it.secret ? tokenInfo(it.secret, t.host) : null,
|
|
1612
|
+
location: it.target,
|
|
1613
|
+
github: null,
|
|
1614
|
+
secret: it.secret || void 0
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
report.stores.push({ store: "windows-credential-manager", available: true, error: null, entries });
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
report.stores.push({ store: "windows-credential-manager", available: false, error: err instanceof Error ? err.message : String(err), entries: [] });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
for (const file of [path.join(home, ".git-credentials"), path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "git", "credentials")]) {
|
|
1623
|
+
const content = readFile2(file);
|
|
1624
|
+
if (content === null) continue;
|
|
1625
|
+
const entries = parseGitCredentialsFile(content).map((e) => ({
|
|
1626
|
+
store: "git-credentials-file",
|
|
1627
|
+
host: e.host,
|
|
1628
|
+
path: e.path,
|
|
1629
|
+
username: e.username,
|
|
1630
|
+
token: tokenInfo(e.secret, e.host),
|
|
1631
|
+
location: file,
|
|
1632
|
+
github: null,
|
|
1633
|
+
secret: e.secret
|
|
1634
|
+
}));
|
|
1635
|
+
report.stores.push({ store: "git-credentials-file", available: true, error: null, entries });
|
|
1636
|
+
}
|
|
1637
|
+
if (platform === "darwin") {
|
|
1638
|
+
const r = run("security", ["dump-keychain"]);
|
|
1639
|
+
if (r.status === 0) {
|
|
1640
|
+
const entries = parseMacKeychainDump(r.stdout).map((e) => ({
|
|
1641
|
+
store: "macos-keychain",
|
|
1642
|
+
host: e.host,
|
|
1643
|
+
path: e.path,
|
|
1644
|
+
username: e.username,
|
|
1645
|
+
token: null,
|
|
1646
|
+
location: `login keychain: ${e.host}`,
|
|
1647
|
+
github: null
|
|
1648
|
+
}));
|
|
1649
|
+
report.stores.push({ store: "macos-keychain", available: true, error: null, entries });
|
|
1650
|
+
} else {
|
|
1651
|
+
report.stores.push({ store: "macos-keychain", available: false, error: r.stderr.trim() || `security exited ${r.status}`, entries: [] });
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
1655
|
+
for (const h of hosts) scopes.set(h.toLowerCase(), { host: h.toLowerCase(), path: null });
|
|
1656
|
+
for (const e of report.stores.flatMap((st) => st.entries)) scopes.set(e.path ? `${e.host}/${e.path}` : e.host, { host: e.host, path: e.path });
|
|
1657
|
+
const probeSecrets = /* @__PURE__ */ new Map();
|
|
1658
|
+
for (const [key, { host, path: p }] of [...scopes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1659
|
+
const filled = credentialFill(host, p ?? void 0, git);
|
|
1660
|
+
const h = git(["config", "--get-urlmatch", "credential.helper", `https://${host}/${p ?? ""}`]);
|
|
1661
|
+
const rawHelper = h.status === 0 ? h.stdout.trim().split(/\r?\n/).pop() ?? null : null;
|
|
1662
|
+
const helper = rawHelper ? redactSecrets(rawHelper) : null;
|
|
1663
|
+
if (filled) probeSecrets.set(key, filled.password);
|
|
1664
|
+
report.probes.push({
|
|
1665
|
+
host,
|
|
1666
|
+
path: p,
|
|
1667
|
+
username: filled?.username || null,
|
|
1668
|
+
token: filled ? tokenInfo(filled.password, host) : null,
|
|
1669
|
+
helper,
|
|
1670
|
+
helperKind: rawHelper ? classifyHelper(rawHelper) : null
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const shadowed = [];
|
|
1674
|
+
for (const store of report.stores) {
|
|
1675
|
+
for (const e of store.entries) {
|
|
1676
|
+
const key = e.path ? `${e.host}/${e.path}` : e.host;
|
|
1677
|
+
const sent = probeSecrets.get(key);
|
|
1678
|
+
const probe = report.probes.find((pr) => pr.host === e.host && pr.path === e.path);
|
|
1679
|
+
const siblings = store.entries.filter((x) => x.host === e.host && x.path === e.path).map((x) => x.secret);
|
|
1680
|
+
if (e.secret && sent && probe && !siblings.includes(sent)) shadowed.push({ entry: e, store: store.store, probe });
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (!deps.offline) {
|
|
1684
|
+
for (const store of report.stores) {
|
|
1685
|
+
for (const e of store.entries) {
|
|
1686
|
+
if (!e.secret || !isGitHubHost(e.host, cfg)) continue;
|
|
1687
|
+
const apiBase = apiBaseForHost(e.host, cfg.github?.baseUrl);
|
|
1688
|
+
const key = `${apiBase}\0${e.secret}`;
|
|
1689
|
+
if (!cache.has(key)) cache.set(key, await inspectGitHubToken(apiBase, e.secret, fetchFn));
|
|
1690
|
+
e.github = cache.get(key);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
report.problems = [...buildStoredProblems(report), ...shadowed.map(({ entry: e, store, probe }) => ({
|
|
1695
|
+
message: `git never sends the ${store} credential for ${e.path ? `${e.host}/${e.path}` : e.host} (${e.username ?? "no username"}, ${e.token?.fingerprint}): the ${probe.helperKind ?? "configured"} helper answers first with ${probe.username ?? "no username"}, ${probe.token?.fingerprint}`,
|
|
1696
|
+
fix: store === "windows-credential-manager" ? `If the stored one is obsolete, delete it: cmdkey /delete:"${e.location}" (git credential reject would go to the ${probe.helperKind ?? "other"} helper, not to this entry). If it should win instead, remove the helper entry from your gitconfig.` : `If the stored one is obsolete, remove it from ${e.location}; if it should win, remove the ${probe.helperKind ?? "other"} helper entry from your gitconfig.`
|
|
1697
|
+
}))];
|
|
1698
|
+
for (const store of report.stores) for (const e of store.entries) delete e.secret;
|
|
1699
|
+
return report;
|
|
1700
|
+
}
|
|
1701
|
+
function buildStoredProblems(r) {
|
|
1702
|
+
const problems = [];
|
|
1703
|
+
const forget = (e) => `pncli git credentials forget --host ${e.host}${e.username ? ` --username ${e.username}` : ""}${e.path ? ` --path ${e.path}` : ""}`;
|
|
1704
|
+
for (const store of r.stores) {
|
|
1705
|
+
for (const e of store.entries) {
|
|
1706
|
+
const who = `The ${store.store} credential for ${e.host}${e.path ? `/${e.path}` : ""} (${e.username ?? "no username"}, ${e.token?.fingerprint ?? "no secret"})`;
|
|
1707
|
+
const g = e.github;
|
|
1708
|
+
if (g?.status === 401) {
|
|
1709
|
+
problems.push({ message: `${who} is rejected by GitHub \u2014 expired or revoked. git keeps sending it until it is removed`, fix: forget(e) });
|
|
1710
|
+
} else if (g && g.expiresInDays !== null && g.expiresInDays <= 14) {
|
|
1711
|
+
problems.push({ message: g.expiresInDays < 0 ? `${who} expired on ${g.expiresAt}` : `${who} expires in ${g.expiresInDays} day(s)`, fix: `Replace it: ${forget(e)}, then sign in again` });
|
|
1712
|
+
}
|
|
1713
|
+
if (g?.missingRepoScope) problems.push({ message: `${who} is a classic token without the "repo" scope \u2014 it cannot read private repositories`, fix: `${forget(e)}, then store a token with "repo"` });
|
|
1714
|
+
if (g?.ssoAuthorizationRequired) problems.push({ message: `${who} is not authorized for the organization's SAML SSO`, fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize" });
|
|
1715
|
+
if (store.store === "git-credentials-file") {
|
|
1716
|
+
problems.push({ message: `${who} is stored in plaintext in ${e.location}`, fix: "Switch to an OS-backed helper (Windows: git config --global credential.helper manager; macOS: osxkeychain) and delete that line" });
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1720
|
+
for (const e of store.entries) {
|
|
1721
|
+
const k = e.path ? `${e.host}/${e.path}` : e.host;
|
|
1722
|
+
if (!byScope.has(k)) byScope.set(k, /* @__PURE__ */ new Set());
|
|
1723
|
+
byScope.get(k).add(e.username ?? "");
|
|
1724
|
+
}
|
|
1725
|
+
for (const [k, users] of byScope) {
|
|
1726
|
+
if (users.size < 2) continue;
|
|
1727
|
+
problems.push({
|
|
1728
|
+
message: `${store.store} holds ${users.size} accounts for ${k} (${[...users].filter(Boolean).join(", ")}) \u2014 git may send a different one than you expect`,
|
|
1729
|
+
fix: `Pin the account: git config --global credential.https://${k}.username <account> (or remove the extra one with pncli git credentials forget)`
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return problems;
|
|
1734
|
+
}
|
|
1735
|
+
function forgetCredential(host, opts = {}, git = defaultGitRunner) {
|
|
1736
|
+
if (opts.path) {
|
|
1737
|
+
const r = git(["config", "--get-urlmatch", "credential.useHttpPath", `https://${host}/${opts.path}`]);
|
|
1738
|
+
if (r.status !== 0 || r.stdout.trim().toLowerCase() !== "true") {
|
|
1739
|
+
throw new Error(`git ignores --path for ${host}: credential.useHttpPath is not enabled for that URL, so this would forget the host-wide credential. Omit --path to forget that deliberately.`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const before = credentialFill(host, opts.path, git);
|
|
1743
|
+
if (!before) return { host, before: null, after: null, removed: false };
|
|
1744
|
+
const username = opts.username ?? before?.username;
|
|
1745
|
+
const sameAsFilled = before && (!opts.username || opts.username === before.username);
|
|
1746
|
+
const fields = [
|
|
1747
|
+
"protocol=https",
|
|
1748
|
+
`host=${host}`,
|
|
1749
|
+
...opts.path ? [`path=${opts.path}`] : [],
|
|
1750
|
+
...username ? [`username=${username}`] : [],
|
|
1751
|
+
...sameAsFilled ? [`password=${before.password}`] : []
|
|
1752
|
+
];
|
|
1753
|
+
git(["credential", "reject"], { input: `${fields.join("\n")}
|
|
1754
|
+
|
|
1755
|
+
`, env: { GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" } });
|
|
1756
|
+
const after = credentialFill(host, opts.path, git);
|
|
1757
|
+
return {
|
|
1758
|
+
host,
|
|
1759
|
+
before: before ? fingerprint(before.password) : null,
|
|
1760
|
+
after: after ? fingerprint(after.password) : null,
|
|
1761
|
+
removed: !!before && (!after || after.password !== before.password)
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
820
1765
|
// src/services/git/commands.ts
|
|
1766
|
+
import fs2 from "fs";
|
|
1767
|
+
import path2 from "path";
|
|
821
1768
|
function requireRepoRoot() {
|
|
822
1769
|
const root = getRepoRoot();
|
|
823
1770
|
if (!root) throw new PncliError("Not a git repository", 1);
|
|
@@ -915,11 +1862,136 @@ function registerGitCommands(program2) {
|
|
|
915
1862
|
fail(err, "git", "current-pr", start);
|
|
916
1863
|
}
|
|
917
1864
|
});
|
|
1865
|
+
const credentials = git.command("credentials").description("Inspect which credential git uses for the remotes of a clone (insteadOf mappings, embedded tokens, helpers) and whether it can read the repo");
|
|
1866
|
+
credentials.addHelpText("after", `
|
|
1867
|
+
Read-only except forget. Tokens are never printed \u2014 only their type and a fingerprint (ghp_\u2026a1b2).
|
|
1868
|
+
|
|
1869
|
+
Examples:
|
|
1870
|
+
pncli git credentials inspect # every remote of the clone you are in
|
|
1871
|
+
pncli git credentials inspect ~/src/tools --remote upstream
|
|
1872
|
+
pncli git credentials inspect https://github.com/imagile/tools.git
|
|
1873
|
+
pncli git credentials inspect --scan ~/src --problems-only # clones mapped without a token, or without access
|
|
1874
|
+
pncli git credentials mappings # every insteadOf rewrite and URL-scoped helper, tokens checked
|
|
1875
|
+
pncli git credentials stored # Git Credential Manager / Windows Credential Manager /
|
|
1876
|
+
# Keychain / ~/.git-credentials: accounts, token health
|
|
1877
|
+
pncli git credentials forget --host github.com --username octo # drop a stale stored credential
|
|
1878
|
+
`);
|
|
1879
|
+
credentials.command("inspect").description("Show the credential each remote uses after insteadOf rewriting, and (online) whether it can read the repo").argument("[target]", "A clone directory or a remote URL (default: the current clone)").option("--remote <name>", "Only this remote (default: all remotes)").option("--scan <dir>", "Inspect every clone under this directory instead").option("--depth <n>", "How many directory levels --scan descends", "2").option("--problems-only", "With --scan, only list remotes that have problems").option("--offline", "Skip network checks (ls-remote and the GitHub API)").action(async (target, cmdOpts) => {
|
|
1880
|
+
const start = Date.now();
|
|
1881
|
+
try {
|
|
1882
|
+
const opts = program2.optsWithGlobals();
|
|
1883
|
+
const cfg = (() => {
|
|
1884
|
+
try {
|
|
1885
|
+
return loadConfig({ configPath: opts.config });
|
|
1886
|
+
} catch {
|
|
1887
|
+
return {};
|
|
1888
|
+
}
|
|
1889
|
+
})();
|
|
1890
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1891
|
+
const deps = { offline: !!cmdOpts.offline, cache };
|
|
1892
|
+
const inspectClone = async (dir2) => {
|
|
1893
|
+
const remotes2 = listRemotes(dir2).filter(([name]) => !cmdOpts.remote || name === cmdOpts.remote);
|
|
1894
|
+
const out = [];
|
|
1895
|
+
for (const [name, url] of remotes2) out.push(await inspectRemoteAuth(name, url, dir2, cfg, deps));
|
|
1896
|
+
return out;
|
|
1897
|
+
};
|
|
1898
|
+
if (cmdOpts.scan) {
|
|
1899
|
+
const depth = Number.parseInt(cmdOpts.depth, 10);
|
|
1900
|
+
if (!Number.isInteger(depth) || depth < 0) throw new PncliError("--depth must be a non-negative integer", 1);
|
|
1901
|
+
const clones = findClones(cmdOpts.scan, depth);
|
|
1902
|
+
const results = [];
|
|
1903
|
+
for (const dir2 of clones) {
|
|
1904
|
+
const remotes2 = await inspectClone(dir2);
|
|
1905
|
+
const shown = cmdOpts.problemsOnly ? remotes2.filter((r) => r.problems.length > 0) : remotes2;
|
|
1906
|
+
if (shown.length > 0) results.push({ path: dir2, remotes: shown });
|
|
1907
|
+
}
|
|
1908
|
+
const all = results.flatMap((r) => r.remotes);
|
|
1909
|
+
success({
|
|
1910
|
+
root: path2.resolve(cmdOpts.scan),
|
|
1911
|
+
clonesScanned: clones.length,
|
|
1912
|
+
summary: {
|
|
1913
|
+
remotes: all.length,
|
|
1914
|
+
withProblems: all.filter((r) => r.problems.length > 0).length,
|
|
1915
|
+
noCredential: all.filter((r) => r.host && r.credentialSource === "none").length,
|
|
1916
|
+
cannotRead: all.filter((r) => r.access && !r.access.ok).length
|
|
1917
|
+
},
|
|
1918
|
+
clones: results
|
|
1919
|
+
}, "git", "credentials-inspect", start);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (target && /^(https?:\/\/|git@|ssh:\/\/)/i.test(target)) {
|
|
1923
|
+
success({ remotes: [await inspectRemoteAuth(null, target, null, cfg, deps)] }, "git", "credentials-inspect", start);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const dir = target ? path2.resolve(target) : requireRepoRoot();
|
|
1927
|
+
if (!fs2.existsSync(path2.join(dir, ".git"))) throw new PncliError(`${dir} is not a git clone`, 1);
|
|
1928
|
+
const remotes = await inspectClone(dir);
|
|
1929
|
+
if (remotes.length === 0) throw new PncliError(cmdOpts.remote ? `No remote named "${cmdOpts.remote}" in ${dir}` : `${dir} has no remotes`, 1);
|
|
1930
|
+
success({ path: dir, remotes }, "git", "credentials-inspect", start);
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
fail(err, "git", "credentials-inspect", start);
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
credentials.command("mappings").description("List every insteadOf URL rewrite and URL-scoped credential helper in your gitconfig, tokens redacted and (online) validated").option("--offline", "Skip validating embedded GitHub tokens").action(async (cmdOpts) => {
|
|
1936
|
+
const start = Date.now();
|
|
1937
|
+
try {
|
|
1938
|
+
const opts = program2.optsWithGlobals();
|
|
1939
|
+
const cfg = (() => {
|
|
1940
|
+
try {
|
|
1941
|
+
return loadConfig({ configPath: opts.config });
|
|
1942
|
+
} catch {
|
|
1943
|
+
return {};
|
|
1944
|
+
}
|
|
1945
|
+
})();
|
|
1946
|
+
const mappings = await listMappings(getRepoRoot(), cfg, { offline: !!cmdOpts.offline });
|
|
1947
|
+
success({ mappings, total: mappings.length }, "git", "credentials-mappings", start);
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
fail(err, "git", "credentials-mappings", start);
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
credentials.command("stored").description("List credentials git keeps in Git Credential Manager, Windows Credential Manager, the macOS Keychain, or ~/.git-credentials, with what git would send per host and (online) token health").option("--host <host...>", "Also ask git what it would send for these hosts").option("--offline", "Skip validating GitHub tokens").action(async (cmdOpts) => {
|
|
1953
|
+
const start = Date.now();
|
|
1954
|
+
try {
|
|
1955
|
+
const opts = program2.optsWithGlobals();
|
|
1956
|
+
const cfg = (() => {
|
|
1957
|
+
try {
|
|
1958
|
+
return loadConfig({ configPath: opts.config });
|
|
1959
|
+
} catch {
|
|
1960
|
+
return {};
|
|
1961
|
+
}
|
|
1962
|
+
})();
|
|
1963
|
+
const hosts = new Set((cmdOpts.host ?? []).map((h) => h.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "")));
|
|
1964
|
+
for (const h of [gitHostFromApiBaseUrl(cfg.github?.baseUrl), hostOf(cfg.bitbucket?.baseUrl), hostOf(cfg.ado?.baseUrl)]) if (h) hosts.add(h);
|
|
1965
|
+
const root = getRepoRoot();
|
|
1966
|
+
if (root) for (const [, url] of listRemotes(root)) {
|
|
1967
|
+
const h = hostOf(url);
|
|
1968
|
+
if (h) hosts.add(h);
|
|
1969
|
+
}
|
|
1970
|
+
const report = await listStoredCredentials([...hosts], cfg, { offline: !!cmdOpts.offline });
|
|
1971
|
+
success(report, "git", "credentials-stored", start);
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
fail(err, "git", "credentials-stored", start);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
credentials.command("forget").description("Remove a stored credential for a host through git credential reject (works for every helper: Git Credential Manager, wincred, osxkeychain, libsecret, store)").requiredOption("--host <host>", "Host to forget, e.g. github.com").option("--username <username>", "Only this account (when several are stored for the host)").option("--path <path>", "Repository path, for helpers that store per repository (e.g. Azure Repos in Git Credential Manager)").action((cmdOpts) => {
|
|
1977
|
+
const start = Date.now();
|
|
1978
|
+
try {
|
|
1979
|
+
const host = cmdOpts.host.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1980
|
+
const result = forgetCredential(host, { username: cmdOpts.username, path: cmdOpts.path });
|
|
1981
|
+
if (!result.before) throw new PncliError(`git has no stored credential for ${host}${cmdOpts.path ? `/${cmdOpts.path}` : ""} \u2014 nothing to forget`, 1);
|
|
1982
|
+
success({
|
|
1983
|
+
...result,
|
|
1984
|
+
next: result.removed ? "git will ask for (or its helper will fetch) a fresh credential next time" : "git still supplies the same credential \u2014 it may come from an insteadOf mapping or a helper that ignores reject; see: pncli git credentials inspect"
|
|
1985
|
+
}, "git", "credentials-forget", start);
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
fail(err, "git", "credentials-forget", start);
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
918
1990
|
}
|
|
919
1991
|
|
|
920
1992
|
// src/services/jira/commands.ts
|
|
921
1993
|
import { readFileSync as readFileSync3, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
922
|
-
import
|
|
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
|
|
1387
|
-
if (
|
|
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 ??
|
|
2768
|
+
const outDir = opts.dir ?? path3.join(process.cwd(), ".pncli");
|
|
1675
2769
|
mkdirSync(outDir, { recursive: true });
|
|
1676
|
-
const outPath =
|
|
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
|
-
|
|
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[
|
|
2849
|
+
result[fieldId] = JSON.parse(raw);
|
|
1755
2850
|
} catch {
|
|
1756
|
-
result[
|
|
2851
|
+
result[fieldId] = raw;
|
|
1757
2852
|
}
|
|
1758
2853
|
} else {
|
|
1759
|
-
result[
|
|
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
|
|
1783
|
-
if (
|
|
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[
|
|
2882
|
+
result[fieldId] = val;
|
|
1788
2883
|
}
|
|
1789
2884
|
return result;
|
|
1790
2885
|
}
|
|
@@ -2306,8 +3401,8 @@ var GitHubClient = class {
|
|
|
2306
3401
|
);
|
|
2307
3402
|
}
|
|
2308
3403
|
async createRepo(opts) {
|
|
2309
|
-
const
|
|
2310
|
-
return this.http.github(
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
4204
|
-
import
|
|
4205
|
-
import { execSync as execSync3, execFileSync as
|
|
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
|
|
5303
|
+
import path5 from "path";
|
|
4209
5304
|
function findNpmManifests(files) {
|
|
4210
5305
|
const lockFiles = new Set(
|
|
4211
5306
|
files.filter((f) => {
|
|
4212
|
-
const base =
|
|
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 (
|
|
5313
|
+
if (path5.basename(file) !== "package.json") continue;
|
|
4219
5314
|
if (file.split("/").includes("node_modules")) continue;
|
|
4220
|
-
const dir =
|
|
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 =
|
|
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
|
|
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 (
|
|
4389
|
-
lockFiles.set(
|
|
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 =
|
|
4395
|
-
const ext =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5637
|
+
import path7 from "path";
|
|
4543
5638
|
function findMavenManifests(files) {
|
|
4544
|
-
const lockFiles = new Set(files.filter((f) =>
|
|
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 =
|
|
5642
|
+
const base = path7.basename(file);
|
|
4548
5643
|
if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
|
|
4549
|
-
const dir =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
6276
|
-
await http.ado(
|
|
7370
|
+
const path14 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
|
|
7371
|
+
await http.ado(path14, { timeoutMs: 1e4 });
|
|
6277
7372
|
results.ado = { status: "valid", message: "ok" };
|
|
6278
7373
|
} catch (err) {
|
|
6279
7374
|
results.ado = categorize(err);
|
|
@@ -6478,44 +7573,304 @@ async function runCredentialChecks(cfg, http) {
|
|
|
6478
7573
|
await http.splitio("/internal/api/v2/workspaces", { timeoutMs: 1e4 });
|
|
6479
7574
|
results.splitio = { status: "valid", message: "ok" };
|
|
6480
7575
|
} catch (err) {
|
|
6481
|
-
results.splitio = categorize(err);
|
|
7576
|
+
results.splitio = categorize(err);
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
if (!cfg.figma.token) {
|
|
7580
|
+
results.figma = { status: "blank", message: "not configured" };
|
|
7581
|
+
} else if (!cfg.figma.baseUrl) {
|
|
7582
|
+
results.figma = { status: "error", message: "baseUrl not configured" };
|
|
7583
|
+
} else {
|
|
7584
|
+
try {
|
|
7585
|
+
await http.figma("/v1/me", { timeoutMs: 1e4 });
|
|
7586
|
+
results.figma = { status: "valid", message: "ok" };
|
|
7587
|
+
} catch (err) {
|
|
7588
|
+
results.figma = categorize(err);
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7591
|
+
if (!cfg.alation.refreshToken) {
|
|
7592
|
+
results.alation = { status: "blank", message: "not configured" };
|
|
7593
|
+
} else if (!cfg.alation.baseUrl) {
|
|
7594
|
+
results.alation = { status: "error", message: "baseUrl not configured" };
|
|
7595
|
+
} else if (cfg.alation.userId === void 0 || cfg.alation.userId === "") {
|
|
7596
|
+
results.alation = { status: "error", message: "userId not configured" };
|
|
7597
|
+
} else {
|
|
7598
|
+
try {
|
|
7599
|
+
await validateAlationAccessToken(cfg);
|
|
7600
|
+
results.alation = { status: "valid", message: "ok" };
|
|
7601
|
+
} catch (err) {
|
|
7602
|
+
results.alation = categorize(err);
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
return results;
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7608
|
+
// src/services/config/keychain-commands.ts
|
|
7609
|
+
import fs4 from "fs";
|
|
7610
|
+
import password from "@inquirer/password";
|
|
7611
|
+
function resolveConfigKey(root, key) {
|
|
7612
|
+
const parts = key.split(".").filter(Boolean);
|
|
7613
|
+
if (parts.length === 0) throw new KeychainError("A config key is required, e.g. github.token");
|
|
7614
|
+
const segments = [];
|
|
7615
|
+
let node = root;
|
|
7616
|
+
for (const [i, part] of parts.entries()) {
|
|
7617
|
+
if (Array.isArray(node)) {
|
|
7618
|
+
const idx = /^\d+$/.test(part) ? Number(part) : node.findIndex((e) => e && typeof e === "object" && e.name === part);
|
|
7619
|
+
if (idx < 0 || idx >= node.length) {
|
|
7620
|
+
throw new KeychainError(`No entry "${part}" in ${parts.slice(0, i).join(".")} \u2014 check the name with: pncli config show`);
|
|
7621
|
+
}
|
|
7622
|
+
segments.push(idx);
|
|
7623
|
+
node = node[idx];
|
|
7624
|
+
} else {
|
|
7625
|
+
segments.push(part);
|
|
7626
|
+
const obj = node;
|
|
7627
|
+
if (i < parts.length - 1 && (typeof obj[part] !== "object" || obj[part] === null)) obj[part] = {};
|
|
7628
|
+
node = obj[part];
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7631
|
+
return segments;
|
|
7632
|
+
}
|
|
7633
|
+
function getAtPath(root, segments) {
|
|
7634
|
+
let node = root;
|
|
7635
|
+
for (const seg of segments) node = node && typeof node === "object" ? node[seg] : void 0;
|
|
7636
|
+
return node;
|
|
7637
|
+
}
|
|
7638
|
+
function deleteAtPath(root, segments) {
|
|
7639
|
+
const parent = getAtPath(root, segments.slice(0, -1));
|
|
7640
|
+
if (parent) delete parent[segments[segments.length - 1]];
|
|
7641
|
+
}
|
|
7642
|
+
async function readSecret(key, opts) {
|
|
7643
|
+
if (opts.value !== void 0) {
|
|
7644
|
+
warn("--value leaves the secret in your shell history. Prefer the interactive prompt or --stdin.");
|
|
7645
|
+
return opts.value;
|
|
7646
|
+
}
|
|
7647
|
+
if (opts.stdin || !process.stdin.isTTY) {
|
|
7648
|
+
const chunks = [];
|
|
7649
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7650
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
7651
|
+
}
|
|
7652
|
+
return (await password({ message: `Secret for ${key}:`, mask: "*" })).trim();
|
|
7653
|
+
}
|
|
7654
|
+
function matchesFilter(config, segments, only) {
|
|
7655
|
+
if (!only || only.size === 0) return true;
|
|
7656
|
+
return only.has(segments.join(".")) || only.has(accountForPath(config, segments));
|
|
7657
|
+
}
|
|
7658
|
+
function unmigrateSecrets(config, backend, dryRun = false, only) {
|
|
7659
|
+
const migrated = [];
|
|
7660
|
+
const failed = [];
|
|
7661
|
+
const refs = findKeychainRefs(config).filter((r) => matchesFilter(config, r.path, only));
|
|
7662
|
+
let secrets = {};
|
|
7663
|
+
if (!dryRun && refs.length > 0) {
|
|
7664
|
+
const accounts = [...new Set(refs.flatMap((r) => {
|
|
7665
|
+
try {
|
|
7666
|
+
return [accountFromRef(r.value)];
|
|
7667
|
+
} catch {
|
|
7668
|
+
return [];
|
|
7669
|
+
}
|
|
7670
|
+
}))];
|
|
7671
|
+
secrets = backend.getMany(accounts);
|
|
7672
|
+
}
|
|
7673
|
+
for (const ref of refs) {
|
|
7674
|
+
let account;
|
|
7675
|
+
try {
|
|
7676
|
+
account = accountFromRef(ref.value);
|
|
7677
|
+
} catch (err) {
|
|
7678
|
+
failed.push({ path: ref.path.join("."), account: ref.value, reason: err.message });
|
|
7679
|
+
continue;
|
|
7680
|
+
}
|
|
7681
|
+
const entry = { path: ref.path.join("."), account };
|
|
7682
|
+
if (dryRun) {
|
|
7683
|
+
migrated.push(entry);
|
|
7684
|
+
continue;
|
|
7685
|
+
}
|
|
7686
|
+
const secret = secrets[account];
|
|
7687
|
+
if (secret == null) {
|
|
7688
|
+
failed.push({ ...entry, reason: "no entry in the keychain" });
|
|
7689
|
+
continue;
|
|
7690
|
+
}
|
|
7691
|
+
setAtPath(config, ref.path, secret);
|
|
7692
|
+
migrated.push(entry);
|
|
7693
|
+
}
|
|
7694
|
+
return { migrated, failed };
|
|
7695
|
+
}
|
|
7696
|
+
function migrateSecrets(config, backend, dryRun = false, only) {
|
|
7697
|
+
const migrated = [];
|
|
7698
|
+
const failed = [];
|
|
7699
|
+
for (const secret of findPlaintextSecrets(config).filter((s) => matchesFilter(config, s.path, only))) {
|
|
7700
|
+
const account = accountForPath(config, secret.path);
|
|
7701
|
+
const entry = { path: secret.path.join("."), account };
|
|
7702
|
+
if (dryRun) {
|
|
7703
|
+
migrated.push(entry);
|
|
7704
|
+
continue;
|
|
7705
|
+
}
|
|
7706
|
+
try {
|
|
7707
|
+
storeVerified(backend, account, secret.value);
|
|
7708
|
+
setAtPath(config, secret.path, keychainRef(account));
|
|
7709
|
+
migrated.push(entry);
|
|
7710
|
+
} catch (err) {
|
|
7711
|
+
failed.push({ ...entry, reason: err instanceof Error ? err.message : String(err) });
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
return { migrated, failed };
|
|
7715
|
+
}
|
|
7716
|
+
function registerKeychainCommands(config, program2) {
|
|
7717
|
+
const keychain = config.command("keychain").description("Store credentials in the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service) instead of plaintext config");
|
|
7718
|
+
keychain.addHelpText("after", `
|
|
7719
|
+
How it works:
|
|
7720
|
+
A config value of the form "keychain:<account>" is a reference. pncli looks the secret up in the
|
|
7721
|
+
OS credential store at run time; ~/.pncli/config.json only ever holds the reference.
|
|
7722
|
+
PNCLI_* environment variables still take precedence over anything stored here.
|
|
7723
|
+
|
|
7724
|
+
Examples:
|
|
7725
|
+
pncli config keychain migrate --dry-run # which plaintext secrets would move
|
|
7726
|
+
pncli config keychain migrate # config -> keychain, every plaintext secret
|
|
7727
|
+
pncli config keychain migrate github.token # just one key
|
|
7728
|
+
pncli config keychain migrate --to config # keychain -> config (entries kept)
|
|
7729
|
+
pncli config keychain migrate --to config --purge # keychain -> config, then delete entries
|
|
7730
|
+
pncli config keychain set github.token # prompts for the secret (or pipe it: --stdin)
|
|
7731
|
+
pncli config keychain set marketplaces.internal-ai.token
|
|
7732
|
+
pncli config keychain status # backend, every reference, and whether it resolves
|
|
7733
|
+
pncli config keychain remove github.token --restore # back to plaintext config
|
|
7734
|
+
|
|
7735
|
+
Set PNCLI_KEYCHAIN_BACKEND=none to ignore references entirely (e.g. on a CI runner).
|
|
7736
|
+
`);
|
|
7737
|
+
keychain.command("status").description("Show the keychain backend, every keychain reference in config, and plaintext secrets that could move").action(() => {
|
|
7738
|
+
const start = Date.now();
|
|
7739
|
+
try {
|
|
7740
|
+
const opts = program2.optsWithGlobals();
|
|
7741
|
+
const raw = loadJsonFile(getGlobalConfigPath(opts.config)) ?? {};
|
|
7742
|
+
const backend = getKeychainBackend();
|
|
7743
|
+
const refs = findKeychainRefs(raw);
|
|
7744
|
+
const available = backend.available();
|
|
7745
|
+
let lookups = {};
|
|
7746
|
+
let lookupError = null;
|
|
7747
|
+
if (available && refs.length > 0) {
|
|
7748
|
+
try {
|
|
7749
|
+
lookups = backend.getMany([...new Set(refs.map((r) => accountFromRef(r.value)))]);
|
|
7750
|
+
} catch (err) {
|
|
7751
|
+
if (err instanceof KeychainBatchError) lookups = err.partial;
|
|
7752
|
+
lookupError = err instanceof Error ? err.message : String(err);
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
const plaintext = findPlaintextSecrets(raw).map((s) => s.path.join("."));
|
|
7756
|
+
success({
|
|
7757
|
+
backend: backend.name,
|
|
7758
|
+
store: backend.store,
|
|
7759
|
+
available,
|
|
7760
|
+
references: refs.map((r) => {
|
|
7761
|
+
const account = r.value.slice("keychain:".length);
|
|
7762
|
+
return { path: r.path.join("."), account, resolves: available ? lookups[account] != null : false };
|
|
7763
|
+
}),
|
|
7764
|
+
lookupError,
|
|
7765
|
+
plaintextSecrets: plaintext,
|
|
7766
|
+
hint: plaintext.length > 0 && available ? `Move ${plaintext.length} plaintext secret(s) into the keychain with: pncli config keychain migrate` : null
|
|
7767
|
+
}, "config", "keychain-status", start);
|
|
7768
|
+
} catch (err) {
|
|
7769
|
+
fail(err, "config", "keychain-status", start);
|
|
7770
|
+
}
|
|
7771
|
+
});
|
|
7772
|
+
keychain.command("set").description("Store a secret in the OS keychain and point a config key at it").argument("<key>", "Config key in dot notation, e.g. github.token or marketplaces.<name>.token").option("--stdin", "Read the secret from stdin instead of prompting").option("--value <secret>", "Pass the secret inline (ends up in shell history \u2014 prefer the prompt or --stdin)").option("--account <account>", "Keychain account name to store under (default: the config key)").action(async (key, cmdOpts) => {
|
|
7773
|
+
const start = Date.now();
|
|
7774
|
+
try {
|
|
7775
|
+
const opts = program2.optsWithGlobals();
|
|
7776
|
+
const configPath = getGlobalConfigPath(opts.config);
|
|
7777
|
+
const raw = loadJsonFile(configPath) ?? {};
|
|
7778
|
+
const segments = resolveConfigKey(raw, key);
|
|
7779
|
+
const account = cmdOpts.account ?? accountForPath(raw, segments);
|
|
7780
|
+
const secret = await readSecret(key, cmdOpts);
|
|
7781
|
+
const backend = getKeychainBackend();
|
|
7782
|
+
storeVerified(backend, account, secret);
|
|
7783
|
+
setAtPath(raw, segments, keychainRef(account));
|
|
7784
|
+
writeGlobalConfig(raw, opts.config);
|
|
7785
|
+
resetKeychainCache();
|
|
7786
|
+
success({ key: segments.join("."), account, store: backend.store, reference: keychainRef(account) }, "config", "keychain-set", start);
|
|
7787
|
+
} catch (err) {
|
|
7788
|
+
fail(err, "config", "keychain-set", start);
|
|
6482
7789
|
}
|
|
6483
|
-
}
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
} else if (!cfg.figma.baseUrl) {
|
|
6487
|
-
results.figma = { status: "error", message: "baseUrl not configured" };
|
|
6488
|
-
} else {
|
|
7790
|
+
});
|
|
7791
|
+
keychain.command("remove").description("Delete a secret from the OS keychain and clear its config reference").argument("<key>", "Config key holding a keychain reference, e.g. github.token").option("--restore", "Write the secret back into config as plaintext instead of clearing the key").action((key, cmdOpts) => {
|
|
7792
|
+
const start = Date.now();
|
|
6489
7793
|
try {
|
|
6490
|
-
|
|
6491
|
-
|
|
7794
|
+
const opts = program2.optsWithGlobals();
|
|
7795
|
+
const raw = loadJsonFile(getGlobalConfigPath(opts.config)) ?? {};
|
|
7796
|
+
const segments = resolveConfigKey(raw, key);
|
|
7797
|
+
const current = getAtPath(raw, segments);
|
|
7798
|
+
if (!isKeychainRef(current)) {
|
|
7799
|
+
throw new KeychainError(`${key} is not a keychain reference (nothing to remove).`);
|
|
7800
|
+
}
|
|
7801
|
+
const account = accountFromRef(current);
|
|
7802
|
+
const backend = getKeychainBackend();
|
|
7803
|
+
let restored = false;
|
|
7804
|
+
if (cmdOpts.restore) {
|
|
7805
|
+
const secret = backend.getMany([account])[account];
|
|
7806
|
+
if (secret == null) throw new KeychainError(`No keychain entry for "${account}" \u2014 nothing to restore. Re-run without --restore to clear the reference.`);
|
|
7807
|
+
setAtPath(raw, segments, secret);
|
|
7808
|
+
restored = true;
|
|
7809
|
+
} else {
|
|
7810
|
+
deleteAtPath(raw, segments);
|
|
7811
|
+
}
|
|
7812
|
+
writeGlobalConfig(raw, opts.config);
|
|
7813
|
+
const deleted = purgeEntries(raw, backend, [account]).length > 0;
|
|
7814
|
+
resetKeychainCache();
|
|
7815
|
+
success({ key: segments.join("."), account, deleted, restoredToConfig: restored }, "config", "keychain-remove", start);
|
|
6492
7816
|
} catch (err) {
|
|
6493
|
-
|
|
7817
|
+
fail(err, "config", "keychain-remove", start);
|
|
6494
7818
|
}
|
|
6495
|
-
}
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
} else if (!cfg.alation.baseUrl) {
|
|
6499
|
-
results.alation = { status: "error", message: "baseUrl not configured" };
|
|
6500
|
-
} else if (cfg.alation.userId === void 0 || cfg.alation.userId === "") {
|
|
6501
|
-
results.alation = { status: "error", message: "userId not configured" };
|
|
6502
|
-
} else {
|
|
7819
|
+
});
|
|
7820
|
+
keychain.command("migrate").description("Move secrets between plaintext config and the OS keychain (--to keychain, the default, or --to config)").argument("[keys...]", "Limit to these config keys (e.g. github.token marketplaces.internal-ai.token); default: all").option("--to <destination>", "keychain (move plaintext secrets in) or config (move keychain secrets back out)", "keychain").option("--purge", "With --to config: also delete the keychain entries once config holds the secrets").option("--dry-run", "List what would move without touching the keychain or config").action((keys, cmdOpts) => {
|
|
7821
|
+
const start = Date.now();
|
|
6503
7822
|
try {
|
|
6504
|
-
|
|
6505
|
-
|
|
7823
|
+
const opts = program2.optsWithGlobals();
|
|
7824
|
+
const dryRun = !!(cmdOpts.dryRun || opts.dryRun);
|
|
7825
|
+
const configPath = getGlobalConfigPath(opts.config);
|
|
7826
|
+
if (cmdOpts.to !== "keychain" && cmdOpts.to !== "config") {
|
|
7827
|
+
throw new KeychainError(`--to must be "keychain" or "config", got "${cmdOpts.to}".`);
|
|
7828
|
+
}
|
|
7829
|
+
if (cmdOpts.purge && cmdOpts.to !== "config") {
|
|
7830
|
+
throw new KeychainError("--purge only applies to --to config.");
|
|
7831
|
+
}
|
|
7832
|
+
if (!fs4.existsSync(configPath)) throw new KeychainError(`No global config at ${configPath} \u2014 nothing to migrate.`);
|
|
7833
|
+
const raw = loadJsonFile(configPath) ?? {};
|
|
7834
|
+
const backend = getKeychainBackend();
|
|
7835
|
+
if (!dryRun && !backend.available()) {
|
|
7836
|
+
throw new KeychainError(`The ${backend.store} backend is not available on this machine.`);
|
|
7837
|
+
}
|
|
7838
|
+
const only = new Set(keys);
|
|
7839
|
+
const result = cmdOpts.to === "keychain" ? migrateSecrets(raw, backend, dryRun, only) : unmigrateSecrets(raw, backend, dryRun, only);
|
|
7840
|
+
let purged = [];
|
|
7841
|
+
if (!dryRun && result.migrated.length > 0) {
|
|
7842
|
+
if (cmdOpts.to === "keychain") {
|
|
7843
|
+
const backup = `${configPath}.pre-keychain.bak`;
|
|
7844
|
+
fs4.copyFileSync(configPath, backup);
|
|
7845
|
+
try {
|
|
7846
|
+
fs4.chmodSync(backup, 384);
|
|
7847
|
+
} catch {
|
|
7848
|
+
}
|
|
7849
|
+
warn(`Plaintext backup written to ${backup} \u2014 delete it once \`pncli config check\` passes.`);
|
|
7850
|
+
}
|
|
7851
|
+
writeGlobalConfig(raw, opts.config);
|
|
7852
|
+
if (cmdOpts.purge) purged = purgeEntries(raw, backend, result.migrated.map((m) => m.account));
|
|
7853
|
+
}
|
|
7854
|
+
resetKeychainCache();
|
|
7855
|
+
success({
|
|
7856
|
+
to: cmdOpts.to,
|
|
7857
|
+
dryRun,
|
|
7858
|
+
store: backend.store,
|
|
7859
|
+
...result,
|
|
7860
|
+
...cmdOpts.to === "config" ? { purged } : {}
|
|
7861
|
+
}, "config", "keychain-migrate", start);
|
|
6506
7862
|
} catch (err) {
|
|
6507
|
-
|
|
7863
|
+
fail(err, "config", "keychain-migrate", start);
|
|
6508
7864
|
}
|
|
6509
|
-
}
|
|
6510
|
-
return results;
|
|
7865
|
+
});
|
|
6511
7866
|
}
|
|
6512
7867
|
|
|
6513
7868
|
// src/services/skills/commands.ts
|
|
6514
|
-
import
|
|
6515
|
-
import
|
|
6516
|
-
import
|
|
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
|
|
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
|
|
6545
|
-
import
|
|
6546
|
-
import
|
|
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: () =>
|
|
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: () =>
|
|
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: () =>
|
|
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
|
|
7922
|
+
return path9.resolve(spec.file());
|
|
6568
7923
|
}
|
|
6569
7924
|
function findMarketplaceInstructions(marketplacePath) {
|
|
6570
|
-
const dir =
|
|
6571
|
-
return INSTRUCTION_SOURCE_FILES.map((file) => ({ file, path:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6667
|
-
|
|
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)
|
|
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 &&
|
|
8037
|
+
const shipped = marketplacePath && fs5.existsSync(marketplacePath) ? findMarketplaceInstructions(marketplacePath) : [];
|
|
6683
8038
|
return agents.map((agent) => {
|
|
6684
8039
|
const file = agentInstructionsFile(agent);
|
|
6685
|
-
const fileExists =
|
|
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,
|
|
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
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
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
|
-
|
|
6714
|
-
} catch {
|
|
6715
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6742
|
-
|
|
6743
|
-
if (
|
|
6744
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
6787
|
-
"github-copilot": { project: ".github/skills", user:
|
|
6788
|
-
"claude-code": { project: ".claude/skills", user:
|
|
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 =
|
|
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 ?
|
|
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 ?
|
|
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) :
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8534
|
+
return path11.join(skillDir, SKILL_ORIGIN_FILENAME);
|
|
6876
8535
|
}
|
|
6877
8536
|
function readSkillOrigin(skillDir) {
|
|
6878
8537
|
const originPath = getSkillOriginPath(skillDir);
|
|
6879
|
-
if (!
|
|
8538
|
+
if (!fs6.existsSync(originPath)) return null;
|
|
6880
8539
|
try {
|
|
6881
|
-
const raw = JSON.parse(
|
|
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(
|
|
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 (!
|
|
6907
|
-
return
|
|
8565
|
+
if (!fs6.existsSync(targetDir)) return [];
|
|
8566
|
+
return fs6.readdirSync(targetDir).filter((name) => {
|
|
6908
8567
|
if (name.startsWith(".")) return false;
|
|
6909
|
-
const dir =
|
|
8568
|
+
const dir = path11.join(targetDir, name);
|
|
6910
8569
|
try {
|
|
6911
|
-
if (!
|
|
8570
|
+
if (!fs6.statSync(dir).isDirectory()) return false;
|
|
6912
8571
|
} catch {
|
|
6913
8572
|
return false;
|
|
6914
8573
|
}
|
|
6915
|
-
return
|
|
8574
|
+
return fs6.existsSync(path11.join(dir, "SKILL.md"));
|
|
6916
8575
|
});
|
|
6917
8576
|
}
|
|
6918
8577
|
function disablePluginSkills(targetDir, plugin, marketplaceFilter) {
|
|
6919
|
-
const resolvedTarget =
|
|
6920
|
-
const stashDir =
|
|
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 =
|
|
6927
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
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 =
|
|
6938
|
-
|
|
6939
|
-
if (
|
|
6940
|
-
|
|
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
|
-
|
|
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 =
|
|
6958
|
-
const stashDir =
|
|
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 =
|
|
6970
|
-
if (!dest.startsWith(resolvedTarget +
|
|
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 =
|
|
6975
|
-
if (!
|
|
8633
|
+
const stashSrc = path11.join(stashDir, skillName);
|
|
8634
|
+
if (!fs6.existsSync(stashSrc)) {
|
|
6976
8635
|
stashMissing.push(skillName);
|
|
6977
8636
|
continue;
|
|
6978
8637
|
}
|
|
6979
|
-
if (
|
|
6980
|
-
|
|
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 (
|
|
6986
|
-
|
|
8644
|
+
if (fs6.existsSync(stashDir) && fs6.readdirSync(stashDir).length === 0) {
|
|
8645
|
+
fs6.rmdirSync(stashDir);
|
|
6987
8646
|
}
|
|
6988
8647
|
if (enabled.length > 0) {
|
|
6989
|
-
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
7059
|
-
const builtIn = Object.values(AGENT_PATHS).flatMap((a) => [
|
|
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 =
|
|
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 =
|
|
8735
|
+
const resolved = path11.resolve(targetDir);
|
|
7077
8736
|
const base = {
|
|
7078
8737
|
agent,
|
|
7079
8738
|
scope,
|
|
7080
8739
|
path: resolved,
|
|
7081
|
-
exists:
|
|
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 =
|
|
7101
|
-
base.disabledStashMissing = disabledNames.filter((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 || !
|
|
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 ?
|
|
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 =
|
|
7173
|
-
if (
|
|
8831
|
+
const skillDir = path11.join(targetDir, skillName);
|
|
8832
|
+
if (fs6.existsSync(skillDir)) {
|
|
7174
8833
|
const origin = { version: 1, ...fullRecord };
|
|
7175
8834
|
try {
|
|
7176
|
-
|
|
8835
|
+
fs6.writeFileSync(getSkillOriginPath(skillDir), JSON.stringify(origin, null, 2), "utf8");
|
|
7177
8836
|
} catch {
|
|
7178
8837
|
}
|
|
7179
8838
|
}
|
|
7180
8839
|
}
|
|
7181
|
-
|
|
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
|
-
|
|
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 =
|
|
7258
|
-
if (
|
|
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
|
|
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
|
|
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(
|
|
8936
|
+
cloneArgs.push(url, resolvedPath);
|
|
7272
8937
|
try {
|
|
7273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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}: ${
|
|
9011
|
+
log(` ${skill}: ${path11.join(skillsSrc, skill)} \u2192 ${path11.join(targetDir, skill)}`);
|
|
7330
9012
|
}
|
|
7331
9013
|
if (failed.length > 0) {
|
|
7332
9014
|
warn(`Skipped ${failed.length} skill(s) with invalid names in "${pluginName}": ${failed.join(", ")}`);
|
|
@@ -7411,11 +9093,11 @@ function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
|
|
|
7411
9093
|
const marketplaceName = marketplaceLabel(m);
|
|
7412
9094
|
try {
|
|
7413
9095
|
const marketplacePath = m.localPath;
|
|
7414
|
-
if (!marketplacePath || !
|
|
9096
|
+
if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
|
|
7415
9097
|
warn(`Marketplace "${marketplaceName}" local path not found \u2014 skipping.`);
|
|
7416
9098
|
return { marketplace: marketplaceName, skipped: true, message: "Local path not found." };
|
|
7417
9099
|
}
|
|
7418
|
-
const { updated } = pullMarketplace(marketplacePath, m
|
|
9100
|
+
const { updated } = pullMarketplace(marketplacePath, m, marketplaceName);
|
|
7419
9101
|
const pluginChoices = resolvePluginChoices(marketplacePath);
|
|
7420
9102
|
if (pluginChoices.length === 0) {
|
|
7421
9103
|
warn(`No plugins found in marketplace "${marketplaceName}" \u2014 skipping.`);
|
|
@@ -7434,19 +9116,40 @@ function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
|
|
|
7434
9116
|
return { marketplace: marketplaceName, skipped: true, error: message };
|
|
7435
9117
|
}
|
|
7436
9118
|
}
|
|
9119
|
+
function parseProviderOption(value) {
|
|
9120
|
+
if (value === void 0) return void 0;
|
|
9121
|
+
if (!PROVIDERS.includes(value)) {
|
|
9122
|
+
throw new Error(`--provider must be one of: ${PROVIDERS.join(", ")} (got "${value}").`);
|
|
9123
|
+
}
|
|
9124
|
+
return value;
|
|
9125
|
+
}
|
|
9126
|
+
function safeProviderConfig() {
|
|
9127
|
+
try {
|
|
9128
|
+
return loadConfig();
|
|
9129
|
+
} catch {
|
|
9130
|
+
return {};
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
7437
9133
|
function performMarketplaceAdd(url, localPath, opts) {
|
|
7438
|
-
const resolvedPath =
|
|
9134
|
+
const resolvedPath = path11.resolve(localPath ?? defaultMarketplacePath(url));
|
|
7439
9135
|
const marketplaceName = opts.name ?? repoNameFromUrl(url);
|
|
7440
|
-
|
|
9136
|
+
const provider = parseProviderOption(opts.provider);
|
|
9137
|
+
cloneOrReuseMarketplace(url, resolvedPath, { ...opts, provider }, marketplaceName);
|
|
7441
9138
|
const configPath = getGlobalConfigPath();
|
|
7442
9139
|
const { existing, all } = loadMarketplacesConfig(configPath);
|
|
9140
|
+
if (opts.keychain && !opts.token) warn("--keychain has no effect without --token.");
|
|
9141
|
+
const account = `marketplaces.${marketplaceName}.token`;
|
|
9142
|
+
const storeInKeychain = !!(opts.token && opts.keychain);
|
|
7443
9143
|
const entry = {
|
|
7444
9144
|
name: marketplaceName,
|
|
7445
9145
|
repoUrl: url,
|
|
7446
9146
|
localPath: resolvedPath,
|
|
7447
|
-
...opts.token ? { token: opts.token } : {}
|
|
9147
|
+
...opts.token ? { token: storeInKeychain ? keychainRef(account) : opts.token } : {},
|
|
9148
|
+
...opts.username ? { username: opts.username } : {},
|
|
9149
|
+
...provider ? { provider } : {}
|
|
7448
9150
|
};
|
|
7449
9151
|
upsertMarketplace(all, entry);
|
|
9152
|
+
if (storeInKeychain) getKeychainBackend().set(account, opts.token);
|
|
7450
9153
|
saveMarketplaces(configPath, existing, all);
|
|
7451
9154
|
const targets = resolveInstallTargets(opts);
|
|
7452
9155
|
const targetResults = installAllPlugins(entry, resolvedPath, targets);
|
|
@@ -7457,6 +9160,8 @@ function performMarketplaceAdd(url, localPath, opts) {
|
|
|
7457
9160
|
localPath: resolvedPath,
|
|
7458
9161
|
branch: opts.branch ?? null,
|
|
7459
9162
|
tokenConfigured: !!opts.token,
|
|
9163
|
+
tokenStorage: !opts.token ? null : opts.keychain ? "keychain" : "config",
|
|
9164
|
+
provider: detectProvider(url, safeProviderConfig(), provider),
|
|
7460
9165
|
// One target keeps the flat shape earlier versions emitted; several nest under `targets`.
|
|
7461
9166
|
...targetResults.length === 1 ? { plugins: targetResults[0].plugins, total: targetResults[0].total, target: targetResults[0].target } : { targets: targetResults, total: targetResults.reduce((sum, r) => sum + r.total, 0) },
|
|
7462
9167
|
...instructions ? { instructions } : {}
|
|
@@ -7471,12 +9176,22 @@ function performMarketplaceRemove(name) {
|
|
|
7471
9176
|
}
|
|
7472
9177
|
const removed = all.splice(idx, 1)[0];
|
|
7473
9178
|
saveMarketplaces(configPath, existing, all);
|
|
9179
|
+
let keychainEntryDeleted = false;
|
|
9180
|
+
if (isKeychainRef(removed.token)) {
|
|
9181
|
+
try {
|
|
9182
|
+
const account = removed.token.slice("keychain:".length);
|
|
9183
|
+
keychainEntryDeleted = purgeEntries(loadJsonFile(configPath) ?? {}, getKeychainBackend(), [account]).length > 0;
|
|
9184
|
+
} catch (err) {
|
|
9185
|
+
warn(`Could not delete the marketplace's keychain entry: ${err instanceof Error ? err.message : String(err)}`);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
7474
9188
|
return {
|
|
7475
9189
|
removed: {
|
|
7476
9190
|
name: marketplaceLabel(removed),
|
|
7477
9191
|
repoUrl: removed.repoUrl,
|
|
7478
9192
|
localPath: removed.localPath
|
|
7479
9193
|
},
|
|
9194
|
+
keychainEntryDeleted,
|
|
7480
9195
|
remaining: all.length
|
|
7481
9196
|
};
|
|
7482
9197
|
}
|
|
@@ -7493,8 +9208,8 @@ function listBundledSkillDirs() {
|
|
|
7493
9208
|
let skillDirs = [];
|
|
7494
9209
|
if (bundledDir !== "") {
|
|
7495
9210
|
try {
|
|
7496
|
-
skillDirs =
|
|
7497
|
-
(name) =>
|
|
9211
|
+
skillDirs = fs6.readdirSync(bundledDir).filter(
|
|
9212
|
+
(name) => fs6.existsSync(path11.join(bundledDir, name, "SKILL.md"))
|
|
7498
9213
|
);
|
|
7499
9214
|
} catch {
|
|
7500
9215
|
}
|
|
@@ -7505,30 +9220,30 @@ function listBundledSkillDirs() {
|
|
|
7505
9220
|
return skillDirs;
|
|
7506
9221
|
}
|
|
7507
9222
|
function installBundledSkillsTo(targetDir, skillDirs) {
|
|
7508
|
-
const resolvedTarget =
|
|
9223
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
7509
9224
|
const bundledDir = getBundledSkillsDir();
|
|
7510
9225
|
const installed = [];
|
|
7511
9226
|
const failed = [];
|
|
7512
9227
|
for (const skillName of skillDirs) {
|
|
7513
|
-
const existingDir =
|
|
7514
|
-
if (!existingDir.startsWith(resolvedTarget +
|
|
7515
|
-
if (
|
|
7516
|
-
|
|
9228
|
+
const existingDir = path11.resolve(targetDir, skillName);
|
|
9229
|
+
if (!existingDir.startsWith(resolvedTarget + path11.sep)) continue;
|
|
9230
|
+
if (fs6.existsSync(existingDir)) {
|
|
9231
|
+
fs6.rmSync(existingDir, { recursive: true, force: true });
|
|
7517
9232
|
}
|
|
7518
9233
|
}
|
|
7519
9234
|
log(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
|
|
7520
9235
|
for (const skillName of skillDirs) {
|
|
7521
|
-
const skillDir =
|
|
7522
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
9236
|
+
const skillDir = path11.resolve(targetDir, skillName);
|
|
9237
|
+
if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
|
|
7523
9238
|
failed.push(skillName);
|
|
7524
9239
|
continue;
|
|
7525
9240
|
}
|
|
7526
9241
|
try {
|
|
7527
|
-
const mdFiles =
|
|
7528
|
-
|
|
9242
|
+
const mdFiles = fs6.readdirSync(path11.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
|
|
9243
|
+
fs6.mkdirSync(skillDir, { recursive: true });
|
|
7529
9244
|
for (const mdFile of mdFiles) {
|
|
7530
|
-
const content =
|
|
7531
|
-
|
|
9245
|
+
const content = fs6.readFileSync(path11.join(bundledDir, skillName, mdFile), "utf8");
|
|
9246
|
+
fs6.writeFileSync(path11.join(skillDir, mdFile), content, "utf8");
|
|
7532
9247
|
}
|
|
7533
9248
|
installed.push(skillName);
|
|
7534
9249
|
} catch {
|
|
@@ -7563,6 +9278,12 @@ Org plugins (skills and AGENTS.md/CLAUDE.md from a git-hosted marketplace):
|
|
|
7563
9278
|
|
|
7564
9279
|
Check what is installed:
|
|
7565
9280
|
pncli skills status | pncli skills locations | pncli doctor
|
|
9281
|
+
|
|
9282
|
+
Private marketplaces and credentials:
|
|
9283
|
+
pncli skills git-auth enable # let git (and agent hosts) authenticate to marketplace hosts
|
|
9284
|
+
pncli config keychain migrate # move tokens out of plaintext config into the OS keychain
|
|
9285
|
+
|
|
9286
|
+
New to this? pncli skills guide # how skills management fits together (or: guide <section>)
|
|
7566
9287
|
`);
|
|
7567
9288
|
skills.command("install").description("Install the bundled pncli skills (project scope by default; --scope user for every repo)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES}`, DEFAULT_AGENT).option("--scope <scope>", "Installation scope: project | user", "project").option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts, cmd) => {
|
|
7568
9289
|
const start = Date.now();
|
|
@@ -7587,7 +9308,7 @@ Check what is installed:
|
|
|
7587
9308
|
return;
|
|
7588
9309
|
}
|
|
7589
9310
|
const agentName = resolveAgentName(opts);
|
|
7590
|
-
const targetDir = opts.target ?
|
|
9311
|
+
const targetDir = opts.target ? path11.resolve(opts.target) : resolveScopedPath(resolveAgentPaths(agentName), scope);
|
|
7591
9312
|
const { installed, failed } = installBundledSkillsTo(targetDir, skillDirs);
|
|
7592
9313
|
let tracked = false;
|
|
7593
9314
|
if (opts.target && installed.length > 0) {
|
|
@@ -7616,12 +9337,12 @@ Check what is installed:
|
|
|
7616
9337
|
try {
|
|
7617
9338
|
let targetDir;
|
|
7618
9339
|
if (opts.target) {
|
|
7619
|
-
targetDir =
|
|
9340
|
+
targetDir = path11.resolve(opts.target);
|
|
7620
9341
|
} else {
|
|
7621
9342
|
const agentConfig = resolveAgentPaths(resolveAgentName(opts));
|
|
7622
9343
|
targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
|
|
7623
9344
|
}
|
|
7624
|
-
if (!
|
|
9345
|
+
if (!fs6.existsSync(targetDir)) {
|
|
7625
9346
|
success({
|
|
7626
9347
|
skills: [],
|
|
7627
9348
|
total: 0,
|
|
@@ -7632,13 +9353,13 @@ Check what is installed:
|
|
|
7632
9353
|
return;
|
|
7633
9354
|
}
|
|
7634
9355
|
const meta = readInstalledMeta(targetDir);
|
|
7635
|
-
const skillDirs =
|
|
9356
|
+
const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
|
|
7636
9357
|
if (name.startsWith(".")) return false;
|
|
7637
|
-
const skillPath =
|
|
7638
|
-
return
|
|
9358
|
+
const skillPath = path11.join(targetDir, name, "SKILL.md");
|
|
9359
|
+
return fs6.existsSync(skillPath);
|
|
7639
9360
|
});
|
|
7640
9361
|
const skillsList = skillDirs.map((name) => {
|
|
7641
|
-
const content =
|
|
9362
|
+
const content = fs6.readFileSync(path11.join(targetDir, name, "SKILL.md"), "utf8");
|
|
7642
9363
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7643
9364
|
const data = {};
|
|
7644
9365
|
const metadata = {};
|
|
@@ -7691,24 +9412,24 @@ Check what is installed:
|
|
|
7691
9412
|
try {
|
|
7692
9413
|
let targetDir;
|
|
7693
9414
|
if (opts.target) {
|
|
7694
|
-
targetDir =
|
|
9415
|
+
targetDir = path11.resolve(opts.target);
|
|
7695
9416
|
} else {
|
|
7696
9417
|
const agentConfig = resolveAgentPaths(resolveAgentName(opts));
|
|
7697
9418
|
targetDir = resolveScopedPath(agentConfig, opts.scope === "project" ? "project" : "user");
|
|
7698
9419
|
}
|
|
7699
|
-
const resolvedTarget =
|
|
7700
|
-
const skillDir =
|
|
7701
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
9420
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
9421
|
+
const skillDir = path11.resolve(targetDir, name);
|
|
9422
|
+
if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
|
|
7702
9423
|
throw new Error(`Invalid skill name: "${name}"`);
|
|
7703
9424
|
}
|
|
7704
|
-
if (!
|
|
9425
|
+
if (!fs6.existsSync(skillDir)) {
|
|
7705
9426
|
throw new Error(`Skill "${name}" not found at ${skillDir}`);
|
|
7706
9427
|
}
|
|
7707
9428
|
const meta = readInstalledMeta(targetDir);
|
|
7708
9429
|
const record = meta.skills[name];
|
|
7709
|
-
|
|
9430
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
7710
9431
|
delete meta.skills[name];
|
|
7711
|
-
|
|
9432
|
+
fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
|
|
7712
9433
|
success({
|
|
7713
9434
|
uninstalled: name,
|
|
7714
9435
|
target: targetDir,
|
|
@@ -7779,7 +9500,7 @@ Check what is installed:
|
|
|
7779
9500
|
const start = Date.now();
|
|
7780
9501
|
try {
|
|
7781
9502
|
const configPath = getGlobalConfigPath();
|
|
7782
|
-
const resolved =
|
|
9503
|
+
const resolved = path11.resolve(dir);
|
|
7783
9504
|
const forgotten = forgetCustomTarget(configPath, resolved);
|
|
7784
9505
|
if (!forgotten) {
|
|
7785
9506
|
warn(`${resolved} is not a tracked custom target \u2014 nothing to forget.`);
|
|
@@ -7793,6 +9514,44 @@ Check what is installed:
|
|
|
7793
9514
|
fail(err, "skills", "forget-target", start);
|
|
7794
9515
|
}
|
|
7795
9516
|
});
|
|
9517
|
+
skills.command("guide").description("Explain how skills management works: bundled skill vs marketplace plugins, agent hosts, scopes, sync, and auth").argument("[section]", "Print only one section (e.g. hosts, sync, auth, troubleshooting)").option("--sections", "List the section names as JSON instead of printing the guide").action((section, cmdOpts) => {
|
|
9518
|
+
const start = Date.now();
|
|
9519
|
+
try {
|
|
9520
|
+
const guidePath = findGuidePath();
|
|
9521
|
+
if (!guidePath) throw new Error(`${GUIDE_FILE} is missing from this pncli install \u2014 reinstall pncli, or read it online at https://kolatts.github.io/pncli/skills-guide/`);
|
|
9522
|
+
const markdown = fs6.readFileSync(guidePath, "utf8").replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
9523
|
+
if (cmdOpts.sections) {
|
|
9524
|
+
success({ sections: listGuideSections(markdown), path: guidePath }, "skills", "guide", start);
|
|
9525
|
+
return;
|
|
9526
|
+
}
|
|
9527
|
+
if (section) {
|
|
9528
|
+
const text = extractGuideSection(markdown, section);
|
|
9529
|
+
if (!text) {
|
|
9530
|
+
throw new Error(`No guide section matches "${section}". Sections: ${listGuideSections(markdown).map((s) => s.slug).join(", ")}`);
|
|
9531
|
+
}
|
|
9532
|
+
writeRawOutput(text);
|
|
9533
|
+
return;
|
|
9534
|
+
}
|
|
9535
|
+
writeRawOutput(markdown);
|
|
9536
|
+
} catch (err) {
|
|
9537
|
+
fail(err, "skills", "guide", start);
|
|
9538
|
+
}
|
|
9539
|
+
});
|
|
9540
|
+
registerGitAuthCommands(skills);
|
|
9541
|
+
skills.command("git-credential", { hidden: true }).description("Git credential helper (used by git after `skills git-auth enable`; not for direct use)").option("--marketplace <name>", "Answer with this marketplace's credential (written by git-auth enable)").argument("<operation>", "get | store | erase (supplied by git)").action(async (operation, cmdOpts) => {
|
|
9542
|
+
if (operation !== "get") return;
|
|
9543
|
+
try {
|
|
9544
|
+
const chunks = [];
|
|
9545
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
9546
|
+
const request = parseCredentialRequest(Buffer.concat(chunks).toString("utf8"));
|
|
9547
|
+
const configPath = skills.optsWithGlobals().config;
|
|
9548
|
+
const globalConfig = loadJsonFile(getGlobalConfigPath(configPath)) ?? {};
|
|
9549
|
+
const cfg = loadConfig({ configPath, keychainPaths: ["github.token", "bitbucket.pat", "ado.pat"] });
|
|
9550
|
+
const answer = resolveCredential(request, globalConfig, cfg, void 0, cmdOpts.marketplace);
|
|
9551
|
+
if (answer) process.stdout.write(formatCredentialAnswer(answer));
|
|
9552
|
+
} catch {
|
|
9553
|
+
}
|
|
9554
|
+
});
|
|
7796
9555
|
const marketplace = skills.command("marketplace").description("Org plugins from git-hosted marketplaces: add, sync, enable/disable, and shipped AGENTS.md/CLAUDE.md");
|
|
7797
9556
|
marketplace.addHelpText("after", `
|
|
7798
9557
|
Workflow:
|
|
@@ -7810,7 +9569,7 @@ Shipped instructions:
|
|
|
7810
9569
|
|
|
7811
9570
|
Plugin skills always install at user scope. --agent picks the host (default: ${DEFAULT_AGENT}); --all-agents covers all of them.
|
|
7812
9571
|
`);
|
|
7813
|
-
marketplace.command("add").alias("setup").description("Register a new marketplace, clone it, install all its plugins, and apply its shipped AGENTS.md / CLAUDE.md (`setup` is an alias)").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action((url, localPath, opts) => {
|
|
9572
|
+
marketplace.command("add").alias("setup").description("Register a new marketplace, clone it, install all its plugins, and apply its shipped AGENTS.md / CLAUDE.md (`setup` is an alias)").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--keychain", "Store --token in the OS keychain instead of plaintext config (see: pncli config keychain --help)").option("--username <username>", "Username sent with the token (default: x-access-token on github.com, x-token-auth elsewhere; Bitbucket Data Center personal tokens usually need your username)").option("--provider <provider>", "Git host type, when detection guesses wrong: github | bitbucket | ado | git").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action((url, localPath, opts) => {
|
|
7814
9573
|
const marketplaceIdx = process.argv.indexOf("marketplace");
|
|
7815
9574
|
const invokedAs = marketplaceIdx !== -1 && process.argv[marketplaceIdx + 1] === "setup" ? "marketplace-setup" : "marketplace-add";
|
|
7816
9575
|
return marketplaceAddAction(url, localPath, opts, invokedAs);
|
|
@@ -7820,13 +9579,17 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7820
9579
|
try {
|
|
7821
9580
|
const configPath = getGlobalConfigPath();
|
|
7822
9581
|
const all = loadMarketplaces(configPath);
|
|
9582
|
+
const providerCfg = safeProviderConfig();
|
|
7823
9583
|
success({
|
|
7824
9584
|
marketplaces: all.map((m) => ({
|
|
7825
9585
|
name: marketplaceLabel(m),
|
|
7826
9586
|
repoUrl: m.repoUrl,
|
|
7827
9587
|
localPath: m.localPath,
|
|
7828
9588
|
tokenConfigured: !!m.token,
|
|
7829
|
-
|
|
9589
|
+
tokenStorage: !m.token ? null : isKeychainRef(m.token) ? "keychain" : "config",
|
|
9590
|
+
provider: detectProvider(m.repoUrl, providerCfg, m.provider),
|
|
9591
|
+
username: m.username ?? null,
|
|
9592
|
+
upstreamRemote: m.localPath && fs6.existsSync(m.localPath) ? getRepoRemoteUrl(m.localPath) : null
|
|
7830
9593
|
})),
|
|
7831
9594
|
total: all.length
|
|
7832
9595
|
}, "skills", "marketplace-list", start);
|
|
@@ -7834,6 +9597,54 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7834
9597
|
fail(err, "skills", "marketplace-list", start);
|
|
7835
9598
|
}
|
|
7836
9599
|
});
|
|
9600
|
+
marketplace.command("update").description("Change a registered marketplace's token, username, or provider without re-adding it (rotate a token here)").argument("<name>", "Marketplace name (or repo URL)").option("--token <token>", "New access token for clone and pull").option("--keychain", "Store --token in the OS keychain instead of plaintext config").option("--username <username>", "Username sent with the token (Bitbucket Data Center personal tokens usually need your username)").option("--provider <provider>", "Git host type: github | bitbucket | ado | git").option("--clear-token", "Remove the marketplace's own token (falls back to the provider token pncli has, if any)").option("--clear-username", "Go back to the default username").option("--clear-provider", "Go back to automatic provider detection").action((name, opts) => {
|
|
9601
|
+
const start = Date.now();
|
|
9602
|
+
try {
|
|
9603
|
+
if (opts.token && opts.clearToken) throw new Error("Pass --token or --clear-token, not both.");
|
|
9604
|
+
if (opts.keychain && !opts.token) throw new Error("--keychain needs --token.");
|
|
9605
|
+
const provider = parseProviderOption(opts.provider);
|
|
9606
|
+
const configPath = getGlobalConfigPath();
|
|
9607
|
+
const { existing, all } = loadMarketplacesConfig(configPath);
|
|
9608
|
+
const idx = all.findIndex((m) => m.name === name || m.repoUrl === name);
|
|
9609
|
+
if (idx === -1) throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
|
|
9610
|
+
const entry = { ...all[idx] };
|
|
9611
|
+
const oldToken = entry.token;
|
|
9612
|
+
const account = `marketplaces.${marketplaceLabel(entry)}.token`;
|
|
9613
|
+
if (opts.token) {
|
|
9614
|
+
if (opts.keychain) {
|
|
9615
|
+
getKeychainBackend().set(account, opts.token);
|
|
9616
|
+
entry.token = keychainRef(account);
|
|
9617
|
+
} else {
|
|
9618
|
+
entry.token = opts.token;
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
if (opts.clearToken) delete entry.token;
|
|
9622
|
+
if (opts.username) entry.username = opts.username;
|
|
9623
|
+
if (opts.clearUsername) delete entry.username;
|
|
9624
|
+
if (provider) entry.provider = provider;
|
|
9625
|
+
if (opts.clearProvider) delete entry.provider;
|
|
9626
|
+
all[idx] = entry;
|
|
9627
|
+
saveMarketplaces(configPath, existing, all);
|
|
9628
|
+
if (isKeychainRef(oldToken) && oldToken !== entry.token) {
|
|
9629
|
+
try {
|
|
9630
|
+
purgeEntries(loadJsonFile(configPath) ?? {}, getKeychainBackend(), [oldToken.slice("keychain:".length)]);
|
|
9631
|
+
} catch (err) {
|
|
9632
|
+
warn(`Could not delete the old keychain entry: ${err instanceof Error ? err.message : String(err)}`);
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
success({
|
|
9636
|
+
name: marketplaceLabel(entry),
|
|
9637
|
+
repoUrl: entry.repoUrl,
|
|
9638
|
+
tokenConfigured: !!entry.token,
|
|
9639
|
+
tokenStorage: !entry.token ? null : isKeychainRef(entry.token) ? "keychain" : "config",
|
|
9640
|
+
username: entry.username ?? null,
|
|
9641
|
+
provider: detectProvider(entry.repoUrl, safeProviderConfig(), entry.provider),
|
|
9642
|
+
next: opts.username || opts.provider || opts.clearUsername || opts.clearProvider ? `If git-auth is enabled for it, refresh that too: pncli skills git-auth enable --marketplace ${marketplaceLabel(entry)}. Then verify with: pncli doctor` : "Verify with: pncli doctor"
|
|
9643
|
+
}, "skills", "marketplace-update", start);
|
|
9644
|
+
} catch (err) {
|
|
9645
|
+
fail(err, "skills", "marketplace-update", start);
|
|
9646
|
+
}
|
|
9647
|
+
});
|
|
7837
9648
|
marketplace.command("plugins").description("List the plugins available in a registered marketplace").argument("<name>", "Marketplace name (or repo URL) to inspect").action((name) => {
|
|
7838
9649
|
const start = Date.now();
|
|
7839
9650
|
try {
|
|
@@ -7843,7 +9654,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7843
9654
|
if (!found) {
|
|
7844
9655
|
throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
|
|
7845
9656
|
}
|
|
7846
|
-
if (!found.localPath || !
|
|
9657
|
+
if (!found.localPath || !fs6.existsSync(found.localPath)) {
|
|
7847
9658
|
throw new Error(`Marketplace "${marketplaceLabel(found)}" local path not found at ${found.localPath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
|
|
7848
9659
|
}
|
|
7849
9660
|
const plugins = resolvePluginChoices(found.localPath);
|
|
@@ -7868,8 +9679,8 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7868
9679
|
const start = Date.now();
|
|
7869
9680
|
try {
|
|
7870
9681
|
const configPath = getGlobalConfigPath();
|
|
7871
|
-
const
|
|
7872
|
-
if (
|
|
9682
|
+
const allMarketplaces2 = loadMarketplaces(configPath);
|
|
9683
|
+
if (allMarketplaces2.length === 0) {
|
|
7873
9684
|
throw new Error("No marketplaces configured. Run: pncli skills marketplace add <url>");
|
|
7874
9685
|
}
|
|
7875
9686
|
const bareSync = isBareMarketplaceSync(plugin, opts);
|
|
@@ -7877,49 +9688,49 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7877
9688
|
const syncOpts = { force: opts.force ?? false, installedOnly: bareSync ? true : opts.installedOnly ?? false, instructions: opts.instructions !== false };
|
|
7878
9689
|
const targetSummary = targets.length === 1 ? { target: targets[0].target } : { targets: targets.map((t) => t.target) };
|
|
7879
9690
|
if (opts.marketplace === "all" || bareSync) {
|
|
7880
|
-
const results =
|
|
9691
|
+
const results = allMarketplaces2.map((m) => syncMarketplacePlugins(m, targets, plugin ?? "all", syncOpts));
|
|
7881
9692
|
success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
|
|
7882
9693
|
return;
|
|
7883
9694
|
}
|
|
7884
9695
|
let selectedMarketplace;
|
|
7885
9696
|
let selectedPlugin = plugin;
|
|
7886
|
-
const canGoBack = !opts.marketplace &&
|
|
9697
|
+
const canGoBack = !opts.marketplace && allMarketplaces2.length > 1;
|
|
7887
9698
|
if (opts.marketplace) {
|
|
7888
|
-
const found =
|
|
9699
|
+
const found = allMarketplaces2.find((m) => m.name === opts.marketplace || m.repoUrl === opts.marketplace);
|
|
7889
9700
|
if (!found) {
|
|
7890
9701
|
throw new Error(`Marketplace "${opts.marketplace}" not found. Run: pncli skills marketplace list`);
|
|
7891
9702
|
}
|
|
7892
9703
|
selectedMarketplace = found;
|
|
7893
|
-
} else if (
|
|
7894
|
-
selectedMarketplace =
|
|
9704
|
+
} else if (allMarketplaces2.length === 1) {
|
|
9705
|
+
selectedMarketplace = allMarketplaces2[0];
|
|
7895
9706
|
}
|
|
7896
9707
|
for (; ; ) {
|
|
7897
9708
|
if (!selectedMarketplace) {
|
|
7898
|
-
assertInteractive(`Multiple marketplaces are registered (${
|
|
9709
|
+
assertInteractive(`Multiple marketplaces are registered (${allMarketplaces2.map(marketplaceLabel).join(", ")}). Pass --marketplace <name> (or --marketplace all) to run this non-interactively.`);
|
|
7899
9710
|
const chosen = await select({
|
|
7900
9711
|
message: "Select a marketplace to sync:",
|
|
7901
9712
|
choices: [
|
|
7902
9713
|
{ value: ALL_MARKETPLACES, name: "All marketplaces \u2014 sync every plugin from every marketplace" },
|
|
7903
9714
|
// Index-based values: labels alone aren't guaranteed unique (two marketplaces can share a derived name).
|
|
7904
|
-
...
|
|
9715
|
+
...allMarketplaces2.map((m2, i) => ({ value: String(i), name: `${marketplaceLabel(m2)} \u2014 ${m2.repoUrl ?? ""}` }))
|
|
7905
9716
|
]
|
|
7906
9717
|
});
|
|
7907
9718
|
if (chosen === ALL_MARKETPLACES) {
|
|
7908
|
-
const results =
|
|
9719
|
+
const results = allMarketplaces2.map((m2) => syncMarketplacePlugins(m2, targets, selectedPlugin ?? "all", syncOpts));
|
|
7909
9720
|
success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
|
|
7910
9721
|
return;
|
|
7911
9722
|
}
|
|
7912
|
-
selectedMarketplace =
|
|
9723
|
+
selectedMarketplace = allMarketplaces2[Number(chosen)];
|
|
7913
9724
|
if (!selectedMarketplace) {
|
|
7914
9725
|
throw new Error(`Marketplace "${chosen}" not found.`);
|
|
7915
9726
|
}
|
|
7916
9727
|
}
|
|
7917
9728
|
const marketplaceName = marketplaceLabel(selectedMarketplace);
|
|
7918
9729
|
const marketplacePath = selectedMarketplace.localPath;
|
|
7919
|
-
if (!marketplacePath || !
|
|
9730
|
+
if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
|
|
7920
9731
|
throw new Error(`Marketplace "${marketplaceName}" local path not found at ${marketplacePath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
|
|
7921
9732
|
}
|
|
7922
|
-
const { updated } = pullMarketplace(marketplacePath, selectedMarketplace
|
|
9733
|
+
const { updated } = pullMarketplace(marketplacePath, selectedMarketplace, marketplaceName);
|
|
7923
9734
|
const pluginChoices = resolvePluginChoices(marketplacePath);
|
|
7924
9735
|
if (pluginChoices.length === 0) {
|
|
7925
9736
|
throw new Error(`No plugins found in marketplace "${marketplaceName}". Check the marketplace repository structure.`);
|
|
@@ -7973,7 +9784,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7973
9784
|
const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
|
|
7974
9785
|
const agents = Object.keys(AGENT_PATHS);
|
|
7975
9786
|
const marketplaces = selected.map((m) => {
|
|
7976
|
-
const shipped = m.localPath &&
|
|
9787
|
+
const shipped = m.localPath && fs6.existsSync(m.localPath) ? findMarketplaceInstructions(m.localPath).map((s) => s.file) : [];
|
|
7977
9788
|
return {
|
|
7978
9789
|
marketplace: marketplaceLabel(m),
|
|
7979
9790
|
localPath: m.localPath ?? null,
|
|
@@ -7999,7 +9810,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7999
9810
|
const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
|
|
8000
9811
|
const results = selected.map((m) => {
|
|
8001
9812
|
const name = marketplaceLabel(m);
|
|
8002
|
-
if (!m.localPath || !
|
|
9813
|
+
if (!m.localPath || !fs6.existsSync(m.localPath)) {
|
|
8003
9814
|
return { marketplace: name, skipped: true, message: "Local path not found. Run: pncli skills marketplace add <url>" };
|
|
8004
9815
|
}
|
|
8005
9816
|
if (findMarketplaceInstructions(m.localPath).length === 0) {
|
|
@@ -8037,31 +9848,31 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8037
9848
|
try {
|
|
8038
9849
|
let targetDir;
|
|
8039
9850
|
if (opts.target) {
|
|
8040
|
-
targetDir =
|
|
9851
|
+
targetDir = path11.resolve(opts.target);
|
|
8041
9852
|
} else {
|
|
8042
9853
|
const agentConfig = resolveAgentPaths(resolveAgentName(opts));
|
|
8043
9854
|
targetDir = resolveScopedPath(agentConfig, opts.scope ?? "user");
|
|
8044
9855
|
}
|
|
8045
|
-
const resolvedTarget =
|
|
8046
|
-
if (!
|
|
9856
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
9857
|
+
if (!fs6.existsSync(targetDir)) {
|
|
8047
9858
|
success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
|
|
8048
9859
|
return;
|
|
8049
9860
|
}
|
|
8050
9861
|
const meta = readInstalledMeta(targetDir);
|
|
8051
9862
|
const removed = [];
|
|
8052
9863
|
const skipped = [];
|
|
8053
|
-
const skillDirs =
|
|
9864
|
+
const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
|
|
8054
9865
|
if (name.startsWith(".")) return false;
|
|
8055
|
-
const p =
|
|
9866
|
+
const p = path11.join(targetDir, name);
|
|
8056
9867
|
try {
|
|
8057
|
-
return
|
|
9868
|
+
return fs6.statSync(p).isDirectory();
|
|
8058
9869
|
} catch {
|
|
8059
9870
|
return false;
|
|
8060
9871
|
}
|
|
8061
9872
|
});
|
|
8062
9873
|
for (const skillName of skillDirs) {
|
|
8063
|
-
const skillDir =
|
|
8064
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
9874
|
+
const skillDir = path11.resolve(targetDir, skillName);
|
|
9875
|
+
if (!skillDir.startsWith(resolvedTarget + path11.sep)) continue;
|
|
8065
9876
|
let record = meta.skills[skillName] ?? null;
|
|
8066
9877
|
if (!record) {
|
|
8067
9878
|
const perSkill = readSkillOrigin(skillDir);
|
|
@@ -8086,12 +9897,12 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8086
9897
|
skipped.push(skillName);
|
|
8087
9898
|
continue;
|
|
8088
9899
|
}
|
|
8089
|
-
|
|
9900
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
8090
9901
|
delete meta.skills[skillName];
|
|
8091
9902
|
removed.push(skillName);
|
|
8092
9903
|
}
|
|
8093
9904
|
if (removed.length > 0) {
|
|
8094
|
-
|
|
9905
|
+
fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
|
|
8095
9906
|
}
|
|
8096
9907
|
success({
|
|
8097
9908
|
removed,
|
|
@@ -8109,7 +9920,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8109
9920
|
const start = Date.now();
|
|
8110
9921
|
try {
|
|
8111
9922
|
const targetDir = resolveTargetDir(opts);
|
|
8112
|
-
if (!
|
|
9923
|
+
if (!fs6.existsSync(targetDir)) {
|
|
8113
9924
|
success({ disabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-disable", start);
|
|
8114
9925
|
return;
|
|
8115
9926
|
}
|
|
@@ -8121,7 +9932,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8121
9932
|
total: result.disabled.length,
|
|
8122
9933
|
target: targetDir,
|
|
8123
9934
|
plugin,
|
|
8124
|
-
stash:
|
|
9935
|
+
stash: path11.join(targetDir, DISABLED_SUBDIR),
|
|
8125
9936
|
...opts.marketplace ? { marketplace: opts.marketplace } : {}
|
|
8126
9937
|
}, "skills", "marketplace-disable", start);
|
|
8127
9938
|
} catch (err) {
|
|
@@ -8132,7 +9943,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8132
9943
|
const start = Date.now();
|
|
8133
9944
|
try {
|
|
8134
9945
|
const targetDir = resolveTargetDir(opts);
|
|
8135
|
-
if (!
|
|
9946
|
+
if (!fs6.existsSync(targetDir)) {
|
|
8136
9947
|
success({ enabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-enable", start);
|
|
8137
9948
|
return;
|
|
8138
9949
|
}
|
|
@@ -8171,9 +9982,9 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8171
9982
|
const appliedInstructions = /* @__PURE__ */ new Map();
|
|
8172
9983
|
const instructionAgents = opts.target ? [] : [resolveAgentName(opts)];
|
|
8173
9984
|
for (; ; ) {
|
|
8174
|
-
const states =
|
|
9985
|
+
const states = fs6.existsSync(targetDir) ? listPluginStates(targetDir) : [];
|
|
8175
9986
|
const registered = loadMarketplaces(getGlobalConfigPath());
|
|
8176
|
-
const shipping = registered.filter((m) => m.localPath &&
|
|
9987
|
+
const shipping = registered.filter((m) => m.localPath && fs6.existsSync(m.localPath) && findMarketplaceInstructions(m.localPath).length > 0);
|
|
8177
9988
|
const action = await select({
|
|
8178
9989
|
message: `Manage skills marketplaces (target: ${targetDir}):`,
|
|
8179
9990
|
choices: [
|
|
@@ -8287,15 +10098,15 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8287
10098
|
const start = Date.now();
|
|
8288
10099
|
try {
|
|
8289
10100
|
const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
|
|
8290
|
-
if (!
|
|
10101
|
+
if (!fs6.existsSync(userDir)) {
|
|
8291
10102
|
success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
|
|
8292
10103
|
return;
|
|
8293
10104
|
}
|
|
8294
|
-
const skillDirs =
|
|
10105
|
+
const skillDirs = fs6.readdirSync(userDir).filter((name) => {
|
|
8295
10106
|
if (name.startsWith(".")) return false;
|
|
8296
|
-
const p =
|
|
10107
|
+
const p = path11.join(userDir, name);
|
|
8297
10108
|
try {
|
|
8298
|
-
return
|
|
10109
|
+
return fs6.statSync(p).isDirectory();
|
|
8299
10110
|
} catch {
|
|
8300
10111
|
return false;
|
|
8301
10112
|
}
|
|
@@ -8311,13 +10122,13 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
|
|
|
8311
10122
|
);
|
|
8312
10123
|
}
|
|
8313
10124
|
for (const name of skillDirs) {
|
|
8314
|
-
const skillDir =
|
|
8315
|
-
if (!skillDir.startsWith(
|
|
8316
|
-
|
|
10125
|
+
const skillDir = path11.resolve(userDir, name);
|
|
10126
|
+
if (!skillDir.startsWith(path11.resolve(userDir) + path11.sep)) continue;
|
|
10127
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
8317
10128
|
}
|
|
8318
10129
|
const metaPath = getInstalledMetaPath(userDir);
|
|
8319
|
-
if (
|
|
8320
|
-
|
|
10130
|
+
if (fs6.existsSync(metaPath)) {
|
|
10131
|
+
fs6.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
|
|
8321
10132
|
}
|
|
8322
10133
|
success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
|
|
8323
10134
|
} catch (err) {
|
|
@@ -8326,46 +10137,46 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
|
|
|
8326
10137
|
});
|
|
8327
10138
|
}
|
|
8328
10139
|
function resolvePluginChoices(marketplacePath) {
|
|
8329
|
-
const marketplaceJsonPath =
|
|
8330
|
-
if (
|
|
10140
|
+
const marketplaceJsonPath = path11.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
10141
|
+
if (fs6.existsSync(marketplaceJsonPath)) {
|
|
8331
10142
|
try {
|
|
8332
|
-
const meta = JSON.parse(
|
|
10143
|
+
const meta = JSON.parse(fs6.readFileSync(marketplaceJsonPath, "utf8"));
|
|
8333
10144
|
if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
|
|
8334
10145
|
return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
|
|
8335
10146
|
}
|
|
8336
10147
|
} catch {
|
|
8337
10148
|
}
|
|
8338
10149
|
}
|
|
8339
|
-
const pluginsDir =
|
|
8340
|
-
if (
|
|
8341
|
-
return
|
|
10150
|
+
const pluginsDir = path11.join(marketplacePath, "plugins");
|
|
10151
|
+
if (fs6.existsSync(pluginsDir)) {
|
|
10152
|
+
return fs6.readdirSync(pluginsDir).filter((name) => fs6.statSync(path11.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
|
|
8342
10153
|
}
|
|
8343
10154
|
return [];
|
|
8344
10155
|
}
|
|
8345
10156
|
function resolveSkillsSrc(marketplacePath, selectedPlugin) {
|
|
8346
|
-
const pluginsBase =
|
|
8347
|
-
const skillsSrc =
|
|
8348
|
-
if (!skillsSrc.startsWith(pluginsBase +
|
|
10157
|
+
const pluginsBase = path11.resolve(marketplacePath, "plugins");
|
|
10158
|
+
const skillsSrc = path11.resolve(pluginsBase, selectedPlugin, "skills");
|
|
10159
|
+
if (!skillsSrc.startsWith(pluginsBase + path11.sep)) {
|
|
8349
10160
|
throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
|
|
8350
10161
|
}
|
|
8351
10162
|
return skillsSrc;
|
|
8352
10163
|
}
|
|
8353
10164
|
function copyPluginSkills(skillsSrc, targetDir, meta) {
|
|
8354
|
-
|
|
8355
|
-
const resolvedTarget =
|
|
8356
|
-
const skillNames =
|
|
8357
|
-
(name) =>
|
|
10165
|
+
fs6.mkdirSync(targetDir, { recursive: true });
|
|
10166
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
10167
|
+
const skillNames = fs6.readdirSync(skillsSrc).filter(
|
|
10168
|
+
(name) => fs6.statSync(path11.join(skillsSrc, name)).isDirectory()
|
|
8358
10169
|
);
|
|
8359
10170
|
const installed = [];
|
|
8360
10171
|
const failed = [];
|
|
8361
10172
|
for (const skillName of skillNames) {
|
|
8362
|
-
const dest =
|
|
8363
|
-
if (!dest.startsWith(resolvedTarget +
|
|
10173
|
+
const dest = path11.resolve(targetDir, skillName);
|
|
10174
|
+
if (!dest.startsWith(resolvedTarget + path11.sep)) {
|
|
8364
10175
|
failed.push(skillName);
|
|
8365
10176
|
continue;
|
|
8366
10177
|
}
|
|
8367
|
-
|
|
8368
|
-
|
|
10178
|
+
fs6.rmSync(dest, { recursive: true, force: true });
|
|
10179
|
+
fs6.cpSync(path11.join(skillsSrc, skillName), dest, { recursive: true });
|
|
8369
10180
|
installed.push(skillName);
|
|
8370
10181
|
}
|
|
8371
10182
|
if (meta) {
|
|
@@ -8382,9 +10193,10 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
|
|
|
8382
10193
|
|
|
8383
10194
|
// src/services/config/commands.ts
|
|
8384
10195
|
import chalk from "chalk";
|
|
8385
|
-
import
|
|
10196
|
+
import fs7 from "fs";
|
|
8386
10197
|
function registerConfigCommands(program2) {
|
|
8387
10198
|
const config = program2.command("config").description("Manage pncli configuration");
|
|
10199
|
+
registerKeychainCommands(config, program2);
|
|
8388
10200
|
config.command("init").description("Setup wizard for service credentials (interactive)").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
|
|
8389
10201
|
const start = Date.now();
|
|
8390
10202
|
try {
|
|
@@ -8496,8 +10308,8 @@ function registerConfigCommands(program2) {
|
|
|
8496
10308
|
if (cfg.ado.baseUrl) {
|
|
8497
10309
|
try {
|
|
8498
10310
|
const collection = cfg.defaults.ado?.collection;
|
|
8499
|
-
const
|
|
8500
|
-
await http.ado(
|
|
10311
|
+
const path14 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
|
|
10312
|
+
await http.ado(path14);
|
|
8501
10313
|
results.ado = { ok: true, message: "connected" };
|
|
8502
10314
|
} catch (err) {
|
|
8503
10315
|
results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
@@ -8790,7 +10602,7 @@ async function initGlobalConfig(start) {
|
|
|
8790
10602
|
message: "Jira base URL (e.g. jira.imagile.dev):",
|
|
8791
10603
|
default: ""
|
|
8792
10604
|
});
|
|
8793
|
-
const jiraApiToken = await
|
|
10605
|
+
const jiraApiToken = await password2({
|
|
8794
10606
|
message: "Jira personal access token:"
|
|
8795
10607
|
});
|
|
8796
10608
|
process.stderr.write("\n\u2500\u2500 Bitbucket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -8798,7 +10610,7 @@ async function initGlobalConfig(start) {
|
|
|
8798
10610
|
message: "Bitbucket Server base URL (e.g. bitbucket.imagile.dev):",
|
|
8799
10611
|
default: ""
|
|
8800
10612
|
});
|
|
8801
|
-
const bitbucketPat = await
|
|
10613
|
+
const bitbucketPat = await password2({
|
|
8802
10614
|
message: "Bitbucket personal access token:"
|
|
8803
10615
|
});
|
|
8804
10616
|
process.stderr.write("\n\u2500\u2500 GitHub \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -8813,7 +10625,7 @@ async function initGlobalConfig(start) {
|
|
|
8813
10625
|
message: "GitHub API base URL\n GitHub.com: api.github.com\n GitHub Enterprise: <host>/api/v3\n URL: ",
|
|
8814
10626
|
default: ""
|
|
8815
10627
|
});
|
|
8816
|
-
githubToken = await
|
|
10628
|
+
githubToken = await password2({
|
|
8817
10629
|
message: "GitHub personal access token (classic or fine-grained):"
|
|
8818
10630
|
});
|
|
8819
10631
|
if (githubBaseUrl && githubToken) {
|
|
@@ -8840,7 +10652,7 @@ async function initGlobalConfig(start) {
|
|
|
8840
10652
|
message: "Confluence base URL (e.g. confluence.imagile.dev):",
|
|
8841
10653
|
default: ""
|
|
8842
10654
|
});
|
|
8843
|
-
const confluenceApiToken = await
|
|
10655
|
+
const confluenceApiToken = await password2({
|
|
8844
10656
|
message: "Confluence personal access token:"
|
|
8845
10657
|
});
|
|
8846
10658
|
process.stderr.write("\n\u2500\u2500 Artifactory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -8858,7 +10670,7 @@ async function initGlobalConfig(start) {
|
|
|
8858
10670
|
message: "Artifactory base URL (e.g. artifactory.imagile.dev):",
|
|
8859
10671
|
default: ""
|
|
8860
10672
|
});
|
|
8861
|
-
artifactoryToken = await
|
|
10673
|
+
artifactoryToken = await password2({
|
|
8862
10674
|
message: "Artifactory API token:"
|
|
8863
10675
|
});
|
|
8864
10676
|
process.stderr.write("\nConfigure which ecosystems you use (skip any that don't apply):\n");
|
|
@@ -8887,7 +10699,7 @@ async function initGlobalConfig(start) {
|
|
|
8887
10699
|
message: "SonarQube Server base URL (e.g. sonar.imagile.dev):",
|
|
8888
10700
|
default: ""
|
|
8889
10701
|
});
|
|
8890
|
-
sonarToken = await
|
|
10702
|
+
sonarToken = await password2({
|
|
8891
10703
|
message: "SonarQube personal access token:"
|
|
8892
10704
|
});
|
|
8893
10705
|
}
|
|
@@ -8905,7 +10717,7 @@ async function initGlobalConfig(start) {
|
|
|
8905
10717
|
default: false
|
|
8906
10718
|
});
|
|
8907
10719
|
if (useSdeConnectionString) {
|
|
8908
|
-
const sdeConnection = await
|
|
10720
|
+
const sdeConnection = await password2({
|
|
8909
10721
|
message: "SDElements connection string (api-token@hostname, e.g. mytoken@myorg.sdelements.com):"
|
|
8910
10722
|
});
|
|
8911
10723
|
const atIdx = sdeConnection.indexOf("@");
|
|
@@ -8918,7 +10730,7 @@ async function initGlobalConfig(start) {
|
|
|
8918
10730
|
message: "SDElements hostname\n Cloud-hosted: imagile.sdelements.com\n On-premise: sde.imagile.dev\n Host: ",
|
|
8919
10731
|
default: ""
|
|
8920
10732
|
});
|
|
8921
|
-
sdeToken = await
|
|
10733
|
+
sdeToken = await password2({
|
|
8922
10734
|
message: "SDElements API token:"
|
|
8923
10735
|
});
|
|
8924
10736
|
}
|
|
@@ -8940,7 +10752,7 @@ async function initGlobalConfig(start) {
|
|
|
8940
10752
|
message: "Azure DevOps Server base URL (e.g. tfs.imagile.dev or devops.imagile.dev/tfs):",
|
|
8941
10753
|
default: ""
|
|
8942
10754
|
});
|
|
8943
|
-
adoPat = await
|
|
10755
|
+
adoPat = await password2({
|
|
8944
10756
|
message: "Azure DevOps personal access token:"
|
|
8945
10757
|
});
|
|
8946
10758
|
adoCollection = await input2({
|
|
@@ -9016,7 +10828,7 @@ async function initGlobalConfig(start) {
|
|
|
9016
10828
|
message: "Jenkins username:",
|
|
9017
10829
|
default: ""
|
|
9018
10830
|
});
|
|
9019
|
-
jenkinsApiToken = await
|
|
10831
|
+
jenkinsApiToken = await password2({
|
|
9020
10832
|
message: "Jenkins API token:"
|
|
9021
10833
|
});
|
|
9022
10834
|
}
|
|
@@ -9039,7 +10851,7 @@ async function initGlobalConfig(start) {
|
|
|
9039
10851
|
message: "Checkmarx One tenant name (IAM realm, e.g. imagile):",
|
|
9040
10852
|
default: ""
|
|
9041
10853
|
});
|
|
9042
|
-
checkmarxApiKey = await
|
|
10854
|
+
checkmarxApiKey = await password2({
|
|
9043
10855
|
message: "Checkmarx One API key (leave blank to use an OAuth client):"
|
|
9044
10856
|
});
|
|
9045
10857
|
if (!checkmarxApiKey) {
|
|
@@ -9047,7 +10859,7 @@ async function initGlobalConfig(start) {
|
|
|
9047
10859
|
message: "Checkmarx One OAuth client ID:",
|
|
9048
10860
|
default: ""
|
|
9049
10861
|
});
|
|
9050
|
-
checkmarxClientSecret = await
|
|
10862
|
+
checkmarxClientSecret = await password2({
|
|
9051
10863
|
message: "Checkmarx One OAuth client secret:",
|
|
9052
10864
|
validate: (v) => v.length > 0 || "Client secret cannot be blank"
|
|
9053
10865
|
});
|
|
@@ -9092,7 +10904,7 @@ async function initGlobalConfig(start) {
|
|
|
9092
10904
|
message: "User Code (from User Token):",
|
|
9093
10905
|
default: ""
|
|
9094
10906
|
});
|
|
9095
|
-
sonatypeIqPasscode = await
|
|
10907
|
+
sonatypeIqPasscode = await password2({
|
|
9096
10908
|
message: "Passcode (from User Token):"
|
|
9097
10909
|
});
|
|
9098
10910
|
if (sonatypeIqBaseUrl && sonatypeIqUserCode && sonatypeIqPasscode) {
|
|
@@ -9140,10 +10952,10 @@ async function initGlobalConfig(start) {
|
|
|
9140
10952
|
message: "Username (email):",
|
|
9141
10953
|
default: ""
|
|
9142
10954
|
});
|
|
9143
|
-
contrastApiKey = await
|
|
10955
|
+
contrastApiKey = await password2({
|
|
9144
10956
|
message: "API key:"
|
|
9145
10957
|
});
|
|
9146
|
-
contrastServiceKey = await
|
|
10958
|
+
contrastServiceKey = await password2({
|
|
9147
10959
|
message: "Service key:"
|
|
9148
10960
|
});
|
|
9149
10961
|
const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
|
|
@@ -9182,7 +10994,7 @@ async function initGlobalConfig(start) {
|
|
|
9182
10994
|
message: "OpenShift API server URL (e.g. https://api.cluster.imagile.dev:6443):",
|
|
9183
10995
|
default: ""
|
|
9184
10996
|
});
|
|
9185
|
-
openShiftToken = await
|
|
10997
|
+
openShiftToken = await password2({
|
|
9186
10998
|
message: "Service account token:"
|
|
9187
10999
|
});
|
|
9188
11000
|
if (openShiftBaseUrl && openShiftToken) {
|
|
@@ -9218,7 +11030,7 @@ async function initGlobalConfig(start) {
|
|
|
9218
11030
|
message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
|
|
9219
11031
|
default: ""
|
|
9220
11032
|
});
|
|
9221
|
-
dynatraceApiToken = await
|
|
11033
|
+
dynatraceApiToken = await password2({
|
|
9222
11034
|
message: "Environment API token (entities.read and problems.read):"
|
|
9223
11035
|
});
|
|
9224
11036
|
const configureTraces = await confirm({
|
|
@@ -9230,7 +11042,7 @@ async function initGlobalConfig(start) {
|
|
|
9230
11042
|
message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
|
|
9231
11043
|
default: ""
|
|
9232
11044
|
});
|
|
9233
|
-
dynatracePlatformToken = await
|
|
11045
|
+
dynatracePlatformToken = await password2({
|
|
9234
11046
|
message: "Platform token with Grail span read permissions:"
|
|
9235
11047
|
});
|
|
9236
11048
|
}
|
|
@@ -9269,7 +11081,7 @@ async function initGlobalConfig(start) {
|
|
|
9269
11081
|
message: "LogScale base URL (e.g. https://logscale.imagile.dev):",
|
|
9270
11082
|
default: ""
|
|
9271
11083
|
});
|
|
9272
|
-
logscaleToken = await
|
|
11084
|
+
logscaleToken = await password2({
|
|
9273
11085
|
message: "LogScale personal access token:"
|
|
9274
11086
|
});
|
|
9275
11087
|
if (logscaleBaseUrl && logscaleToken) {
|
|
@@ -9303,7 +11115,7 @@ async function initGlobalConfig(start) {
|
|
|
9303
11115
|
message: "Split.IO Admin API base URL (e.g. https://api.split.io):",
|
|
9304
11116
|
default: ""
|
|
9305
11117
|
});
|
|
9306
|
-
splitioAdminApiKey = await
|
|
11118
|
+
splitioAdminApiKey = await password2({
|
|
9307
11119
|
message: "Split.IO Admin API key:"
|
|
9308
11120
|
});
|
|
9309
11121
|
if (splitioBaseUrl && splitioAdminApiKey) {
|
|
@@ -9333,7 +11145,7 @@ async function initGlobalConfig(start) {
|
|
|
9333
11145
|
let figmaToken = "";
|
|
9334
11146
|
if (useFigma) {
|
|
9335
11147
|
process.stderr.write(" Generate a personal access token in Figma under Account Settings \u2192 Personal access tokens.\n");
|
|
9336
|
-
figmaToken = await
|
|
11148
|
+
figmaToken = await password2({
|
|
9337
11149
|
message: "Figma personal access token:"
|
|
9338
11150
|
});
|
|
9339
11151
|
if (figmaToken) {
|
|
@@ -9370,7 +11182,7 @@ async function initGlobalConfig(start) {
|
|
|
9370
11182
|
message: "Alation base URL (e.g. https://alation.imagile.dev):",
|
|
9371
11183
|
validate: (v) => v.trim().length > 0 || "Required"
|
|
9372
11184
|
});
|
|
9373
|
-
alationRefreshToken = await
|
|
11185
|
+
alationRefreshToken = await password2({
|
|
9374
11186
|
message: "Alation refresh token:"
|
|
9375
11187
|
});
|
|
9376
11188
|
alationUserId = await input2({
|
|
@@ -9426,6 +11238,11 @@ async function initGlobalConfig(start) {
|
|
|
9426
11238
|
const existingOpenshift = existingGlobal?.openshift;
|
|
9427
11239
|
writeGlobalConfig({
|
|
9428
11240
|
jenkinsInstances: existingInstances,
|
|
11241
|
+
// Not prompted for here, so carry them over: dropping gitAuth strands `git-auth disable`
|
|
11242
|
+
// (it restores from the recorded backups), and marketplaces/skillsTargets hold tokens and paths.
|
|
11243
|
+
...existingGlobal?.marketplaces ? { marketplaces: existingGlobal.marketplaces } : {},
|
|
11244
|
+
...existingGlobal?.skillsTargets ? { skillsTargets: existingGlobal.skillsTargets } : {},
|
|
11245
|
+
...existingGlobal?.gitAuth ? { gitAuth: existingGlobal.gitAuth } : {},
|
|
9429
11246
|
user: {
|
|
9430
11247
|
email: userEmail || void 0,
|
|
9431
11248
|
userId: userId || void 0
|
|
@@ -9628,7 +11445,7 @@ async function initRepoConfig(start) {
|
|
|
9628
11445
|
process.exitCode = ExitCode.SUCCESS;
|
|
9629
11446
|
return;
|
|
9630
11447
|
}
|
|
9631
|
-
if (
|
|
11448
|
+
if (fs7.existsSync(".pncli.json")) {
|
|
9632
11449
|
const overwrite = await confirm({
|
|
9633
11450
|
message: ".pncli.json already exists. Overwrite?",
|
|
9634
11451
|
default: false
|
|
@@ -9662,14 +11479,172 @@ async function initRepoConfig(start) {
|
|
|
9662
11479
|
}
|
|
9663
11480
|
|
|
9664
11481
|
// src/services/doctor/commands.ts
|
|
9665
|
-
import
|
|
9666
|
-
import
|
|
11482
|
+
import fs9 from "fs";
|
|
11483
|
+
import path12 from "path";
|
|
11484
|
+
|
|
11485
|
+
// src/services/doctor/git-auth-checks.ts
|
|
11486
|
+
import fs8 from "fs";
|
|
11487
|
+
var TOKEN_EXPIRY_WARNING_DAYS = 14;
|
|
11488
|
+
function checkKeychain(rawGlobalConfig, backend = getKeychainBackend(), configPath) {
|
|
11489
|
+
const refs = findKeychainRefs(rawGlobalConfig);
|
|
11490
|
+
const plaintext = findPlaintextSecrets(rawGlobalConfig).map((s) => s.path.join("."));
|
|
11491
|
+
let unresolved = [];
|
|
11492
|
+
if (refs.length > 0) {
|
|
11493
|
+
resolveKeychainRefs(rawGlobalConfig, backend);
|
|
11494
|
+
unresolved = getUnresolvedKeychainRefs();
|
|
11495
|
+
}
|
|
11496
|
+
const available = refs.length > 0 ? backend.available() : null;
|
|
11497
|
+
const backupPath = configPath ? `${configPath}.pre-keychain.bak` : null;
|
|
11498
|
+
return {
|
|
11499
|
+
backend: backend.name,
|
|
11500
|
+
store: backend.store,
|
|
11501
|
+
available,
|
|
11502
|
+
references: refs.length,
|
|
11503
|
+
unresolved,
|
|
11504
|
+
plaintextSecrets: plaintext,
|
|
11505
|
+
plaintextBackup: backupPath && fs8.existsSync(backupPath) ? backupPath : null,
|
|
11506
|
+
hint: plaintext.length > 0 && backend.name !== "none" ? `${plaintext.length} secret(s) are stored in plaintext config. Move them into the ${backend.store} with: pncli config keychain migrate` : null
|
|
11507
|
+
};
|
|
11508
|
+
}
|
|
11509
|
+
async function checkGitAuth(rawGlobalConfig, cfg, offline, fetchFn = fetch, git = defaultGitRunner) {
|
|
11510
|
+
const reports = [];
|
|
11511
|
+
const inspected = /* @__PURE__ */ new Map();
|
|
11512
|
+
const byName = new Map(allMarketplaces(rawGlobalConfig).map((m) => [m.name ?? m.repoUrl, m]));
|
|
11513
|
+
for (const local of describeAllGitAuth(rawGlobalConfig, cfg, git)) {
|
|
11514
|
+
let access = null;
|
|
11515
|
+
let token = null;
|
|
11516
|
+
const m = local.marketplace ? byName.get(local.marketplace) : void 0;
|
|
11517
|
+
if (!offline && m?.repoUrl && !local.credentialError) {
|
|
11518
|
+
let auth = null;
|
|
11519
|
+
try {
|
|
11520
|
+
auth = resolveMarketplaceAuth(m, cfg);
|
|
11521
|
+
} catch {
|
|
11522
|
+
}
|
|
11523
|
+
access = lsRemote(m.repoUrl, auth, git, { keepHelpers: !auth });
|
|
11524
|
+
if (auth && auth.provider === "github") {
|
|
11525
|
+
const apiBase = apiBaseForHost(local.host, cfg.github?.baseUrl);
|
|
11526
|
+
const key = `${apiBase}\0${auth.password}`;
|
|
11527
|
+
if (!inspected.has(key)) inspected.set(key, await inspectGitHubToken(apiBase, auth.password, fetchFn));
|
|
11528
|
+
token = inspected.get(key);
|
|
11529
|
+
}
|
|
11530
|
+
}
|
|
11531
|
+
reports.push({ ...local, access, token });
|
|
11532
|
+
}
|
|
11533
|
+
return reports;
|
|
11534
|
+
}
|
|
11535
|
+
function buildKeychainProblems(report) {
|
|
11536
|
+
const problems = report.unresolved.map((u) => ({
|
|
11537
|
+
area: "keychain",
|
|
11538
|
+
message: `${u.path} points at keychain entry "${u.account}", which could not be read (${u.reason})`,
|
|
11539
|
+
fix: `Store it again with: pncli config keychain set ${u.path} (or move it back to plaintext: pncli config keychain migrate --to config ${u.path})`
|
|
11540
|
+
}));
|
|
11541
|
+
if (report.plaintextBackup) {
|
|
11542
|
+
problems.push({
|
|
11543
|
+
area: "keychain",
|
|
11544
|
+
message: `A plaintext copy of your pre-migration config (every secret in it) is still on disk: ${report.plaintextBackup}`,
|
|
11545
|
+
fix: "Once `pncli config check` passes, delete that file."
|
|
11546
|
+
});
|
|
11547
|
+
}
|
|
11548
|
+
return problems;
|
|
11549
|
+
}
|
|
11550
|
+
function rotateFix(source) {
|
|
11551
|
+
if (source?.startsWith("marketplace:")) {
|
|
11552
|
+
const name = source.slice("marketplace:".length);
|
|
11553
|
+
return `Regenerate it, then: pncli skills marketplace update ${name} --token <new-token> [--keychain]`;
|
|
11554
|
+
}
|
|
11555
|
+
const key = source === "bitbucket.pat" || source === "ado.pat" ? source : "github.token";
|
|
11556
|
+
return `Regenerate it, then: pncli config keychain set ${key} (or: pncli config set ${key} <new-token>)`;
|
|
11557
|
+
}
|
|
11558
|
+
function subject(r) {
|
|
11559
|
+
return r.marketplace ? `marketplace "${r.marketplace}"` : r.scope;
|
|
11560
|
+
}
|
|
11561
|
+
function enableFix(r) {
|
|
11562
|
+
return r.marketplace ? `pncli skills git-auth enable --marketplace ${r.marketplace}` : `pncli skills git-auth enable --host ${r.host}`;
|
|
11563
|
+
}
|
|
11564
|
+
function accessFix(r) {
|
|
11565
|
+
if (!r.pncliTokenSource) return `If the repo is private, give it a token: pncli skills marketplace update ${r.marketplace} --token <token>`;
|
|
11566
|
+
if (r.provider === "bitbucket" && r.username === "x-token-auth") {
|
|
11567
|
+
return `Bitbucket Data Center personal access tokens are usually sent with your username: pncli skills marketplace update ${r.marketplace} --username <your-bitbucket-username>. If that is not it, the token itself: ${rotateFix(r.pncliTokenSource)}`;
|
|
11568
|
+
}
|
|
11569
|
+
if (r.provider === "ado") return `Check the PAT has Code (Read) for this collection and has not expired. ${rotateFix(r.pncliTokenSource)}`;
|
|
11570
|
+
return `Check the token can read this repo and has not expired. ${rotateFix(r.pncliTokenSource)}`;
|
|
11571
|
+
}
|
|
11572
|
+
function buildGitAuthProblems(reports) {
|
|
11573
|
+
const problems = [];
|
|
11574
|
+
for (const r of reports) {
|
|
11575
|
+
const t = r.token;
|
|
11576
|
+
if (r.credentialError) {
|
|
11577
|
+
problems.push({ area: "git-auth", message: r.credentialError, fix: `Re-store it: pncli config keychain set marketplaces.${r.marketplace}.token` });
|
|
11578
|
+
continue;
|
|
11579
|
+
}
|
|
11580
|
+
if (r.access && !r.access.ok) {
|
|
11581
|
+
problems.push({
|
|
11582
|
+
area: "git-auth",
|
|
11583
|
+
message: `git cannot read ${subject(r)} (${r.repoUrl}) with the credential pncli has (${r.pncliTokenSource ?? "none"}): ${r.access.error}`,
|
|
11584
|
+
fix: accessFix(r)
|
|
11585
|
+
});
|
|
11586
|
+
}
|
|
11587
|
+
if (r.clonesWithEmbeddedToken.length > 0) {
|
|
11588
|
+
problems.push({
|
|
11589
|
+
area: "git-auth",
|
|
11590
|
+
message: `The clone of ${subject(r)} stores a token in plaintext in .git/config (${r.clonesWithEmbeddedToken.join(", ")})`,
|
|
11591
|
+
fix: `Run: ${enableFix(r)} (it rewrites the remote without the token; the next sync does too)`
|
|
11592
|
+
});
|
|
11593
|
+
}
|
|
11594
|
+
if (r.pncliTokenSource && r.mode === "none") {
|
|
11595
|
+
problems.push({
|
|
11596
|
+
area: "git-auth",
|
|
11597
|
+
message: `git itself has no credential for ${subject(r)}: agent hosts that clone marketplaces themselves (e.g. Claude Code's /plugin marketplace add) will fail if it is private`,
|
|
11598
|
+
fix: `Run: ${enableFix(r)}`
|
|
11599
|
+
});
|
|
11600
|
+
}
|
|
11601
|
+
if (r.mode === "keychain" && r.matchesPncliToken === false) {
|
|
11602
|
+
problems.push({
|
|
11603
|
+
area: "git-auth",
|
|
11604
|
+
message: `The token git has in the OS keychain for ${subject(r)} differs from the one pncli uses \u2014 usually a rotation that was not re-stored`,
|
|
11605
|
+
fix: `Re-store it: ${enableFix(r)} --mode keychain (or switch to helper mode, which never goes stale)`
|
|
11606
|
+
});
|
|
11607
|
+
}
|
|
11608
|
+
if (!t) continue;
|
|
11609
|
+
const who = `The GitHub token for ${subject(r)}`;
|
|
11610
|
+
if (t.ssoAuthorizationRequired) {
|
|
11611
|
+
problems.push({
|
|
11612
|
+
area: "git-auth",
|
|
11613
|
+
message: `${who} is not authorized for the organization's SAML SSO`,
|
|
11614
|
+
fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize for the org"
|
|
11615
|
+
});
|
|
11616
|
+
} else if (t.status === 401) {
|
|
11617
|
+
problems.push({
|
|
11618
|
+
area: "git-auth",
|
|
11619
|
+
message: `${who} was rejected (HTTP 401) \u2014 expired or revoked (source: ${r.pncliTokenSource})`,
|
|
11620
|
+
fix: rotateFix(r.pncliTokenSource)
|
|
11621
|
+
});
|
|
11622
|
+
}
|
|
11623
|
+
if (t.missingRepoScope) {
|
|
11624
|
+
problems.push({
|
|
11625
|
+
area: "git-auth",
|
|
11626
|
+
message: `${who} is a classic token without the "repo" scope (has: ${t.scopes?.join(", ") || "none"}), so a private marketplace will not clone`,
|
|
11627
|
+
fix: 'Edit the token in GitHub \u2192 Settings \u2192 Developer settings \u2192 Tokens (classic) and tick "repo"'
|
|
11628
|
+
});
|
|
11629
|
+
}
|
|
11630
|
+
if (t.expiresInDays !== null && t.expiresInDays <= TOKEN_EXPIRY_WARNING_DAYS) {
|
|
11631
|
+
problems.push({
|
|
11632
|
+
area: "git-auth",
|
|
11633
|
+
message: t.expiresInDays < 0 ? `${who} expired on ${t.expiresAt}` : `${who} expires in ${t.expiresInDays} day(s) (${t.expiresAt})`,
|
|
11634
|
+
fix: `${rotateFix(r.pncliTokenSource)} \u2014 helper mode picks up the new token automatically`
|
|
11635
|
+
});
|
|
11636
|
+
}
|
|
11637
|
+
}
|
|
11638
|
+
return problems;
|
|
11639
|
+
}
|
|
11640
|
+
|
|
11641
|
+
// src/services/doctor/commands.ts
|
|
9667
11642
|
function checkConfigFile(filePath) {
|
|
9668
|
-
if (!
|
|
11643
|
+
if (!fs9.existsSync(filePath)) {
|
|
9669
11644
|
return { path: filePath, exists: false, valid: false, message: "not present" };
|
|
9670
11645
|
}
|
|
9671
11646
|
try {
|
|
9672
|
-
JSON.parse(
|
|
11647
|
+
JSON.parse(fs9.readFileSync(filePath, "utf8"));
|
|
9673
11648
|
return { path: filePath, exists: true, valid: true, message: "ok" };
|
|
9674
11649
|
} catch (err) {
|
|
9675
11650
|
return {
|
|
@@ -9750,13 +11725,13 @@ function buildProblems(globalFile, repoFile, credentials, skillLocations, market
|
|
|
9750
11725
|
return problems;
|
|
9751
11726
|
}
|
|
9752
11727
|
function registerDoctorCommands(program2) {
|
|
9753
|
-
program2.command("doctor").description("Diagnose pncli setup: config files, credentials, agent skills, and
|
|
11728
|
+
program2.command("doctor").description("Diagnose pncli setup: config files, credentials, keychain references, git auth for marketplace hosts, agent skills, and marketplaces").option("--offline", "Skip credential checks (no network calls)").action(async (cmdOpts) => {
|
|
9754
11729
|
const start = Date.now();
|
|
9755
11730
|
try {
|
|
9756
11731
|
const opts = program2.optsWithGlobals();
|
|
9757
11732
|
const globalFile = checkConfigFile(opts.config ?? getGlobalConfigPath());
|
|
9758
11733
|
const repoRoot = findGitRoot() ?? process.cwd();
|
|
9759
|
-
const repoFile = checkConfigFile(
|
|
11734
|
+
const repoFile = checkConfigFile(path12.join(repoRoot, ".pncli.json"));
|
|
9760
11735
|
let credentials = null;
|
|
9761
11736
|
let credentialsSkipped = cmdOpts.offline ? "--offline" : null;
|
|
9762
11737
|
if (!cmdOpts.offline) {
|
|
@@ -9781,9 +11756,33 @@ function registerDoctorCommands(program2) {
|
|
|
9781
11756
|
name: m.name ?? m.repoUrl ?? "(unnamed)",
|
|
9782
11757
|
repoUrl: m.repoUrl ?? null,
|
|
9783
11758
|
localPath: m.localPath ?? null,
|
|
9784
|
-
cloneExists: !!m.localPath &&
|
|
11759
|
+
cloneExists: !!m.localPath && fs9.existsSync(m.localPath)
|
|
9785
11760
|
}));
|
|
9786
|
-
const
|
|
11761
|
+
const keychain = (() => {
|
|
11762
|
+
try {
|
|
11763
|
+
return checkKeychain(globalConfig, void 0, opts.config ?? getGlobalConfigPath());
|
|
11764
|
+
} catch (err) {
|
|
11765
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
11766
|
+
}
|
|
11767
|
+
})();
|
|
11768
|
+
let gitAuth = [];
|
|
11769
|
+
try {
|
|
11770
|
+
const providerCfg = (() => {
|
|
11771
|
+
try {
|
|
11772
|
+
return loadConfig({ configPath: opts.config });
|
|
11773
|
+
} catch {
|
|
11774
|
+
return {};
|
|
11775
|
+
}
|
|
11776
|
+
})();
|
|
11777
|
+
gitAuth = await checkGitAuth(globalConfig, providerCfg, !!cmdOpts.offline);
|
|
11778
|
+
} catch (err) {
|
|
11779
|
+
gitAuth = { error: err instanceof Error ? err.message : String(err) };
|
|
11780
|
+
}
|
|
11781
|
+
const problems = [
|
|
11782
|
+
...buildProblems(globalFile, repoFile, credentials, skillLocations, marketplaces),
|
|
11783
|
+
..."error" in keychain ? [] : buildKeychainProblems(keychain),
|
|
11784
|
+
...Array.isArray(gitAuth) ? buildGitAuthProblems(gitAuth) : []
|
|
11785
|
+
];
|
|
9787
11786
|
if (credentials) {
|
|
9788
11787
|
const statuses = Object.values(credentials).map((r) => r.status);
|
|
9789
11788
|
if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
|
|
@@ -9806,7 +11805,10 @@ function registerDoctorCommands(program2) {
|
|
|
9806
11805
|
registered: marketplaces,
|
|
9807
11806
|
hint: marketplaces.length === 0 ? "No org marketplaces registered. Add one with: pncli skills marketplace add <git-url> --all-agents" : "Refresh with: pncli skills marketplace sync --marketplace all --all-agents"
|
|
9808
11807
|
},
|
|
9809
|
-
|
|
11808
|
+
keychain,
|
|
11809
|
+
gitAuth,
|
|
11810
|
+
problems,
|
|
11811
|
+
hint: problems.length === 0 ? null : "New to skills and marketplaces? pncli skills guide troubleshooting"
|
|
9810
11812
|
}, "doctor", "check", start);
|
|
9811
11813
|
} catch (err) {
|
|
9812
11814
|
fail(err, "doctor", "check", start);
|
|
@@ -10195,8 +12197,8 @@ function registerAdoProjectCommands(ado) {
|
|
|
10195
12197
|
}
|
|
10196
12198
|
|
|
10197
12199
|
// src/services/ado/commands/work.ts
|
|
10198
|
-
import
|
|
10199
|
-
import
|
|
12200
|
+
import fs10 from "fs";
|
|
12201
|
+
import path13 from "path";
|
|
10200
12202
|
|
|
10201
12203
|
// src/services/ado/input-schema.ts
|
|
10202
12204
|
var ADO_WORK_INPUT_FILE_SCHEMA = {
|
|
@@ -10437,11 +12439,11 @@ function registerAdoWorkCommands(ado) {
|
|
|
10437
12439
|
const attachments = await workClient.listAttachments(collection, parseInt(opts.id, 10));
|
|
10438
12440
|
const attachment = attachments.find((a) => a.id === opts.attachmentId);
|
|
10439
12441
|
if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
|
|
10440
|
-
const outDir = opts.dir ??
|
|
10441
|
-
|
|
10442
|
-
const outPath =
|
|
12442
|
+
const outDir = opts.dir ?? path13.join(process.cwd(), ".pncli");
|
|
12443
|
+
fs10.mkdirSync(outDir, { recursive: true });
|
|
12444
|
+
const outPath = path13.join(outDir, path13.basename(attachment.name));
|
|
10443
12445
|
const buffer = await workClient.downloadAttachment(attachment.url);
|
|
10444
|
-
|
|
12446
|
+
fs10.writeFileSync(outPath, buffer);
|
|
10445
12447
|
success({ saved: outPath, name: attachment.name, size: buffer.length }, "ado", "work-download-attachment", start);
|
|
10446
12448
|
} catch (err) {
|
|
10447
12449
|
fail(err, "ado", "work-download-attachment", start);
|
|
@@ -10475,10 +12477,10 @@ function registerAdoWorkCommands(ado) {
|
|
|
10475
12477
|
const types = await discoverTypes(workClient, collection, project);
|
|
10476
12478
|
if (opts.save) {
|
|
10477
12479
|
const globalConfigPath = getGlobalConfigPath(globalOpts.config);
|
|
10478
|
-
const
|
|
10479
|
-
const existing = JSON.parse(
|
|
12480
|
+
const fs13 = await import("fs");
|
|
12481
|
+
const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
10480
12482
|
existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
|
|
10481
|
-
|
|
12483
|
+
fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
10482
12484
|
warn(`Saved ${types.length} types to ${globalConfigPath}`);
|
|
10483
12485
|
}
|
|
10484
12486
|
success(types, "ado", "work-types", start);
|
|
@@ -10507,11 +12509,11 @@ function registerAdoWorkCommands(ado) {
|
|
|
10507
12509
|
) : allFields;
|
|
10508
12510
|
if (opts.save) {
|
|
10509
12511
|
const globalConfigPath = getGlobalConfigPath(globalOpts.config);
|
|
10510
|
-
const
|
|
10511
|
-
const existing = JSON.parse(
|
|
12512
|
+
const fs13 = await import("fs");
|
|
12513
|
+
const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
10512
12514
|
const aliases = buildDefaultAliases(allFields);
|
|
10513
12515
|
existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
|
|
10514
|
-
|
|
12516
|
+
fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
10515
12517
|
warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
|
|
10516
12518
|
}
|
|
10517
12519
|
success(fields, "ado", "work-fields", start);
|
|
@@ -10808,7 +12810,7 @@ function registerAdoRepoCommands(ado) {
|
|
|
10808
12810
|
const start = Date.now();
|
|
10809
12811
|
try {
|
|
10810
12812
|
const globalOpts = ado.optsWithGlobals();
|
|
10811
|
-
const { loadConfig: lc } = await import("./config-
|
|
12813
|
+
const { loadConfig: lc } = await import("./config-OH2O5E3P.js");
|
|
10812
12814
|
const config = lc({ configPath: globalOpts.config });
|
|
10813
12815
|
const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
|
|
10814
12816
|
const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
|
|
@@ -11109,6 +13111,10 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
|
|
|
11109
13111
|
}
|
|
11110
13112
|
throw new PncliError(`Timed out waiting for build #${buildNumber} to complete after ${timeoutMs / 1e3}s`, 1);
|
|
11111
13113
|
}
|
|
13114
|
+
function rawJenkinsInstances(configPath) {
|
|
13115
|
+
const stored = loadJsonFile(getGlobalConfigPath(configPath))?.jenkinsInstances;
|
|
13116
|
+
return Array.isArray(stored) ? stored : [];
|
|
13117
|
+
}
|
|
11112
13118
|
function registerJenkinsCommands(program2) {
|
|
11113
13119
|
const jenkins = program2.command("jenkins").description("Jenkins operations").option("--instance <name>", "Named Jenkins instance from global config (overrides default jenkins config)");
|
|
11114
13120
|
const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
|
|
@@ -11132,15 +13138,15 @@ function registerJenkinsCommands(program2) {
|
|
|
11132
13138
|
try {
|
|
11133
13139
|
const opts = jenkins.optsWithGlobals();
|
|
11134
13140
|
const configPath = opts.config;
|
|
11135
|
-
const existing =
|
|
13141
|
+
const existing = rawJenkinsInstances(configPath);
|
|
11136
13142
|
const idx = existing.findIndex((i) => i.name === o.name);
|
|
11137
13143
|
if (idx >= 0 && !o.force) {
|
|
11138
13144
|
throw new PncliError(`Jenkins instance "${o.name}" already exists. Pass --force to overwrite it, or run: pncli jenkins instance remove --name ${o.name}`, 1);
|
|
11139
13145
|
}
|
|
11140
13146
|
let apiToken = o.apiToken;
|
|
11141
13147
|
if (!apiToken && process.stdin.isTTY) {
|
|
11142
|
-
const { default:
|
|
11143
|
-
apiToken = await
|
|
13148
|
+
const { default: password3 } = await import("@inquirer/password");
|
|
13149
|
+
apiToken = await password3({ message: `API token for "${o.name}" (leave blank to skip):` });
|
|
11144
13150
|
}
|
|
11145
13151
|
const entry = {
|
|
11146
13152
|
name: o.name,
|
|
@@ -11160,7 +13166,7 @@ function registerJenkinsCommands(program2) {
|
|
|
11160
13166
|
try {
|
|
11161
13167
|
const opts = jenkins.optsWithGlobals();
|
|
11162
13168
|
const configPath = opts.config;
|
|
11163
|
-
const existing =
|
|
13169
|
+
const existing = rawJenkinsInstances(configPath);
|
|
11164
13170
|
if (!existing.some((i) => i.name === o.name)) {
|
|
11165
13171
|
throw new PncliError(`Jenkins instance "${o.name}" not found. Run: pncli jenkins instance list`, 1);
|
|
11166
13172
|
}
|
|
@@ -11282,18 +13288,18 @@ var ArtifactoryClient = class {
|
|
|
11282
13288
|
}
|
|
11283
13289
|
});
|
|
11284
13290
|
}
|
|
11285
|
-
async getStorageInfo(repo,
|
|
11286
|
-
const cleanPath =
|
|
13291
|
+
async getStorageInfo(repo, path14) {
|
|
13292
|
+
const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
|
|
11287
13293
|
return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`);
|
|
11288
13294
|
}
|
|
11289
|
-
async getProperties(repo,
|
|
11290
|
-
const cleanPath =
|
|
13295
|
+
async getProperties(repo, path14) {
|
|
13296
|
+
const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
|
|
11291
13297
|
return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
|
|
11292
13298
|
params: { properties: "" }
|
|
11293
13299
|
});
|
|
11294
13300
|
}
|
|
11295
|
-
async setProperties(repo,
|
|
11296
|
-
const cleanPath =
|
|
13301
|
+
async setProperties(repo, path14, properties, opts = {}) {
|
|
13302
|
+
const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
|
|
11297
13303
|
const propString = Object.entries(properties).map(([k, v]) => `${k}=${v}`).join(";");
|
|
11298
13304
|
await this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
|
|
11299
13305
|
method: "PUT",
|
|
@@ -11375,27 +13381,27 @@ function registerArtifactoryCommands(program2) {
|
|
|
11375
13381
|
fail(err, "artifactory", "repos", start);
|
|
11376
13382
|
}
|
|
11377
13383
|
});
|
|
11378
|
-
art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo,
|
|
13384
|
+
art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path14) => {
|
|
11379
13385
|
const start = Date.now();
|
|
11380
13386
|
try {
|
|
11381
13387
|
const client = getClient8(program2);
|
|
11382
|
-
const data = await client.getStorageInfo(repo,
|
|
13388
|
+
const data = await client.getStorageInfo(repo, path14);
|
|
11383
13389
|
success(data, "artifactory", "artifact-info", start);
|
|
11384
13390
|
} catch (err) {
|
|
11385
13391
|
fail(err, "artifactory", "artifact-info", start);
|
|
11386
13392
|
}
|
|
11387
13393
|
});
|
|
11388
|
-
art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo,
|
|
13394
|
+
art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path14) => {
|
|
11389
13395
|
const start = Date.now();
|
|
11390
13396
|
try {
|
|
11391
13397
|
const client = getClient8(program2);
|
|
11392
|
-
const data = await client.getProperties(repo,
|
|
13398
|
+
const data = await client.getProperties(repo, path14);
|
|
11393
13399
|
success(data, "artifactory", "properties-get", start);
|
|
11394
13400
|
} catch (err) {
|
|
11395
13401
|
fail(err, "artifactory", "properties-get", start);
|
|
11396
13402
|
}
|
|
11397
13403
|
});
|
|
11398
|
-
art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo,
|
|
13404
|
+
art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo, path14, keyvals, opts) => {
|
|
11399
13405
|
const start = Date.now();
|
|
11400
13406
|
try {
|
|
11401
13407
|
if (!keyvals.length) throw new PncliError("Provide at least one key=value pair");
|
|
@@ -11406,8 +13412,8 @@ function registerArtifactoryCommands(program2) {
|
|
|
11406
13412
|
properties[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
11407
13413
|
}
|
|
11408
13414
|
const client = getClient8(program2);
|
|
11409
|
-
await client.setProperties(repo,
|
|
11410
|
-
success({ repo, path:
|
|
13415
|
+
await client.setProperties(repo, path14, properties, { recursive: opts.recursive });
|
|
13416
|
+
success({ repo, path: path14, properties }, "artifactory", "properties-set", start);
|
|
11411
13417
|
} catch (err) {
|
|
11412
13418
|
fail(err, "artifactory", "properties-set", start);
|
|
11413
13419
|
}
|
|
@@ -11725,8 +13731,8 @@ function registerSonatypeIqCommands(program2) {
|
|
|
11725
13731
|
const startTime = Date.now();
|
|
11726
13732
|
try {
|
|
11727
13733
|
const http = getHttpClient(program2);
|
|
11728
|
-
const
|
|
11729
|
-
const data = await http.sonatypeiq(
|
|
13734
|
+
const path14 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
|
|
13735
|
+
const data = await http.sonatypeiq(path14);
|
|
11730
13736
|
success(data, "sonatypeiq", "policies list", startTime);
|
|
11731
13737
|
} catch (err) {
|
|
11732
13738
|
fail(err, "sonatypeiq", "policies list", startTime);
|
|
@@ -12421,7 +14427,7 @@ function registerLogscaleCommands(program2) {
|
|
|
12421
14427
|
}
|
|
12422
14428
|
|
|
12423
14429
|
// src/services/splitio/commands.ts
|
|
12424
|
-
import
|
|
14430
|
+
import fs11 from "fs";
|
|
12425
14431
|
import confirm2 from "@inquirer/confirm";
|
|
12426
14432
|
function getHttp4(program2) {
|
|
12427
14433
|
const opts = program2.optsWithGlobals();
|
|
@@ -12433,9 +14439,9 @@ function getHttp4(program2) {
|
|
|
12433
14439
|
function loadInputFile(filePath) {
|
|
12434
14440
|
let raw;
|
|
12435
14441
|
if (filePath === "-") {
|
|
12436
|
-
raw =
|
|
14442
|
+
raw = fs11.readFileSync(process.stdin.fd, "utf8");
|
|
12437
14443
|
} else {
|
|
12438
|
-
raw =
|
|
14444
|
+
raw = fs11.readFileSync(filePath, "utf8");
|
|
12439
14445
|
}
|
|
12440
14446
|
try {
|
|
12441
14447
|
return JSON.parse(raw);
|
|
@@ -12967,7 +14973,7 @@ function registerFigmaCommands(program2) {
|
|
|
12967
14973
|
}
|
|
12968
14974
|
|
|
12969
14975
|
// src/services/alation/commands.ts
|
|
12970
|
-
import
|
|
14976
|
+
import fs12 from "fs";
|
|
12971
14977
|
var MAX_PAGE = 1e3;
|
|
12972
14978
|
function parseIntOption(value, flag) {
|
|
12973
14979
|
if (value === void 0) return void 0;
|
|
@@ -13080,8 +15086,8 @@ function getHttp6(program2) {
|
|
|
13080
15086
|
Boolean(opts.dryRun)
|
|
13081
15087
|
);
|
|
13082
15088
|
}
|
|
13083
|
-
async function getOne(http,
|
|
13084
|
-
const rows = await http.alation(
|
|
15089
|
+
async function getOne(http, path14, id, what) {
|
|
15090
|
+
const rows = await http.alation(path14, { params: { id, limit: 1 } });
|
|
13085
15091
|
const row = Array.isArray(rows) ? rows.find((r) => r.id === id) ?? rows[0] : void 0;
|
|
13086
15092
|
if (!row) throw new PncliError(`${what} ${id} not found`, 404);
|
|
13087
15093
|
return row;
|
|
@@ -13131,7 +15137,7 @@ function registerAlationCommands(program2) {
|
|
|
13131
15137
|
const opts = program2.optsWithGlobals();
|
|
13132
15138
|
const config = loadConfig({ configPath: opts.config });
|
|
13133
15139
|
if (opts.dryRun) {
|
|
13134
|
-
|
|
15140
|
+
fs12.writeSync(process.stderr.fd, `DRY RUN: POST ${config.alation.baseUrl ?? "<alation.baseUrl>"}/integration/v1/createAPIAccessToken/ (token exchange skipped)
|
|
13135
15141
|
`);
|
|
13136
15142
|
process.exitCode = ExitCode.SUCCESS;
|
|
13137
15143
|
throw new PncliError("dry-run", 0);
|
|
@@ -13438,8 +15444,8 @@ Services:
|
|
|
13438
15444
|
splitio Split.IO (feature flags, Change Requests)
|
|
13439
15445
|
figma Figma (files, comments, version history)
|
|
13440
15446
|
alation Alation (data sources, schemas, tables, columns, search, Document Hubs)
|
|
13441
|
-
config Manage pncli configuration
|
|
13442
|
-
doctor Diagnose pncli setup (config
|
|
15447
|
+
config Manage pncli configuration (and OS keychain storage for credentials)
|
|
15448
|
+
doctor Diagnose pncli setup (config, credentials, keychain, git auth, agent skills, marketplaces)
|
|
13443
15449
|
skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)
|
|
13444
15450
|
jwt JWT token utilities (decode header and payload)
|
|
13445
15451
|
|
|
@@ -13448,7 +15454,12 @@ Agent skills and org plugins:
|
|
|
13448
15454
|
pncli skills marketplace add <git-url> --all-agents Org plugins (skills + AGENTS.md/CLAUDE.md) from a git marketplace
|
|
13449
15455
|
pncli skills marketplace sync Pull and refresh everything installed from marketplaces
|
|
13450
15456
|
pncli skills marketplace manage Interactive: toggle plugins, add/remove marketplaces
|
|
15457
|
+
pncli skills git-auth enable Let git and agent hosts authenticate to private marketplace hosts
|
|
15458
|
+
pncli skills guide How skills management fits together
|
|
13451
15459
|
pncli skills --help Every install location and command
|
|
15460
|
+
|
|
15461
|
+
Credentials:
|
|
15462
|
+
pncli config keychain migrate Move plaintext tokens into the OS keychain (--to config reverses it)
|
|
13452
15463
|
`);
|
|
13453
15464
|
program.parseAsync(process.argv).catch((err) => {
|
|
13454
15465
|
if (process.exitCode !== void 0) return;
|