@kolatts/pncli 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunk-L6OY2546.js +984 -0
- package/dist/chunk-L6OY2546.js.map +1 -0
- package/dist/cli.js +2439 -427
- 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 +69 -13
- 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]);
|
|
@@ -587,30 +605,30 @@ var BitbucketClient = class {
|
|
|
587
605
|
);
|
|
588
606
|
}
|
|
589
607
|
async createPR(opts) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
id: `refs/heads/${opts.target}`,
|
|
606
|
-
repository: {
|
|
607
|
-
slug: opts.repo,
|
|
608
|
-
project: { key: opts.project }
|
|
609
|
-
}
|
|
610
|
-
},
|
|
611
|
-
reviewers: (opts.reviewers ?? []).map((name) => ({ user: { name } }))
|
|
608
|
+
const body = {
|
|
609
|
+
title: opts.title,
|
|
610
|
+
description: opts.description,
|
|
611
|
+
fromRef: {
|
|
612
|
+
id: `refs/heads/${opts.source}`,
|
|
613
|
+
repository: {
|
|
614
|
+
slug: opts.repo,
|
|
615
|
+
project: { key: opts.project }
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
toRef: {
|
|
619
|
+
id: `refs/heads/${opts.target}`,
|
|
620
|
+
repository: {
|
|
621
|
+
slug: opts.repo,
|
|
622
|
+
project: { key: opts.project }
|
|
612
623
|
}
|
|
613
624
|
}
|
|
625
|
+
};
|
|
626
|
+
if (opts.reviewers && opts.reviewers.length > 0) {
|
|
627
|
+
body.reviewers = opts.reviewers.map((name) => ({ user: { name } }));
|
|
628
|
+
}
|
|
629
|
+
return this.http.bitbucket(
|
|
630
|
+
`${API}/projects/${opts.project}/repos/${opts.repo}/pull-requests`,
|
|
631
|
+
{ method: "POST", body }
|
|
614
632
|
);
|
|
615
633
|
}
|
|
616
634
|
async updatePR(opts) {
|
|
@@ -817,7 +835,936 @@ var BitbucketClient = class {
|
|
|
817
835
|
}
|
|
818
836
|
};
|
|
819
837
|
|
|
838
|
+
// src/services/git/credentials.ts
|
|
839
|
+
import fs from "fs";
|
|
840
|
+
import os from "os";
|
|
841
|
+
import path from "path";
|
|
842
|
+
|
|
843
|
+
// src/services/skills/git-auth.ts
|
|
844
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
845
|
+
var HELPER_COMMAND = "!pncli skills git-credential";
|
|
846
|
+
var HELPER_NAME_PATTERN = /^[^'\r\n]+$/;
|
|
847
|
+
function helperCommandFor(marketplaceName, configPath) {
|
|
848
|
+
let command = HELPER_COMMAND;
|
|
849
|
+
if (configPath !== void 0) {
|
|
850
|
+
if (!HELPER_NAME_PATTERN.test(configPath)) {
|
|
851
|
+
throw new Error(`Config path ${JSON.stringify(configPath)} cannot be used in a git credential helper (no quotes or newlines).`);
|
|
852
|
+
}
|
|
853
|
+
command += ` --config '${configPath}'`;
|
|
854
|
+
}
|
|
855
|
+
if (marketplaceName === void 0) return command;
|
|
856
|
+
if (!HELPER_NAME_PATTERN.test(marketplaceName)) {
|
|
857
|
+
throw new Error(`Marketplace name ${JSON.stringify(marketplaceName)} cannot be used in a git credential helper (no quotes or newlines). Re-add it with a simpler --name.`);
|
|
858
|
+
}
|
|
859
|
+
return `${command} --marketplace '${marketplaceName}'`;
|
|
860
|
+
}
|
|
861
|
+
function isPncliHelperValue(value) {
|
|
862
|
+
return value.trim().startsWith(HELPER_COMMAND);
|
|
863
|
+
}
|
|
864
|
+
function helperConfigKey(scope) {
|
|
865
|
+
return `credential.${scope}.helper`;
|
|
866
|
+
}
|
|
867
|
+
function useHttpPathKey(scope) {
|
|
868
|
+
return `credential.${scope}.useHttpPath`;
|
|
869
|
+
}
|
|
870
|
+
function gitUsernameForHost(host) {
|
|
871
|
+
return host === "github.com" ? "x-access-token" : "x-token-auth";
|
|
872
|
+
}
|
|
873
|
+
function gitHostFromApiBaseUrl(baseUrl) {
|
|
874
|
+
if (!baseUrl) return null;
|
|
875
|
+
try {
|
|
876
|
+
const host = new URL(baseUrl).host.toLowerCase();
|
|
877
|
+
return host === "api.github.com" ? "github.com" : host;
|
|
878
|
+
} catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function apiBaseForHost(host, githubBaseUrl) {
|
|
883
|
+
if (host === "github.com") return "https://api.github.com";
|
|
884
|
+
if (githubBaseUrl && gitHostFromApiBaseUrl(githubBaseUrl) === host) return githubBaseUrl.replace(/\/+$/, "");
|
|
885
|
+
return `https://${host}/api/v3`;
|
|
886
|
+
}
|
|
887
|
+
function hostOf(url) {
|
|
888
|
+
if (!url) return null;
|
|
889
|
+
try {
|
|
890
|
+
const u = new URL(url);
|
|
891
|
+
return u.protocol === "https:" ? u.host.toLowerCase() : null;
|
|
892
|
+
} catch {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function httpHostOf(url) {
|
|
897
|
+
if (!url) return null;
|
|
898
|
+
try {
|
|
899
|
+
const u = new URL(url);
|
|
900
|
+
return u.protocol === "https:" || u.protocol === "http:" ? u.host.toLowerCase() : null;
|
|
901
|
+
} catch {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function hostnameOf(url) {
|
|
906
|
+
if (!url) return null;
|
|
907
|
+
try {
|
|
908
|
+
return new URL(url).hostname.toLowerCase();
|
|
909
|
+
} catch {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function normalizeRepoPath(p) {
|
|
914
|
+
return p.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
|
|
915
|
+
}
|
|
916
|
+
function classifyGitHubToken(token) {
|
|
917
|
+
if (token.startsWith("ghp_")) return "classic";
|
|
918
|
+
if (token.startsWith("github_pat_")) return "fine-grained";
|
|
919
|
+
if (token.startsWith("gho_")) return "oauth";
|
|
920
|
+
if (token.startsWith("ghu_") || token.startsWith("ghs_")) return "github-app";
|
|
921
|
+
if (/^[0-9a-f]{40}$/i.test(token)) return "legacy-hex";
|
|
922
|
+
return "unknown";
|
|
923
|
+
}
|
|
924
|
+
function allMarketplaces(globalConfig) {
|
|
925
|
+
return [...globalConfig.marketplaces ?? [], ...globalConfig.marketplace ? [globalConfig.marketplace] : []];
|
|
926
|
+
}
|
|
927
|
+
var PROVIDERS = ["github", "bitbucket", "ado", "git"];
|
|
928
|
+
function detectProvider(repoUrl, cfg, explicit) {
|
|
929
|
+
if (explicit && PROVIDERS.includes(explicit)) return explicit;
|
|
930
|
+
const hostname = hostnameOf(repoUrl);
|
|
931
|
+
if (!hostname) return "git";
|
|
932
|
+
const path14 = (() => {
|
|
933
|
+
try {
|
|
934
|
+
return new URL(repoUrl).pathname;
|
|
935
|
+
} catch {
|
|
936
|
+
return "";
|
|
937
|
+
}
|
|
938
|
+
})();
|
|
939
|
+
const githubHost = hostnameOf(cfg.github?.baseUrl);
|
|
940
|
+
if (hostname === "github.com" || githubHost && (hostname === githubHost || githubHost === `api.${hostname}`)) return "github";
|
|
941
|
+
if (hostname === hostnameOf(cfg.ado?.baseUrl) || /\/_git\//.test(path14) || hostname === "dev.azure.com" || hostname.endsWith(".visualstudio.com")) return "ado";
|
|
942
|
+
if (hostname === hostnameOf(cfg.bitbucket?.baseUrl) || /(^|\/)scm\//.test(path14) || hostname === "bitbucket.org") return "bitbucket";
|
|
943
|
+
return "git";
|
|
944
|
+
}
|
|
945
|
+
function providerFallbackToken(provider, repoUrl, cfg) {
|
|
946
|
+
const hostname = hostnameOf(repoUrl);
|
|
947
|
+
if (!hostname) return null;
|
|
948
|
+
if (provider === "github") {
|
|
949
|
+
const githubHost = hostnameOf(cfg.github?.baseUrl);
|
|
950
|
+
const isGitHubHost2 = hostname === "github.com" || !!githubHost && githubHost === hostname;
|
|
951
|
+
return isGitHubHost2 && cfg.github?.token ? { token: cfg.github.token, source: "github.token" } : null;
|
|
952
|
+
}
|
|
953
|
+
if (provider === "bitbucket") {
|
|
954
|
+
return hostname === hostnameOf(cfg.bitbucket?.baseUrl) && cfg.bitbucket?.pat ? { token: cfg.bitbucket.pat, source: "bitbucket.pat" } : null;
|
|
955
|
+
}
|
|
956
|
+
if (provider === "ado") {
|
|
957
|
+
return hostname === hostnameOf(cfg.ado?.baseUrl) && cfg.ado?.pat ? { token: cfg.ado.pat, source: "ado.pat" } : null;
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
var UnresolvedMarketplaceTokenError = class extends Error {
|
|
962
|
+
};
|
|
963
|
+
function marketplaceLabelOf(m) {
|
|
964
|
+
return m.name ?? m.repoUrl ?? "(unnamed)";
|
|
965
|
+
}
|
|
966
|
+
function resolveMarketplaceAuth(m, cfg, resolveSecret = resolveSecretValue) {
|
|
967
|
+
const host = httpHostOf(m.repoUrl);
|
|
968
|
+
if (!host) return null;
|
|
969
|
+
const provider = detectProvider(m.repoUrl, cfg, m.provider);
|
|
970
|
+
const username = m.username || gitUsernameForHost(host);
|
|
971
|
+
if (m.token) {
|
|
972
|
+
const secret = resolveSecret(m.token);
|
|
973
|
+
if (!secret && isKeychainRef(m.token)) {
|
|
974
|
+
throw new UnresolvedMarketplaceTokenError(`The token for marketplace "${marketplaceLabelOf(m)}" is stored in the OS keychain as "${m.token}", but it could not be read. Check with: pncli config keychain status`);
|
|
975
|
+
}
|
|
976
|
+
if (secret) return { username, password: secret, source: `marketplace:${marketplaceLabelOf(m)}`, provider };
|
|
977
|
+
}
|
|
978
|
+
const fallback = providerFallbackToken(provider, m.repoUrl, cfg);
|
|
979
|
+
return fallback ? { username, password: fallback.token, source: fallback.source, provider } : null;
|
|
980
|
+
}
|
|
981
|
+
function parseCredentialRequest(input3) {
|
|
982
|
+
const out = {};
|
|
983
|
+
for (const line of input3.split(/\r?\n/)) {
|
|
984
|
+
if (line === "") break;
|
|
985
|
+
const eq = line.indexOf("=");
|
|
986
|
+
if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
function resolveCredential(request, globalConfig, cfg, resolveSecret = resolveSecretValue, marketplaceName) {
|
|
991
|
+
if (request.protocol !== "https" || !request.host) return null;
|
|
992
|
+
const host = request.host.toLowerCase().replace(/:443$/, "");
|
|
993
|
+
const marketplaces = allMarketplaces(globalConfig).filter((m) => hostOf(m.repoUrl) === host);
|
|
994
|
+
if (marketplaceName !== void 0) {
|
|
995
|
+
const m = marketplaces.find((x) => x.name === marketplaceName || x.repoUrl === marketplaceName);
|
|
996
|
+
if (!m) return null;
|
|
997
|
+
const auth = resolveMarketplaceAuth(m, cfg, resolveSecret);
|
|
998
|
+
return auth ? { username: auth.username, password: auth.password, source: auth.source } : null;
|
|
999
|
+
}
|
|
1000
|
+
if (request.path) {
|
|
1001
|
+
const wanted = normalizeRepoPath(request.path);
|
|
1002
|
+
const exact = marketplaces.find((m) => normalizeRepoPath(new URL(m.repoUrl).pathname) === wanted);
|
|
1003
|
+
if (exact) {
|
|
1004
|
+
const auth = resolveMarketplaceAuth(exact, cfg, resolveSecret);
|
|
1005
|
+
if (auth) return { username: auth.username, password: auth.password, source: auth.source };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const url = `https://${host}/`;
|
|
1009
|
+
for (const provider of ["github", "bitbucket", "ado"]) {
|
|
1010
|
+
const fallback = providerFallbackToken(provider, url, cfg);
|
|
1011
|
+
if (fallback) return { username: gitUsernameForHost(host), password: fallback.token, source: fallback.source };
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
function formatCredentialAnswer(answer) {
|
|
1016
|
+
return `username=${answer.username}
|
|
1017
|
+
password=${answer.password}
|
|
1018
|
+
`;
|
|
1019
|
+
}
|
|
1020
|
+
function parseScope(scope) {
|
|
1021
|
+
const u = new URL(scope);
|
|
1022
|
+
const path14 = u.pathname.replace(/^\/+/, "");
|
|
1023
|
+
return { scope, host: u.host.toLowerCase(), path: path14 || null };
|
|
1024
|
+
}
|
|
1025
|
+
function hostScope(host) {
|
|
1026
|
+
return `https://${host}`;
|
|
1027
|
+
}
|
|
1028
|
+
function marketplaceScopes(repoUrl, provider) {
|
|
1029
|
+
const u = new URL(repoUrl);
|
|
1030
|
+
u.username = "";
|
|
1031
|
+
u.password = "";
|
|
1032
|
+
u.search = "";
|
|
1033
|
+
u.hash = "";
|
|
1034
|
+
const base = `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
|
|
1035
|
+
if (provider === "ado") return [base];
|
|
1036
|
+
const toggled = /\.git$/i.test(base) ? base.replace(/\.git$/i, "") : `${base}.git`;
|
|
1037
|
+
return [base, toggled];
|
|
1038
|
+
}
|
|
1039
|
+
var defaultGitRunner = (args, opts) => {
|
|
1040
|
+
const r = spawnSync("git", args, {
|
|
1041
|
+
input: opts?.input,
|
|
1042
|
+
encoding: "utf8",
|
|
1043
|
+
windowsHide: true,
|
|
1044
|
+
timeout: opts?.timeoutMs ?? 3e4,
|
|
1045
|
+
env: { ...process.env, ...opts?.env }
|
|
1046
|
+
});
|
|
1047
|
+
if (r.error) throw r.error;
|
|
1048
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1049
|
+
};
|
|
1050
|
+
var NON_INTERACTIVE_ENV = {
|
|
1051
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1052
|
+
GCM_INTERACTIVE: "never",
|
|
1053
|
+
GIT_ASKPASS: "",
|
|
1054
|
+
SSH_ASKPASS: ""
|
|
1055
|
+
};
|
|
1056
|
+
function readHelperEntries(scope, git = defaultGitRunner) {
|
|
1057
|
+
const r = git(["config", "--global", "--get-all", helperConfigKey(scope)]);
|
|
1058
|
+
return r.status === 0 ? r.stdout.split(/\r?\n/).filter((l, i, a) => i < a.length - 1 || l !== "") : [];
|
|
1059
|
+
}
|
|
1060
|
+
function readGlobalHelpers(git = defaultGitRunner) {
|
|
1061
|
+
const r = git(["config", "--get-all", "credential.helper"]);
|
|
1062
|
+
return r.status === 0 ? r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : [];
|
|
1063
|
+
}
|
|
1064
|
+
function readUseHttpPath(scope, git) {
|
|
1065
|
+
const r = git(["config", "--global", "--get", useHttpPathKey(scope)]);
|
|
1066
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
1067
|
+
}
|
|
1068
|
+
function afterLastReset(entries) {
|
|
1069
|
+
const own = entries.filter((e) => !isPncliHelperValue(e));
|
|
1070
|
+
return own.slice(own.lastIndexOf("") + 1).filter((e) => e.trim() !== "");
|
|
1071
|
+
}
|
|
1072
|
+
function effectiveFallbackHelpers(scopeEntries, genericHelpers, hostEntries = []) {
|
|
1073
|
+
for (const entries of [scopeEntries, hostEntries]) {
|
|
1074
|
+
if (entries.some((e) => !isPncliHelperValue(e))) return afterLastReset(entries);
|
|
1075
|
+
}
|
|
1076
|
+
return genericHelpers.filter((h) => h.trim() !== "" && !isPncliHelperValue(h));
|
|
1077
|
+
}
|
|
1078
|
+
function enableHelper(scope, helperValue, git = defaultGitRunner) {
|
|
1079
|
+
const { host, path: path14 } = parseScope(scope);
|
|
1080
|
+
const previousHelpers = readHelperEntries(scope, git).filter((e) => !isPncliHelperValue(e));
|
|
1081
|
+
const previousUseHttpPath = readUseHttpPath(scope, git);
|
|
1082
|
+
const hostEntries = path14 ? readHelperEntries(hostScope(host), git) : [];
|
|
1083
|
+
const fallbacks = effectiveFallbackHelpers(previousHelpers, readGlobalHelpers(git), hostEntries);
|
|
1084
|
+
git(["config", "--global", "--unset-all", helperConfigKey(scope)]);
|
|
1085
|
+
for (const value of ["", helperValue, ...fallbacks]) {
|
|
1086
|
+
const r = git(["config", "--global", "--add", helperConfigKey(scope), value]);
|
|
1087
|
+
if (r.status !== 0) throw new Error(`git config failed: ${r.stderr.trim()}`);
|
|
1088
|
+
}
|
|
1089
|
+
return { previousHelpers, previousUseHttpPath };
|
|
1090
|
+
}
|
|
1091
|
+
function restoreUseHttpPath(scope, previous, git) {
|
|
1092
|
+
if (previous === null) git(["config", "--global", "--unset", useHttpPathKey(scope)]);
|
|
1093
|
+
else git(["config", "--global", useHttpPathKey(scope), previous]);
|
|
1094
|
+
}
|
|
1095
|
+
function disableHelper(scope, backup, git = defaultGitRunner) {
|
|
1096
|
+
const current = readHelperEntries(scope, git);
|
|
1097
|
+
if (!current.some(isPncliHelperValue)) return false;
|
|
1098
|
+
const restore = backup ? backup.previousHelpers : current.slice(current.findIndex(isPncliHelperValue) + 1);
|
|
1099
|
+
git(["config", "--global", "--unset-all", helperConfigKey(scope)]);
|
|
1100
|
+
for (const value of restore) git(["config", "--global", "--add", helperConfigKey(scope), value]);
|
|
1101
|
+
if (backup) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
function inlineCredentialArgs(username, token) {
|
|
1105
|
+
const helper = `credential.helper=!f() { test "$1" = get || exit 0; printf '%s\\n' "username=$PNCLI_GIT_USERNAME" "password=$PNCLI_GIT_PASSWORD"; }; f`;
|
|
1106
|
+
return {
|
|
1107
|
+
args: ["-c", "credential.helper=", "-c", helper],
|
|
1108
|
+
env: { PNCLI_GIT_USERNAME: username, PNCLI_GIT_PASSWORD: token }
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function credentialInput(host, fields = {}) {
|
|
1112
|
+
return Object.entries({ protocol: "https", host, ...fields }).map(([k, v]) => `${k}=${v}`).join("\n") + "\n\n";
|
|
1113
|
+
}
|
|
1114
|
+
function keychainStore(scope, username, token, git = defaultGitRunner) {
|
|
1115
|
+
const helpers = readGlobalHelpers(git);
|
|
1116
|
+
if (helpers.length === 0) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
"git has no credential.helper configured, so there is no OS keychain to store into. Configure one first \u2014 Windows: `git config --global credential.helper manager` (ships with Git for Windows); macOS: `git config --global credential.helper osxkeychain`; Linux: git-credential-libsecret. Or use --mode helper, which needs no keychain."
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const { host, path: path14 } = parseScope(scope);
|
|
1122
|
+
const backup = { previousHelpers: [], previousUseHttpPath: readUseHttpPath(scope, git) };
|
|
1123
|
+
if (path14) git(["config", "--global", useHttpPathKey(scope), "true"]);
|
|
1124
|
+
const r = git(["credential", "approve"], { input: credentialInput(host, { ...path14 ? { path: path14 } : {}, username, password: token }), env: NON_INTERACTIVE_ENV });
|
|
1125
|
+
if (r.status !== 0) {
|
|
1126
|
+
if (path14) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
|
|
1127
|
+
throw new Error(`git credential approve failed: ${r.stderr.trim()}`);
|
|
1128
|
+
}
|
|
1129
|
+
return { helpers, plaintextStore: helpers.some((h) => /^store\b/.test(h)), backup };
|
|
1130
|
+
}
|
|
1131
|
+
function keychainErase(scope, username, git = defaultGitRunner) {
|
|
1132
|
+
const { host, path: path14 } = parseScope(scope);
|
|
1133
|
+
git(["credential", "reject"], { input: credentialInput(host, { ...path14 ? { path: path14 } : {}, username }), env: NON_INTERACTIVE_ENV });
|
|
1134
|
+
}
|
|
1135
|
+
function disableScope(scope, record, forgetKeychain, git = defaultGitRunner) {
|
|
1136
|
+
const backup = record ? { previousHelpers: record.previousHelpers ?? [], previousUseHttpPath: record.previousUseHttpPath ?? null } : void 0;
|
|
1137
|
+
const helperRemoved = disableHelper(scope, record?.mode === "helper" ? backup : void 0, git);
|
|
1138
|
+
let keychainErased = false;
|
|
1139
|
+
if (record?.mode === "keychain") {
|
|
1140
|
+
if (forgetKeychain) {
|
|
1141
|
+
keychainErase(scope, record.username ?? gitUsernameForHost(parseScope(scope).host), git);
|
|
1142
|
+
keychainErased = true;
|
|
1143
|
+
}
|
|
1144
|
+
keychainRestoreScope(scope, backup, git);
|
|
1145
|
+
}
|
|
1146
|
+
return { helperRemoved, keychainErased };
|
|
1147
|
+
}
|
|
1148
|
+
function keychainRestoreScope(scope, backup, git = defaultGitRunner) {
|
|
1149
|
+
if (parseScope(scope).path) restoreUseHttpPath(scope, backup.previousUseHttpPath, git);
|
|
1150
|
+
}
|
|
1151
|
+
function credentialFill(host, path14, git = defaultGitRunner) {
|
|
1152
|
+
try {
|
|
1153
|
+
const r = git(["credential", "fill"], { input: credentialInput(host, path14 ? { path: path14 } : {}), env: NON_INTERACTIVE_ENV });
|
|
1154
|
+
if (r.status !== 0) return null;
|
|
1155
|
+
const parsed = parseCredentialRequest(r.stdout);
|
|
1156
|
+
return parsed.password ? { username: parsed.username ?? "", password: parsed.password } : null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function lsRemote(repoUrl, auth, git = defaultGitRunner, opts = {}) {
|
|
1162
|
+
const inline = auth ? inlineCredentialArgs(auth.username, auth.password) : { args: opts.keepHelpers ? [] : ["-c", "credential.helper="], env: {} };
|
|
1163
|
+
try {
|
|
1164
|
+
const r = git([...inline.args, "ls-remote", "--heads", repoUrl], { env: { ...NON_INTERACTIVE_ENV, ...inline.env }, timeoutMs: 2e4 });
|
|
1165
|
+
if (r.status === 0) return { ok: true, error: null };
|
|
1166
|
+
const scrub2 = (s) => auth ? s.split(auth.password).join("***") : s;
|
|
1167
|
+
return { ok: false, error: scrub2(r.stderr.trim().split(/\r?\n/).slice(-2).join(" ")) || `git exited with ${r.status}` };
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function originHasCredentials(repoPath) {
|
|
1173
|
+
try {
|
|
1174
|
+
const url = execFileSync2("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: "pipe" }).trim();
|
|
1175
|
+
const u = new URL(url);
|
|
1176
|
+
return !!u.password;
|
|
1177
|
+
} catch {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
async function inspectGitHubToken(apiBase, token, fetchFn = fetch, now = /* @__PURE__ */ new Date()) {
|
|
1182
|
+
const kind = classifyGitHubToken(token);
|
|
1183
|
+
const base = {
|
|
1184
|
+
kind,
|
|
1185
|
+
status: null,
|
|
1186
|
+
valid: false,
|
|
1187
|
+
login: null,
|
|
1188
|
+
scopes: null,
|
|
1189
|
+
missingRepoScope: false,
|
|
1190
|
+
expiresAt: null,
|
|
1191
|
+
expiresInDays: null,
|
|
1192
|
+
ssoAuthorizationRequired: false,
|
|
1193
|
+
error: null
|
|
1194
|
+
};
|
|
1195
|
+
let res;
|
|
1196
|
+
try {
|
|
1197
|
+
res = await fetchFn(`${apiBase.replace(/\/+$/, "")}/user`, {
|
|
1198
|
+
headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "pncli" },
|
|
1199
|
+
signal: AbortSignal.timeout(1e4)
|
|
1200
|
+
});
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
return { ...base, error: err instanceof Error ? err.message : String(err) };
|
|
1203
|
+
}
|
|
1204
|
+
const scopesHeader = res.headers.get("x-oauth-scopes");
|
|
1205
|
+
const scopes = scopesHeader === null ? null : scopesHeader.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1206
|
+
const expiryHeader = res.headers.get("github-authentication-token-expiration");
|
|
1207
|
+
let expiresAt = null;
|
|
1208
|
+
let expiresInDays = null;
|
|
1209
|
+
if (expiryHeader) {
|
|
1210
|
+
const t = Date.parse(expiryHeader.replace(/ UTC$/, "Z").replace(" ", "T").replace(" ", ""));
|
|
1211
|
+
if (!Number.isNaN(t)) {
|
|
1212
|
+
expiresAt = new Date(t).toISOString();
|
|
1213
|
+
expiresInDays = Math.floor((t - now.getTime()) / 864e5);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
let login = null;
|
|
1217
|
+
if (res.ok) {
|
|
1218
|
+
try {
|
|
1219
|
+
login = (await res.json()).login ?? null;
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const classicLike = kind === "classic" || kind === "legacy-hex";
|
|
1224
|
+
return {
|
|
1225
|
+
...base,
|
|
1226
|
+
status: res.status,
|
|
1227
|
+
valid: res.ok,
|
|
1228
|
+
login,
|
|
1229
|
+
scopes,
|
|
1230
|
+
missingRepoScope: res.ok && classicLike && scopes !== null && !scopes.includes("repo"),
|
|
1231
|
+
expiresAt,
|
|
1232
|
+
expiresInDays,
|
|
1233
|
+
ssoAuthorizationRequired: res.status === 403 && res.headers.has("x-github-sso"),
|
|
1234
|
+
error: res.ok ? null : `GET /user returned HTTP ${res.status}`
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/services/git/credentials.ts
|
|
1239
|
+
function fingerprint(token) {
|
|
1240
|
+
const prefix = /^(github_pat_|gh[pousr]_)/.exec(token)?.[1] ?? "";
|
|
1241
|
+
return token.length <= 8 ? "\u2026" : `${prefix}\u2026${token.slice(-4)}`;
|
|
1242
|
+
}
|
|
1243
|
+
function looksLikeToken(value) {
|
|
1244
|
+
return /^(github_pat_|gh[pousr]_)/.test(value) || /^[0-9a-f]{40}$/i.test(value) || value.length >= 30 && !/[@\s]/.test(value);
|
|
1245
|
+
}
|
|
1246
|
+
function tokenFromUrl(url) {
|
|
1247
|
+
try {
|
|
1248
|
+
const u = new URL(url);
|
|
1249
|
+
const user = decodeURIComponent(u.username);
|
|
1250
|
+
const pass = decodeURIComponent(u.password);
|
|
1251
|
+
if (pass) return { username: user, token: pass };
|
|
1252
|
+
if (user && looksLikeToken(user)) return { username: "", token: user };
|
|
1253
|
+
return null;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function redactUrl(url) {
|
|
1259
|
+
try {
|
|
1260
|
+
const u = new URL(url);
|
|
1261
|
+
if (!u.username && !u.password) return url;
|
|
1262
|
+
const found = tokenFromUrl(url);
|
|
1263
|
+
const shown = found ? found.username ? `${found.username}:${fingerprint(found.token)}` : fingerprint(found.token) : "***";
|
|
1264
|
+
return `${u.protocol}//${shown}@${u.host}${u.pathname}${u.search}`;
|
|
1265
|
+
} catch {
|
|
1266
|
+
return url.replace(/\/\/[^/@]+@/, "//***@");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function redactSecrets(text) {
|
|
1270
|
+
return text.replace(/(github_pat_|gh[pousr]_)[A-Za-z0-9_]{8,}/g, (m) => fingerprint(m)).replace(/\b[0-9a-f]{40}\b/gi, (m) => fingerprint(m)).replace(/(password=)([^\s;'"]+)/gi, (m, k, v) => v.includes("\u2026") ? m : `${k}${fingerprint(v)}`).replace(/(\/\/)([^/@\s:]+):([^/@\s]+)@/g, (_m, s, u, p) => `${s}${u}:${fingerprint(p)}@`);
|
|
1271
|
+
}
|
|
1272
|
+
function scrub(text, secret) {
|
|
1273
|
+
return secret ? text.split(secret).join(fingerprint(secret)) : text;
|
|
1274
|
+
}
|
|
1275
|
+
function parseInsteadOf(output) {
|
|
1276
|
+
const mappings = [];
|
|
1277
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1278
|
+
const m = /^url\.(.+)\.insteadof (.+)$/i.exec(line.trim());
|
|
1279
|
+
if (m) mappings.push({ from: m[2], to: redactUrl(m[1]), rawTo: m[1] });
|
|
1280
|
+
}
|
|
1281
|
+
return mappings;
|
|
1282
|
+
}
|
|
1283
|
+
function matchMapping(url, mappings) {
|
|
1284
|
+
let best = null;
|
|
1285
|
+
for (const m of mappings) {
|
|
1286
|
+
if (url.startsWith(m.from) && (!best || m.from.length > best.from.length)) best = m;
|
|
1287
|
+
}
|
|
1288
|
+
return best;
|
|
1289
|
+
}
|
|
1290
|
+
function parseScopedHelpers(output) {
|
|
1291
|
+
const out = [];
|
|
1292
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1293
|
+
const m = /^credential\.(.+)\.helper(?: (.*))?$/i.exec(line.trim());
|
|
1294
|
+
if (m) out.push({ scope: m[1], helper: m[2] ?? "" });
|
|
1295
|
+
}
|
|
1296
|
+
return out;
|
|
1297
|
+
}
|
|
1298
|
+
function githubRepoSlug(url) {
|
|
1299
|
+
try {
|
|
1300
|
+
const parts = new URL(url).pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
|
|
1301
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async function checkGitHubRepoAccess(apiBase, slug, token, fetchFn = fetch) {
|
|
1307
|
+
try {
|
|
1308
|
+
const res = await fetchFn(`${apiBase.replace(/\/+$/, "")}/repos/${slug}`, {
|
|
1309
|
+
headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "pncli" },
|
|
1310
|
+
signal: AbortSignal.timeout(1e4)
|
|
1311
|
+
});
|
|
1312
|
+
return {
|
|
1313
|
+
status: res.status,
|
|
1314
|
+
canRead: res.ok,
|
|
1315
|
+
ssoAuthorizationRequired: res.status === 403 && res.headers.has("x-github-sso"),
|
|
1316
|
+
error: res.ok ? null : `GET /repos/${slug} returned HTTP ${res.status}`
|
|
1317
|
+
};
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
return { status: null, canRead: false, ssoAuthorizationRequired: false, error: err instanceof Error ? err.message : String(err) };
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function gitIn(cwd, git) {
|
|
1323
|
+
return cwd ? (args, opts) => git(["-C", cwd, ...args], opts) : git;
|
|
1324
|
+
}
|
|
1325
|
+
async function inspectRemoteAuth(remote, url, cwd, cfg, deps = {}) {
|
|
1326
|
+
const baseGit = deps.git ?? defaultGitRunner;
|
|
1327
|
+
const git = gitIn(cwd, baseGit);
|
|
1328
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1329
|
+
const effRaw = (() => {
|
|
1330
|
+
const r = git(["ls-remote", "--get-url", url]);
|
|
1331
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : url;
|
|
1332
|
+
})();
|
|
1333
|
+
const mappingRaw = matchMapping(url, parseInsteadOf(git(["config", "--get-regexp", "^url\\..*\\.insteadof$"]).stdout));
|
|
1334
|
+
let host = null;
|
|
1335
|
+
let repoPath = null;
|
|
1336
|
+
try {
|
|
1337
|
+
const u = new URL(effRaw);
|
|
1338
|
+
if (u.protocol === "https:" || u.protocol === "http:") {
|
|
1339
|
+
host = u.host.toLowerCase();
|
|
1340
|
+
repoPath = u.pathname.replace(/^\/+/, "");
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
const plainEffective = (() => {
|
|
1345
|
+
try {
|
|
1346
|
+
const u = new URL(effRaw);
|
|
1347
|
+
u.username = "";
|
|
1348
|
+
u.password = "";
|
|
1349
|
+
return u.toString();
|
|
1350
|
+
} catch {
|
|
1351
|
+
return effRaw;
|
|
1352
|
+
}
|
|
1353
|
+
})();
|
|
1354
|
+
const provider = detectProvider(plainEffective, cfg);
|
|
1355
|
+
let source = "none";
|
|
1356
|
+
let credential = null;
|
|
1357
|
+
let helper = null;
|
|
1358
|
+
const embedded = tokenFromUrl(effRaw);
|
|
1359
|
+
if (embedded) {
|
|
1360
|
+
credential = embedded;
|
|
1361
|
+
source = mappingRaw && tokenFromUrl(mappingRaw.rawTo) ? "insteadOf" : "url";
|
|
1362
|
+
} else if (host) {
|
|
1363
|
+
const filled = credentialFill(host, repoPath ?? void 0, git);
|
|
1364
|
+
if (filled) {
|
|
1365
|
+
credential = { username: filled.username, token: filled.password };
|
|
1366
|
+
source = "helper";
|
|
1367
|
+
const h = git(["config", "--get-urlmatch", "credential.helper", plainEffective]);
|
|
1368
|
+
helper = h.status === 0 ? redactSecrets(h.stdout.trim().split(/\r?\n/).pop() ?? "") || null : null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const report = {
|
|
1372
|
+
remote,
|
|
1373
|
+
url: redactUrl(url),
|
|
1374
|
+
effectiveUrl: redactUrl(effRaw),
|
|
1375
|
+
host,
|
|
1376
|
+
provider,
|
|
1377
|
+
mapping: mappingRaw ? { from: mappingRaw.from, to: mappingRaw.to } : null,
|
|
1378
|
+
credentialSource: source,
|
|
1379
|
+
helper,
|
|
1380
|
+
helperKind: helper ? classifyHelper(helper) : null,
|
|
1381
|
+
credentialStore: helper && classifyHelper(helper) === "git-credential-manager" ? gcmCredentialStore(git) : null,
|
|
1382
|
+
token: credential ? { kind: provider === "github" ? classifyGitHubToken(credential.token) : "unknown", fingerprint: fingerprint(credential.token), username: credential.username || null } : null,
|
|
1383
|
+
access: null,
|
|
1384
|
+
github: null,
|
|
1385
|
+
problems: []
|
|
1386
|
+
};
|
|
1387
|
+
if (host && !deps.offline) {
|
|
1388
|
+
const auth = source === "helper" && credential ? { username: credential.username, password: credential.token } : null;
|
|
1389
|
+
const r = lsRemote(remote && cwd ? remote : source === "helper" ? plainEffective : url, auth, git);
|
|
1390
|
+
report.access = { ok: r.ok, error: r.error ? scrub(r.error, credential?.token ?? null) : null };
|
|
1391
|
+
if (provider === "github" && credential) {
|
|
1392
|
+
const apiBase = apiBaseForHost(host, cfg.github?.baseUrl);
|
|
1393
|
+
const key = `${apiBase}\0${credential.token}`;
|
|
1394
|
+
if (!deps.cache?.has(key)) deps.cache?.set(key, await inspectGitHubToken(apiBase, credential.token, fetchFn));
|
|
1395
|
+
const tokenInfo = deps.cache?.get(key) ?? await inspectGitHubToken(apiBase, credential.token, fetchFn);
|
|
1396
|
+
const slug = githubRepoSlug(plainEffective);
|
|
1397
|
+
const repo = slug && tokenInfo.valid ? await checkGitHubRepoAccess(apiBase, slug, credential.token, fetchFn) : null;
|
|
1398
|
+
report.github = { token: tokenInfo, repo };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
report.problems = buildRemoteProblems(report, url);
|
|
1402
|
+
return report;
|
|
1403
|
+
}
|
|
1404
|
+
function buildRemoteProblems(r, originalUrl) {
|
|
1405
|
+
const problems = [];
|
|
1406
|
+
const where = r.credentialSource === "insteadOf" && r.mapping ? `the insteadOf mapping for ${r.mapping.from}` : r.credentialSource === "url" ? "the remote URL" : r.credentialSource === "helper" ? `the credential helper (${r.helper ?? "unknown"})` : "nothing";
|
|
1407
|
+
const org = (() => {
|
|
1408
|
+
try {
|
|
1409
|
+
const u = new URL(originalUrl);
|
|
1410
|
+
return `${u.protocol}//${u.host}/${u.pathname.split("/").filter(Boolean)[0] ?? ""}/`;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return originalUrl;
|
|
1413
|
+
}
|
|
1414
|
+
})();
|
|
1415
|
+
if (!r.host) return problems;
|
|
1416
|
+
if (r.credentialSource === "url") {
|
|
1417
|
+
problems.push({
|
|
1418
|
+
message: "The token is embedded in the remote URL, so it sits in plaintext in this clone's .git/config",
|
|
1419
|
+
fix: `Move it to a mapping or helper and reset the remote: git remote set-url ${r.remote ?? "origin"} ${r.url.replace(/\/\/[^@]+@/, "//")}`
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (r.access && !r.access.ok) {
|
|
1423
|
+
if (r.credentialSource === "none") {
|
|
1424
|
+
problems.push({
|
|
1425
|
+
message: `No credential is mapped for ${r.effectiveUrl} \u2014 git reached it anonymously and was refused (${r.access.error})`,
|
|
1426
|
+
fix: r.provider === "github" ? `Map a token for the org: git config --global url."https://<classic-PAT>@${r.host}/${org.split("/").slice(3).join("/")}".insteadOf "${org}" \u2014 or let pncli answer with github.token: pncli skills git-auth enable --host ${r.host}` : `Give git a credential for ${r.host} (a credential helper, or pncli skills git-auth enable --host ${r.host})`
|
|
1427
|
+
});
|
|
1428
|
+
} else if (!r.github) {
|
|
1429
|
+
problems.push({
|
|
1430
|
+
message: `The credential from ${where} (${r.token?.fingerprint}) cannot read ${r.effectiveUrl}: ${r.access.error}`,
|
|
1431
|
+
fix: "Check the token has not expired and its account can read this repo; replace it where it is mapped"
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const g = r.github;
|
|
1436
|
+
if (g) {
|
|
1437
|
+
const t = g.token;
|
|
1438
|
+
const who = `The token from ${where} (${r.token?.fingerprint}${t.login ? `, ${t.login}` : ""})`;
|
|
1439
|
+
if (t.status === 401) problems.push({ message: `${who} was rejected \u2014 expired or revoked`, fix: "Generate a new token and replace it where it is mapped" });
|
|
1440
|
+
if (t.ssoAuthorizationRequired || g.repo?.ssoAuthorizationRequired) {
|
|
1441
|
+
problems.push({ message: `${who} is not authorized for the organization's SAML SSO`, fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize for the org" });
|
|
1442
|
+
}
|
|
1443
|
+
if (t.missingRepoScope) problems.push({ message: `${who} is a classic token without the "repo" scope, so it cannot read private repositories`, fix: 'Edit the token (Settings \u2192 Developer settings \u2192 Tokens (classic)) and tick "repo"' });
|
|
1444
|
+
if (g.repo && !g.repo.canRead && !g.repo.ssoAuthorizationRequired && t.valid) {
|
|
1445
|
+
problems.push({
|
|
1446
|
+
message: `${who} is valid but cannot see ${githubRepoSlug(r.effectiveUrl.replace(/\/\/[^@]+@/, "//"))} (HTTP ${g.repo.status})`,
|
|
1447
|
+
fix: r.token?.kind === "fine-grained" ? "Add this repository to the fine-grained token's repository access, or map a different token for it" : "The token's account needs access to this repo (or the org may block classic tokens); map a token from an account that has access"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
if (t.expiresInDays !== null && t.expiresInDays <= 14) {
|
|
1451
|
+
problems.push({ message: t.expiresInDays < 0 ? `${who} expired on ${t.expiresAt}` : `${who} expires in ${t.expiresInDays} day(s)`, fix: "Regenerate it and replace it where it is mapped" });
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return problems;
|
|
1455
|
+
}
|
|
1456
|
+
function listRemotes(cwd, git = defaultGitRunner) {
|
|
1457
|
+
const r = git(["-C", cwd, "config", "--get-regexp", "^remote\\..*\\.url$"]);
|
|
1458
|
+
if (r.status !== 0) return [];
|
|
1459
|
+
return r.stdout.split(/\r?\n/).map((l) => /^remote\.(.+)\.url (.+)$/.exec(l.trim())).filter((m) => !!m).map((m) => [m[1], m[2]]);
|
|
1460
|
+
}
|
|
1461
|
+
function findClones(root, depth) {
|
|
1462
|
+
const found = [];
|
|
1463
|
+
const walk = (dir, level) => {
|
|
1464
|
+
if (fs.existsSync(path.join(dir, ".git"))) {
|
|
1465
|
+
found.push(dir);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (level >= depth) return;
|
|
1469
|
+
let entries;
|
|
1470
|
+
try {
|
|
1471
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1472
|
+
} catch {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
for (const e of entries) {
|
|
1476
|
+
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") walk(path.join(dir, e.name), level + 1);
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
walk(path.resolve(root), 0);
|
|
1480
|
+
return found;
|
|
1481
|
+
}
|
|
1482
|
+
async function listMappings(cwd, cfg, deps = {}) {
|
|
1483
|
+
const git = gitIn(cwd, deps.git ?? defaultGitRunner);
|
|
1484
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1485
|
+
const reports = [];
|
|
1486
|
+
for (const m of parseInsteadOf(git(["config", "--get-regexp", "^url\\..*\\.insteadof$"]).stdout)) {
|
|
1487
|
+
const embedded = tokenFromUrl(m.rawTo);
|
|
1488
|
+
let host = null;
|
|
1489
|
+
try {
|
|
1490
|
+
host = new URL(m.rawTo).host.toLowerCase();
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
const provider = detectProvider(m.rawTo.replace(/\/\/[^@]+@/, "//"), cfg);
|
|
1494
|
+
let github = null;
|
|
1495
|
+
if (embedded && host && provider === "github" && !deps.offline) {
|
|
1496
|
+
github = await inspectGitHubToken(apiBaseForHost(host, cfg.github?.baseUrl), embedded.token, fetchFn);
|
|
1497
|
+
}
|
|
1498
|
+
reports.push({
|
|
1499
|
+
kind: "insteadOf",
|
|
1500
|
+
from: m.from,
|
|
1501
|
+
to: m.to,
|
|
1502
|
+
token: embedded ? { kind: provider === "github" ? classifyGitHubToken(embedded.token) : "unknown", fingerprint: fingerprint(embedded.token) } : null,
|
|
1503
|
+
github
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
for (const h of parseScopedHelpers(git(["config", "--get-regexp", "^credential\\..*\\.helper$"]).stdout)) {
|
|
1507
|
+
reports.push({ kind: "helper", from: h.scope, to: redactSecrets(h.helper), token: null, github: null });
|
|
1508
|
+
}
|
|
1509
|
+
return reports;
|
|
1510
|
+
}
|
|
1511
|
+
function classifyHelper(helper) {
|
|
1512
|
+
const h = helper.trim().replace(/^!/, "").replace(/['"]/g, "").replace(/\\/g, "/");
|
|
1513
|
+
const exe = h.split(/\s+(?=[a-z-]+(\s|$))/i)[0].split("/").pop().replace(/\.exe$/i, "").toLowerCase();
|
|
1514
|
+
if (/^manager(-core)?(\s|$)/.test(h) || /git-credential-manager/i.test(h)) return "git-credential-manager";
|
|
1515
|
+
if (/^wincred(\s|$)/.test(h) || exe === "git-credential-wincred") return "wincred";
|
|
1516
|
+
if (/^osxkeychain(\s|$)/.test(h) || exe === "git-credential-osxkeychain") return "osxkeychain";
|
|
1517
|
+
if (/libsecret/i.test(h)) return "libsecret";
|
|
1518
|
+
if (/^store(\s|$)/.test(h)) return "store";
|
|
1519
|
+
if (/^cache(\s|$)/.test(h)) return "cache";
|
|
1520
|
+
if (/(^|\/)gh(\.exe)?\s+auth\s+git-credential/i.test(h)) return "gh";
|
|
1521
|
+
if (/(^|\/)pncli(\.cmd)?\s+skills\s+git-credential/i.test(h)) return "pncli";
|
|
1522
|
+
return "other";
|
|
1523
|
+
}
|
|
1524
|
+
function gcmCredentialStore(git, platform = process.platform, env = process.env) {
|
|
1525
|
+
const r = git(["config", "--get", "credential.credentialStore"]);
|
|
1526
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
1527
|
+
if (env.GCM_CREDENTIAL_STORE) return env.GCM_CREDENTIAL_STORE;
|
|
1528
|
+
return platform === "win32" ? "wincredman" : platform === "darwin" ? "keychain" : "unset";
|
|
1529
|
+
}
|
|
1530
|
+
function parseGitTarget(target) {
|
|
1531
|
+
const m = /^git:(https?:\/\/.+)$/i.exec(target);
|
|
1532
|
+
if (!m) return null;
|
|
1533
|
+
try {
|
|
1534
|
+
const u = new URL(m[1]);
|
|
1535
|
+
const p = u.pathname.replace(/^\/+|\/+$/g, "");
|
|
1536
|
+
return { host: u.host.toLowerCase(), path: p || null, username: u.username ? decodeURIComponent(u.username) : null };
|
|
1537
|
+
} catch {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function parseGitCredentialsFile(content) {
|
|
1542
|
+
const out = [];
|
|
1543
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1544
|
+
const t = line.trim();
|
|
1545
|
+
if (!t) continue;
|
|
1546
|
+
try {
|
|
1547
|
+
const u = new URL(t);
|
|
1548
|
+
const found = tokenFromUrl(t);
|
|
1549
|
+
if (!found) continue;
|
|
1550
|
+
const p = u.pathname.replace(/^\/+|\/+$/g, "");
|
|
1551
|
+
out.push({ host: u.host.toLowerCase(), path: p || null, username: found.username || null, secret: found.token });
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1557
|
+
function parseMacKeychainDump(output) {
|
|
1558
|
+
const out = [];
|
|
1559
|
+
for (const block of output.split(/^keychain: /m)) {
|
|
1560
|
+
if (!/^class: "inet"/m.test(block)) continue;
|
|
1561
|
+
const attr = (name) => new RegExp(`"${name}"<[^>]+>="([^"]*)"`).exec(block)?.[1] ?? null;
|
|
1562
|
+
const protocol = attr("ptcl");
|
|
1563
|
+
if (protocol && protocol !== "htps" && protocol !== "http") continue;
|
|
1564
|
+
const host = attr("srvr");
|
|
1565
|
+
if (!host) continue;
|
|
1566
|
+
const p = attr("path");
|
|
1567
|
+
out.push({ host: host.toLowerCase(), path: p ? p.replace(/^\/+/, "") || null : null, username: attr("acct") });
|
|
1568
|
+
}
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
function isGitHubHost(host, cfg) {
|
|
1572
|
+
return detectProvider(`https://${host}/`, cfg) === "github";
|
|
1573
|
+
}
|
|
1574
|
+
async function listStoredCredentials(hosts, cfg, deps = {}) {
|
|
1575
|
+
const git = deps.git ?? defaultGitRunner;
|
|
1576
|
+
const platform = deps.platform ?? process.platform;
|
|
1577
|
+
const env = deps.env ?? process.env;
|
|
1578
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1579
|
+
const run = deps.run ?? defaultRunner;
|
|
1580
|
+
const cache = deps.cache ?? /* @__PURE__ */ new Map();
|
|
1581
|
+
const readFile2 = deps.readFile ?? ((p) => {
|
|
1582
|
+
try {
|
|
1583
|
+
return fs.readFileSync(p, "utf8");
|
|
1584
|
+
} catch {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
const home = deps.homedir ?? os.homedir();
|
|
1589
|
+
const helperOut = git(["config", "--get-all", "credential.helper"]);
|
|
1590
|
+
const rawHelpers = helperOut.status === 0 ? helperOut.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : [];
|
|
1591
|
+
const kinds = [...new Set(rawHelpers.map(classifyHelper))];
|
|
1592
|
+
const configured = rawHelpers.map(redactSecrets);
|
|
1593
|
+
const report = {
|
|
1594
|
+
helpers: { configured, kinds, gcmCredentialStore: kinds.includes("git-credential-manager") ? gcmCredentialStore(git, platform, env) : null },
|
|
1595
|
+
stores: [],
|
|
1596
|
+
probes: [],
|
|
1597
|
+
problems: []
|
|
1598
|
+
};
|
|
1599
|
+
const tokenInfo = (secret, host) => ({ kind: isGitHubHost(host, cfg) ? classifyGitHubToken(secret) : "unknown", fingerprint: fingerprint(secret) });
|
|
1600
|
+
if (platform === "win32") {
|
|
1601
|
+
try {
|
|
1602
|
+
const entries = [];
|
|
1603
|
+
for (const it of windowsListCredentials("git:*", run)) {
|
|
1604
|
+
const t = parseGitTarget(it.target);
|
|
1605
|
+
if (!t) continue;
|
|
1606
|
+
entries.push({
|
|
1607
|
+
store: "windows-credential-manager",
|
|
1608
|
+
host: t.host,
|
|
1609
|
+
path: t.path,
|
|
1610
|
+
username: it.user || t.username,
|
|
1611
|
+
token: it.secret ? tokenInfo(it.secret, t.host) : null,
|
|
1612
|
+
location: it.target,
|
|
1613
|
+
github: null,
|
|
1614
|
+
secret: it.secret || void 0
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
report.stores.push({ store: "windows-credential-manager", available: true, error: null, entries });
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
report.stores.push({ store: "windows-credential-manager", available: false, error: err instanceof Error ? err.message : String(err), entries: [] });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
for (const file of [path.join(home, ".git-credentials"), path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "git", "credentials")]) {
|
|
1623
|
+
const content = readFile2(file);
|
|
1624
|
+
if (content === null) continue;
|
|
1625
|
+
const entries = parseGitCredentialsFile(content).map((e) => ({
|
|
1626
|
+
store: "git-credentials-file",
|
|
1627
|
+
host: e.host,
|
|
1628
|
+
path: e.path,
|
|
1629
|
+
username: e.username,
|
|
1630
|
+
token: tokenInfo(e.secret, e.host),
|
|
1631
|
+
location: file,
|
|
1632
|
+
github: null,
|
|
1633
|
+
secret: e.secret
|
|
1634
|
+
}));
|
|
1635
|
+
report.stores.push({ store: "git-credentials-file", available: true, error: null, entries });
|
|
1636
|
+
}
|
|
1637
|
+
if (platform === "darwin") {
|
|
1638
|
+
const r = run("security", ["dump-keychain"]);
|
|
1639
|
+
if (r.status === 0) {
|
|
1640
|
+
const entries = parseMacKeychainDump(r.stdout).map((e) => ({
|
|
1641
|
+
store: "macos-keychain",
|
|
1642
|
+
host: e.host,
|
|
1643
|
+
path: e.path,
|
|
1644
|
+
username: e.username,
|
|
1645
|
+
token: null,
|
|
1646
|
+
location: `login keychain: ${e.host}`,
|
|
1647
|
+
github: null
|
|
1648
|
+
}));
|
|
1649
|
+
report.stores.push({ store: "macos-keychain", available: true, error: null, entries });
|
|
1650
|
+
} else {
|
|
1651
|
+
report.stores.push({ store: "macos-keychain", available: false, error: r.stderr.trim() || `security exited ${r.status}`, entries: [] });
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
1655
|
+
for (const h of hosts) scopes.set(h.toLowerCase(), { host: h.toLowerCase(), path: null });
|
|
1656
|
+
for (const e of report.stores.flatMap((st) => st.entries)) scopes.set(e.path ? `${e.host}/${e.path}` : e.host, { host: e.host, path: e.path });
|
|
1657
|
+
const probeSecrets = /* @__PURE__ */ new Map();
|
|
1658
|
+
for (const [key, { host, path: p }] of [...scopes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1659
|
+
const filled = credentialFill(host, p ?? void 0, git);
|
|
1660
|
+
const h = git(["config", "--get-urlmatch", "credential.helper", `https://${host}/${p ?? ""}`]);
|
|
1661
|
+
const rawHelper = h.status === 0 ? h.stdout.trim().split(/\r?\n/).pop() ?? null : null;
|
|
1662
|
+
const helper = rawHelper ? redactSecrets(rawHelper) : null;
|
|
1663
|
+
if (filled) probeSecrets.set(key, filled.password);
|
|
1664
|
+
report.probes.push({
|
|
1665
|
+
host,
|
|
1666
|
+
path: p,
|
|
1667
|
+
username: filled?.username || null,
|
|
1668
|
+
token: filled ? tokenInfo(filled.password, host) : null,
|
|
1669
|
+
helper,
|
|
1670
|
+
helperKind: rawHelper ? classifyHelper(rawHelper) : null
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const shadowed = [];
|
|
1674
|
+
for (const store of report.stores) {
|
|
1675
|
+
for (const e of store.entries) {
|
|
1676
|
+
const key = e.path ? `${e.host}/${e.path}` : e.host;
|
|
1677
|
+
const sent = probeSecrets.get(key);
|
|
1678
|
+
const probe = report.probes.find((pr) => pr.host === e.host && pr.path === e.path);
|
|
1679
|
+
const siblings = store.entries.filter((x) => x.host === e.host && x.path === e.path).map((x) => x.secret);
|
|
1680
|
+
if (e.secret && sent && probe && !siblings.includes(sent)) shadowed.push({ entry: e, store: store.store, probe });
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (!deps.offline) {
|
|
1684
|
+
for (const store of report.stores) {
|
|
1685
|
+
for (const e of store.entries) {
|
|
1686
|
+
if (!e.secret || !isGitHubHost(e.host, cfg)) continue;
|
|
1687
|
+
const apiBase = apiBaseForHost(e.host, cfg.github?.baseUrl);
|
|
1688
|
+
const key = `${apiBase}\0${e.secret}`;
|
|
1689
|
+
if (!cache.has(key)) cache.set(key, await inspectGitHubToken(apiBase, e.secret, fetchFn));
|
|
1690
|
+
e.github = cache.get(key);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
report.problems = [...buildStoredProblems(report), ...shadowed.map(({ entry: e, store, probe }) => ({
|
|
1695
|
+
message: `git never sends the ${store} credential for ${e.path ? `${e.host}/${e.path}` : e.host} (${e.username ?? "no username"}, ${e.token?.fingerprint}): the ${probe.helperKind ?? "configured"} helper answers first with ${probe.username ?? "no username"}, ${probe.token?.fingerprint}`,
|
|
1696
|
+
fix: store === "windows-credential-manager" ? `If the stored one is obsolete, delete it: cmdkey /delete:"${e.location}" (git credential reject would go to the ${probe.helperKind ?? "other"} helper, not to this entry). If it should win instead, remove the helper entry from your gitconfig.` : `If the stored one is obsolete, remove it from ${e.location}; if it should win, remove the ${probe.helperKind ?? "other"} helper entry from your gitconfig.`
|
|
1697
|
+
}))];
|
|
1698
|
+
for (const store of report.stores) for (const e of store.entries) delete e.secret;
|
|
1699
|
+
return report;
|
|
1700
|
+
}
|
|
1701
|
+
function buildStoredProblems(r) {
|
|
1702
|
+
const problems = [];
|
|
1703
|
+
const forget = (e) => `pncli git credentials forget --host ${e.host}${e.username ? ` --username ${e.username}` : ""}${e.path ? ` --path ${e.path}` : ""}`;
|
|
1704
|
+
for (const store of r.stores) {
|
|
1705
|
+
for (const e of store.entries) {
|
|
1706
|
+
const who = `The ${store.store} credential for ${e.host}${e.path ? `/${e.path}` : ""} (${e.username ?? "no username"}, ${e.token?.fingerprint ?? "no secret"})`;
|
|
1707
|
+
const g = e.github;
|
|
1708
|
+
if (g?.status === 401) {
|
|
1709
|
+
problems.push({ message: `${who} is rejected by GitHub \u2014 expired or revoked. git keeps sending it until it is removed`, fix: forget(e) });
|
|
1710
|
+
} else if (g && g.expiresInDays !== null && g.expiresInDays <= 14) {
|
|
1711
|
+
problems.push({ message: g.expiresInDays < 0 ? `${who} expired on ${g.expiresAt}` : `${who} expires in ${g.expiresInDays} day(s)`, fix: `Replace it: ${forget(e)}, then sign in again` });
|
|
1712
|
+
}
|
|
1713
|
+
if (g?.missingRepoScope) problems.push({ message: `${who} is a classic token without the "repo" scope \u2014 it cannot read private repositories`, fix: `${forget(e)}, then store a token with "repo"` });
|
|
1714
|
+
if (g?.ssoAuthorizationRequired) problems.push({ message: `${who} is not authorized for the organization's SAML SSO`, fix: "GitHub \u2192 Settings \u2192 Developer settings \u2192 Personal access tokens \u2192 Configure SSO \u2192 Authorize" });
|
|
1715
|
+
if (store.store === "git-credentials-file") {
|
|
1716
|
+
problems.push({ message: `${who} is stored in plaintext in ${e.location}`, fix: "Switch to an OS-backed helper (Windows: git config --global credential.helper manager; macOS: osxkeychain) and delete that line" });
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1720
|
+
for (const e of store.entries) {
|
|
1721
|
+
const k = e.path ? `${e.host}/${e.path}` : e.host;
|
|
1722
|
+
if (!byScope.has(k)) byScope.set(k, /* @__PURE__ */ new Set());
|
|
1723
|
+
byScope.get(k).add(e.username ?? "");
|
|
1724
|
+
}
|
|
1725
|
+
for (const [k, users] of byScope) {
|
|
1726
|
+
if (users.size < 2) continue;
|
|
1727
|
+
problems.push({
|
|
1728
|
+
message: `${store.store} holds ${users.size} accounts for ${k} (${[...users].filter(Boolean).join(", ")}) \u2014 git may send a different one than you expect`,
|
|
1729
|
+
fix: `Pin the account: git config --global credential.https://${k}.username <account> (or remove the extra one with pncli git credentials forget)`
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return problems;
|
|
1734
|
+
}
|
|
1735
|
+
function forgetCredential(host, opts = {}, git = defaultGitRunner) {
|
|
1736
|
+
if (opts.path) {
|
|
1737
|
+
const r = git(["config", "--get-urlmatch", "credential.useHttpPath", `https://${host}/${opts.path}`]);
|
|
1738
|
+
if (r.status !== 0 || r.stdout.trim().toLowerCase() !== "true") {
|
|
1739
|
+
throw new Error(`git ignores --path for ${host}: credential.useHttpPath is not enabled for that URL, so this would forget the host-wide credential. Omit --path to forget that deliberately.`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const before = credentialFill(host, opts.path, git);
|
|
1743
|
+
if (!before) return { host, before: null, after: null, removed: false };
|
|
1744
|
+
const username = opts.username ?? before?.username;
|
|
1745
|
+
const sameAsFilled = before && (!opts.username || opts.username === before.username);
|
|
1746
|
+
const fields = [
|
|
1747
|
+
"protocol=https",
|
|
1748
|
+
`host=${host}`,
|
|
1749
|
+
...opts.path ? [`path=${opts.path}`] : [],
|
|
1750
|
+
...username ? [`username=${username}`] : [],
|
|
1751
|
+
...sameAsFilled ? [`password=${before.password}`] : []
|
|
1752
|
+
];
|
|
1753
|
+
git(["credential", "reject"], { input: `${fields.join("\n")}
|
|
1754
|
+
|
|
1755
|
+
`, env: { GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" } });
|
|
1756
|
+
const after = credentialFill(host, opts.path, git);
|
|
1757
|
+
return {
|
|
1758
|
+
host,
|
|
1759
|
+
before: before ? fingerprint(before.password) : null,
|
|
1760
|
+
after: after ? fingerprint(after.password) : null,
|
|
1761
|
+
removed: !!before && (!after || after.password !== before.password)
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
820
1765
|
// src/services/git/commands.ts
|
|
1766
|
+
import fs2 from "fs";
|
|
1767
|
+
import path2 from "path";
|
|
821
1768
|
function requireRepoRoot() {
|
|
822
1769
|
const root = getRepoRoot();
|
|
823
1770
|
if (!root) throw new PncliError("Not a git repository", 1);
|
|
@@ -915,11 +1862,136 @@ function registerGitCommands(program2) {
|
|
|
915
1862
|
fail(err, "git", "current-pr", start);
|
|
916
1863
|
}
|
|
917
1864
|
});
|
|
1865
|
+
const credentials = git.command("credentials").description("Inspect which credential git uses for the remotes of a clone (insteadOf mappings, embedded tokens, helpers) and whether it can read the repo");
|
|
1866
|
+
credentials.addHelpText("after", `
|
|
1867
|
+
Read-only except forget. Tokens are never printed \u2014 only their type and a fingerprint (ghp_\u2026a1b2).
|
|
1868
|
+
|
|
1869
|
+
Examples:
|
|
1870
|
+
pncli git credentials inspect # every remote of the clone you are in
|
|
1871
|
+
pncli git credentials inspect ~/src/tools --remote upstream
|
|
1872
|
+
pncli git credentials inspect https://github.com/imagile/tools.git
|
|
1873
|
+
pncli git credentials inspect --scan ~/src --problems-only # clones mapped without a token, or without access
|
|
1874
|
+
pncli git credentials mappings # every insteadOf rewrite and URL-scoped helper, tokens checked
|
|
1875
|
+
pncli git credentials stored # Git Credential Manager / Windows Credential Manager /
|
|
1876
|
+
# Keychain / ~/.git-credentials: accounts, token health
|
|
1877
|
+
pncli git credentials forget --host github.com --username octo # drop a stale stored credential
|
|
1878
|
+
`);
|
|
1879
|
+
credentials.command("inspect").description("Show the credential each remote uses after insteadOf rewriting, and (online) whether it can read the repo").argument("[target]", "A clone directory or a remote URL (default: the current clone)").option("--remote <name>", "Only this remote (default: all remotes)").option("--scan <dir>", "Inspect every clone under this directory instead").option("--depth <n>", "How many directory levels --scan descends", "2").option("--problems-only", "With --scan, only list remotes that have problems").option("--offline", "Skip network checks (ls-remote and the GitHub API)").action(async (target, cmdOpts) => {
|
|
1880
|
+
const start = Date.now();
|
|
1881
|
+
try {
|
|
1882
|
+
const opts = program2.optsWithGlobals();
|
|
1883
|
+
const cfg = (() => {
|
|
1884
|
+
try {
|
|
1885
|
+
return loadConfig({ configPath: opts.config });
|
|
1886
|
+
} catch {
|
|
1887
|
+
return {};
|
|
1888
|
+
}
|
|
1889
|
+
})();
|
|
1890
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1891
|
+
const deps = { offline: !!cmdOpts.offline, cache };
|
|
1892
|
+
const inspectClone = async (dir2) => {
|
|
1893
|
+
const remotes2 = listRemotes(dir2).filter(([name]) => !cmdOpts.remote || name === cmdOpts.remote);
|
|
1894
|
+
const out = [];
|
|
1895
|
+
for (const [name, url] of remotes2) out.push(await inspectRemoteAuth(name, url, dir2, cfg, deps));
|
|
1896
|
+
return out;
|
|
1897
|
+
};
|
|
1898
|
+
if (cmdOpts.scan) {
|
|
1899
|
+
const depth = Number.parseInt(cmdOpts.depth, 10);
|
|
1900
|
+
if (!Number.isInteger(depth) || depth < 0) throw new PncliError("--depth must be a non-negative integer", 1);
|
|
1901
|
+
const clones = findClones(cmdOpts.scan, depth);
|
|
1902
|
+
const results = [];
|
|
1903
|
+
for (const dir2 of clones) {
|
|
1904
|
+
const remotes2 = await inspectClone(dir2);
|
|
1905
|
+
const shown = cmdOpts.problemsOnly ? remotes2.filter((r) => r.problems.length > 0) : remotes2;
|
|
1906
|
+
if (shown.length > 0) results.push({ path: dir2, remotes: shown });
|
|
1907
|
+
}
|
|
1908
|
+
const all = results.flatMap((r) => r.remotes);
|
|
1909
|
+
success({
|
|
1910
|
+
root: path2.resolve(cmdOpts.scan),
|
|
1911
|
+
clonesScanned: clones.length,
|
|
1912
|
+
summary: {
|
|
1913
|
+
remotes: all.length,
|
|
1914
|
+
withProblems: all.filter((r) => r.problems.length > 0).length,
|
|
1915
|
+
noCredential: all.filter((r) => r.host && r.credentialSource === "none").length,
|
|
1916
|
+
cannotRead: all.filter((r) => r.access && !r.access.ok).length
|
|
1917
|
+
},
|
|
1918
|
+
clones: results
|
|
1919
|
+
}, "git", "credentials-inspect", start);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (target && /^(https?:\/\/|git@|ssh:\/\/)/i.test(target)) {
|
|
1923
|
+
success({ remotes: [await inspectRemoteAuth(null, target, null, cfg, deps)] }, "git", "credentials-inspect", start);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
const dir = target ? path2.resolve(target) : requireRepoRoot();
|
|
1927
|
+
if (!fs2.existsSync(path2.join(dir, ".git"))) throw new PncliError(`${dir} is not a git clone`, 1);
|
|
1928
|
+
const remotes = await inspectClone(dir);
|
|
1929
|
+
if (remotes.length === 0) throw new PncliError(cmdOpts.remote ? `No remote named "${cmdOpts.remote}" in ${dir}` : `${dir} has no remotes`, 1);
|
|
1930
|
+
success({ path: dir, remotes }, "git", "credentials-inspect", start);
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
fail(err, "git", "credentials-inspect", start);
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
credentials.command("mappings").description("List every insteadOf URL rewrite and URL-scoped credential helper in your gitconfig, tokens redacted and (online) validated").option("--offline", "Skip validating embedded GitHub tokens").action(async (cmdOpts) => {
|
|
1936
|
+
const start = Date.now();
|
|
1937
|
+
try {
|
|
1938
|
+
const opts = program2.optsWithGlobals();
|
|
1939
|
+
const cfg = (() => {
|
|
1940
|
+
try {
|
|
1941
|
+
return loadConfig({ configPath: opts.config });
|
|
1942
|
+
} catch {
|
|
1943
|
+
return {};
|
|
1944
|
+
}
|
|
1945
|
+
})();
|
|
1946
|
+
const mappings = await listMappings(getRepoRoot(), cfg, { offline: !!cmdOpts.offline });
|
|
1947
|
+
success({ mappings, total: mappings.length }, "git", "credentials-mappings", start);
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
fail(err, "git", "credentials-mappings", start);
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
credentials.command("stored").description("List credentials git keeps in Git Credential Manager, Windows Credential Manager, the macOS Keychain, or ~/.git-credentials, with what git would send per host and (online) token health").option("--host <host...>", "Also ask git what it would send for these hosts").option("--offline", "Skip validating GitHub tokens").action(async (cmdOpts) => {
|
|
1953
|
+
const start = Date.now();
|
|
1954
|
+
try {
|
|
1955
|
+
const opts = program2.optsWithGlobals();
|
|
1956
|
+
const cfg = (() => {
|
|
1957
|
+
try {
|
|
1958
|
+
return loadConfig({ configPath: opts.config });
|
|
1959
|
+
} catch {
|
|
1960
|
+
return {};
|
|
1961
|
+
}
|
|
1962
|
+
})();
|
|
1963
|
+
const hosts = new Set((cmdOpts.host ?? []).map((h) => h.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "")));
|
|
1964
|
+
for (const h of [gitHostFromApiBaseUrl(cfg.github?.baseUrl), hostOf(cfg.bitbucket?.baseUrl), hostOf(cfg.ado?.baseUrl)]) if (h) hosts.add(h);
|
|
1965
|
+
const root = getRepoRoot();
|
|
1966
|
+
if (root) for (const [, url] of listRemotes(root)) {
|
|
1967
|
+
const h = hostOf(url);
|
|
1968
|
+
if (h) hosts.add(h);
|
|
1969
|
+
}
|
|
1970
|
+
const report = await listStoredCredentials([...hosts], cfg, { offline: !!cmdOpts.offline });
|
|
1971
|
+
success(report, "git", "credentials-stored", start);
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
fail(err, "git", "credentials-stored", start);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
credentials.command("forget").description("Remove a stored credential for a host through git credential reject (works for every helper: Git Credential Manager, wincred, osxkeychain, libsecret, store)").requiredOption("--host <host>", "Host to forget, e.g. github.com").option("--username <username>", "Only this account (when several are stored for the host)").option("--path <path>", "Repository path, for helpers that store per repository (e.g. Azure Repos in Git Credential Manager)").action((cmdOpts) => {
|
|
1977
|
+
const start = Date.now();
|
|
1978
|
+
try {
|
|
1979
|
+
const host = cmdOpts.host.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1980
|
+
const result = forgetCredential(host, { username: cmdOpts.username, path: cmdOpts.path });
|
|
1981
|
+
if (!result.before) throw new PncliError(`git has no stored credential for ${host}${cmdOpts.path ? `/${cmdOpts.path}` : ""} \u2014 nothing to forget`, 1);
|
|
1982
|
+
success({
|
|
1983
|
+
...result,
|
|
1984
|
+
next: result.removed ? "git will ask for (or its helper will fetch) a fresh credential next time" : "git still supplies the same credential \u2014 it may come from an insteadOf mapping or a helper that ignores reject; see: pncli git credentials inspect"
|
|
1985
|
+
}, "git", "credentials-forget", start);
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
fail(err, "git", "credentials-forget", start);
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
918
1990
|
}
|
|
919
1991
|
|
|
920
1992
|
// src/services/jira/commands.ts
|
|
921
1993
|
import { readFileSync as readFileSync3, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
922
|
-
import
|
|
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
|
}
|
|
@@ -1861,7 +2956,7 @@ function registerBitbucketCommands(program2) {
|
|
|
1861
2956
|
const { client, project, repo } = getClient2(bb, { project: opts.project, repo: opts.repo });
|
|
1862
2957
|
const config = loadConfig({ configPath: bb.optsWithGlobals().config });
|
|
1863
2958
|
const target = opts.target ?? config.defaults.bitbucket?.targetBranch ?? "main";
|
|
1864
|
-
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) :
|
|
2959
|
+
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : void 0;
|
|
1865
2960
|
const data = await client.createPR({ project, repo, title: opts.title, source: opts.source, target, description: opts.description, reviewers });
|
|
1866
2961
|
success(data, "bitbucket", "create-pr", start);
|
|
1867
2962
|
} catch (err) {
|
|
@@ -1885,7 +2980,7 @@ function registerBitbucketCommands(program2) {
|
|
|
1885
2980
|
throw new PncliError("--repos must contain at least one repository slug", 1);
|
|
1886
2981
|
}
|
|
1887
2982
|
const title = opts.title ?? `Promote ${opts.from} to ${opts.to}`;
|
|
1888
|
-
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) :
|
|
2983
|
+
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : void 0;
|
|
1889
2984
|
const results = [];
|
|
1890
2985
|
let created = 0;
|
|
1891
2986
|
let nothingToPromote = 0;
|
|
@@ -2306,8 +3401,8 @@ var GitHubClient = class {
|
|
|
2306
3401
|
);
|
|
2307
3402
|
}
|
|
2308
3403
|
async createRepo(opts) {
|
|
2309
|
-
const
|
|
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);
|
|
@@ -6487,35 +7582,295 @@ async function runCredentialChecks(cfg, http) {
|
|
|
6487
7582
|
results.figma = { status: "error", message: "baseUrl not configured" };
|
|
6488
7583
|
} else {
|
|
6489
7584
|
try {
|
|
6490
|
-
await http.figma("/v1/me", { timeoutMs: 1e4 });
|
|
6491
|
-
results.figma = { status: "valid", message: "ok" };
|
|
7585
|
+
await http.figma("/v1/me", { timeoutMs: 1e4 });
|
|
7586
|
+
results.figma = { status: "valid", message: "ok" };
|
|
7587
|
+
} catch (err) {
|
|
7588
|
+
results.figma = categorize(err);
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7591
|
+
if (!cfg.alation.refreshToken) {
|
|
7592
|
+
results.alation = { status: "blank", message: "not configured" };
|
|
7593
|
+
} else if (!cfg.alation.baseUrl) {
|
|
7594
|
+
results.alation = { status: "error", message: "baseUrl not configured" };
|
|
7595
|
+
} else if (cfg.alation.userId === void 0 || cfg.alation.userId === "") {
|
|
7596
|
+
results.alation = { status: "error", message: "userId not configured" };
|
|
7597
|
+
} else {
|
|
7598
|
+
try {
|
|
7599
|
+
await validateAlationAccessToken(cfg);
|
|
7600
|
+
results.alation = { status: "valid", message: "ok" };
|
|
7601
|
+
} catch (err) {
|
|
7602
|
+
results.alation = categorize(err);
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7605
|
+
return results;
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7608
|
+
// src/services/config/keychain-commands.ts
|
|
7609
|
+
import fs4 from "fs";
|
|
7610
|
+
import password from "@inquirer/password";
|
|
7611
|
+
function resolveConfigKey(root, key) {
|
|
7612
|
+
const parts = key.split(".").filter(Boolean);
|
|
7613
|
+
if (parts.length === 0) throw new KeychainError("A config key is required, e.g. github.token");
|
|
7614
|
+
const segments = [];
|
|
7615
|
+
let node = root;
|
|
7616
|
+
for (const [i, part] of parts.entries()) {
|
|
7617
|
+
if (Array.isArray(node)) {
|
|
7618
|
+
const idx = /^\d+$/.test(part) ? Number(part) : node.findIndex((e) => e && typeof e === "object" && e.name === part);
|
|
7619
|
+
if (idx < 0 || idx >= node.length) {
|
|
7620
|
+
throw new KeychainError(`No entry "${part}" in ${parts.slice(0, i).join(".")} \u2014 check the name with: pncli config show`);
|
|
7621
|
+
}
|
|
7622
|
+
segments.push(idx);
|
|
7623
|
+
node = node[idx];
|
|
7624
|
+
} else {
|
|
7625
|
+
segments.push(part);
|
|
7626
|
+
const obj = node;
|
|
7627
|
+
if (i < parts.length - 1 && (typeof obj[part] !== "object" || obj[part] === null)) obj[part] = {};
|
|
7628
|
+
node = obj[part];
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7631
|
+
return segments;
|
|
7632
|
+
}
|
|
7633
|
+
function getAtPath(root, segments) {
|
|
7634
|
+
let node = root;
|
|
7635
|
+
for (const seg of segments) node = node && typeof node === "object" ? node[seg] : void 0;
|
|
7636
|
+
return node;
|
|
7637
|
+
}
|
|
7638
|
+
function deleteAtPath(root, segments) {
|
|
7639
|
+
const parent = getAtPath(root, segments.slice(0, -1));
|
|
7640
|
+
if (parent) delete parent[segments[segments.length - 1]];
|
|
7641
|
+
}
|
|
7642
|
+
async function readSecret(key, opts) {
|
|
7643
|
+
if (opts.value !== void 0) {
|
|
7644
|
+
warn("--value leaves the secret in your shell history. Prefer the interactive prompt or --stdin.");
|
|
7645
|
+
return opts.value;
|
|
7646
|
+
}
|
|
7647
|
+
if (opts.stdin || !process.stdin.isTTY) {
|
|
7648
|
+
const chunks = [];
|
|
7649
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7650
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
7651
|
+
}
|
|
7652
|
+
return (await password({ message: `Secret for ${key}:`, mask: "*" })).trim();
|
|
7653
|
+
}
|
|
7654
|
+
function matchesFilter(config, segments, only) {
|
|
7655
|
+
if (!only || only.size === 0) return true;
|
|
7656
|
+
return only.has(segments.join(".")) || only.has(accountForPath(config, segments));
|
|
7657
|
+
}
|
|
7658
|
+
function unmigrateSecrets(config, backend, dryRun = false, only) {
|
|
7659
|
+
const migrated = [];
|
|
7660
|
+
const failed = [];
|
|
7661
|
+
const refs = findKeychainRefs(config).filter((r) => matchesFilter(config, r.path, only));
|
|
7662
|
+
let secrets = {};
|
|
7663
|
+
if (!dryRun && refs.length > 0) {
|
|
7664
|
+
const accounts = [...new Set(refs.flatMap((r) => {
|
|
7665
|
+
try {
|
|
7666
|
+
return [accountFromRef(r.value)];
|
|
7667
|
+
} catch {
|
|
7668
|
+
return [];
|
|
7669
|
+
}
|
|
7670
|
+
}))];
|
|
7671
|
+
secrets = backend.getMany(accounts);
|
|
7672
|
+
}
|
|
7673
|
+
for (const ref of refs) {
|
|
7674
|
+
let account;
|
|
7675
|
+
try {
|
|
7676
|
+
account = accountFromRef(ref.value);
|
|
7677
|
+
} catch (err) {
|
|
7678
|
+
failed.push({ path: ref.path.join("."), account: ref.value, reason: err.message });
|
|
7679
|
+
continue;
|
|
7680
|
+
}
|
|
7681
|
+
const entry = { path: ref.path.join("."), account };
|
|
7682
|
+
if (dryRun) {
|
|
7683
|
+
migrated.push(entry);
|
|
7684
|
+
continue;
|
|
7685
|
+
}
|
|
7686
|
+
const secret = secrets[account];
|
|
7687
|
+
if (secret == null) {
|
|
7688
|
+
failed.push({ ...entry, reason: "no entry in the keychain" });
|
|
7689
|
+
continue;
|
|
7690
|
+
}
|
|
7691
|
+
setAtPath(config, ref.path, secret);
|
|
7692
|
+
migrated.push(entry);
|
|
7693
|
+
}
|
|
7694
|
+
return { migrated, failed };
|
|
7695
|
+
}
|
|
7696
|
+
function migrateSecrets(config, backend, dryRun = false, only) {
|
|
7697
|
+
const migrated = [];
|
|
7698
|
+
const failed = [];
|
|
7699
|
+
for (const secret of findPlaintextSecrets(config).filter((s) => matchesFilter(config, s.path, only))) {
|
|
7700
|
+
const account = accountForPath(config, secret.path);
|
|
7701
|
+
const entry = { path: secret.path.join("."), account };
|
|
7702
|
+
if (dryRun) {
|
|
7703
|
+
migrated.push(entry);
|
|
7704
|
+
continue;
|
|
7705
|
+
}
|
|
7706
|
+
try {
|
|
7707
|
+
storeVerified(backend, account, secret.value);
|
|
7708
|
+
setAtPath(config, secret.path, keychainRef(account));
|
|
7709
|
+
migrated.push(entry);
|
|
7710
|
+
} catch (err) {
|
|
7711
|
+
failed.push({ ...entry, reason: err instanceof Error ? err.message : String(err) });
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
return { migrated, failed };
|
|
7715
|
+
}
|
|
7716
|
+
function registerKeychainCommands(config, program2) {
|
|
7717
|
+
const keychain = config.command("keychain").description("Store credentials in the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service) instead of plaintext config");
|
|
7718
|
+
keychain.addHelpText("after", `
|
|
7719
|
+
How it works:
|
|
7720
|
+
A config value of the form "keychain:<account>" is a reference. pncli looks the secret up in the
|
|
7721
|
+
OS credential store at run time; ~/.pncli/config.json only ever holds the reference.
|
|
7722
|
+
PNCLI_* environment variables still take precedence over anything stored here.
|
|
7723
|
+
|
|
7724
|
+
Examples:
|
|
7725
|
+
pncli config keychain migrate --dry-run # which plaintext secrets would move
|
|
7726
|
+
pncli config keychain migrate # config -> keychain, every plaintext secret
|
|
7727
|
+
pncli config keychain migrate github.token # just one key
|
|
7728
|
+
pncli config keychain migrate --to config # keychain -> config (entries kept)
|
|
7729
|
+
pncli config keychain migrate --to config --purge # keychain -> config, then delete entries
|
|
7730
|
+
pncli config keychain set github.token # prompts for the secret (or pipe it: --stdin)
|
|
7731
|
+
pncli config keychain set marketplaces.internal-ai.token
|
|
7732
|
+
pncli config keychain status # backend, every reference, and whether it resolves
|
|
7733
|
+
pncli config keychain remove github.token --restore # back to plaintext config
|
|
7734
|
+
|
|
7735
|
+
Set PNCLI_KEYCHAIN_BACKEND=none to ignore references entirely (e.g. on a CI runner).
|
|
7736
|
+
`);
|
|
7737
|
+
keychain.command("status").description("Show the keychain backend, every keychain reference in config, and plaintext secrets that could move").action(() => {
|
|
7738
|
+
const start = Date.now();
|
|
7739
|
+
try {
|
|
7740
|
+
const opts = program2.optsWithGlobals();
|
|
7741
|
+
const raw = loadJsonFile(getGlobalConfigPath(opts.config)) ?? {};
|
|
7742
|
+
const backend = getKeychainBackend();
|
|
7743
|
+
const refs = findKeychainRefs(raw);
|
|
7744
|
+
const available = backend.available();
|
|
7745
|
+
let lookups = {};
|
|
7746
|
+
let lookupError = null;
|
|
7747
|
+
if (available && refs.length > 0) {
|
|
7748
|
+
try {
|
|
7749
|
+
lookups = backend.getMany([...new Set(refs.map((r) => accountFromRef(r.value)))]);
|
|
7750
|
+
} catch (err) {
|
|
7751
|
+
if (err instanceof KeychainBatchError) lookups = err.partial;
|
|
7752
|
+
lookupError = err instanceof Error ? err.message : String(err);
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
const plaintext = findPlaintextSecrets(raw).map((s) => s.path.join("."));
|
|
7756
|
+
success({
|
|
7757
|
+
backend: backend.name,
|
|
7758
|
+
store: backend.store,
|
|
7759
|
+
available,
|
|
7760
|
+
references: refs.map((r) => {
|
|
7761
|
+
const account = r.value.slice("keychain:".length);
|
|
7762
|
+
return { path: r.path.join("."), account, resolves: available ? lookups[account] != null : false };
|
|
7763
|
+
}),
|
|
7764
|
+
lookupError,
|
|
7765
|
+
plaintextSecrets: plaintext,
|
|
7766
|
+
hint: plaintext.length > 0 && available ? `Move ${plaintext.length} plaintext secret(s) into the keychain with: pncli config keychain migrate` : null
|
|
7767
|
+
}, "config", "keychain-status", start);
|
|
7768
|
+
} catch (err) {
|
|
7769
|
+
fail(err, "config", "keychain-status", start);
|
|
7770
|
+
}
|
|
7771
|
+
});
|
|
7772
|
+
keychain.command("set").description("Store a secret in the OS keychain and point a config key at it").argument("<key>", "Config key in dot notation, e.g. github.token or marketplaces.<name>.token").option("--stdin", "Read the secret from stdin instead of prompting").option("--value <secret>", "Pass the secret inline (ends up in shell history \u2014 prefer the prompt or --stdin)").option("--account <account>", "Keychain account name to store under (default: the config key)").action(async (key, cmdOpts) => {
|
|
7773
|
+
const start = Date.now();
|
|
7774
|
+
try {
|
|
7775
|
+
const opts = program2.optsWithGlobals();
|
|
7776
|
+
const configPath = getGlobalConfigPath(opts.config);
|
|
7777
|
+
const raw = loadJsonFile(configPath) ?? {};
|
|
7778
|
+
const segments = resolveConfigKey(raw, key);
|
|
7779
|
+
const account = cmdOpts.account ?? accountForPath(raw, segments);
|
|
7780
|
+
const secret = await readSecret(key, cmdOpts);
|
|
7781
|
+
const backend = getKeychainBackend();
|
|
7782
|
+
storeVerified(backend, account, secret);
|
|
7783
|
+
setAtPath(raw, segments, keychainRef(account));
|
|
7784
|
+
writeGlobalConfig(raw, opts.config);
|
|
7785
|
+
resetKeychainCache();
|
|
7786
|
+
success({ key: segments.join("."), account, store: backend.store, reference: keychainRef(account) }, "config", "keychain-set", start);
|
|
6492
7787
|
} catch (err) {
|
|
6493
|
-
|
|
7788
|
+
fail(err, "config", "keychain-set", start);
|
|
6494
7789
|
}
|
|
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 {
|
|
7790
|
+
});
|
|
7791
|
+
keychain.command("remove").description("Delete a secret from the OS keychain and clear its config reference").argument("<key>", "Config key holding a keychain reference, e.g. github.token").option("--restore", "Write the secret back into config as plaintext instead of clearing the key").action((key, cmdOpts) => {
|
|
7792
|
+
const start = Date.now();
|
|
6503
7793
|
try {
|
|
6504
|
-
|
|
6505
|
-
|
|
7794
|
+
const opts = program2.optsWithGlobals();
|
|
7795
|
+
const raw = loadJsonFile(getGlobalConfigPath(opts.config)) ?? {};
|
|
7796
|
+
const segments = resolveConfigKey(raw, key);
|
|
7797
|
+
const current = getAtPath(raw, segments);
|
|
7798
|
+
if (!isKeychainRef(current)) {
|
|
7799
|
+
throw new KeychainError(`${key} is not a keychain reference (nothing to remove).`);
|
|
7800
|
+
}
|
|
7801
|
+
const account = accountFromRef(current);
|
|
7802
|
+
const backend = getKeychainBackend();
|
|
7803
|
+
let restored = false;
|
|
7804
|
+
if (cmdOpts.restore) {
|
|
7805
|
+
const secret = backend.getMany([account])[account];
|
|
7806
|
+
if (secret == null) throw new KeychainError(`No keychain entry for "${account}" \u2014 nothing to restore. Re-run without --restore to clear the reference.`);
|
|
7807
|
+
setAtPath(raw, segments, secret);
|
|
7808
|
+
restored = true;
|
|
7809
|
+
} else {
|
|
7810
|
+
deleteAtPath(raw, segments);
|
|
7811
|
+
}
|
|
7812
|
+
writeGlobalConfig(raw, opts.config);
|
|
7813
|
+
const deleted = purgeEntries(raw, backend, [account]).length > 0;
|
|
7814
|
+
resetKeychainCache();
|
|
7815
|
+
success({ key: segments.join("."), account, deleted, restoredToConfig: restored }, "config", "keychain-remove", start);
|
|
6506
7816
|
} catch (err) {
|
|
6507
|
-
|
|
7817
|
+
fail(err, "config", "keychain-remove", start);
|
|
6508
7818
|
}
|
|
6509
|
-
}
|
|
6510
|
-
|
|
7819
|
+
});
|
|
7820
|
+
keychain.command("migrate").description("Move secrets between plaintext config and the OS keychain (--to keychain, the default, or --to config)").argument("[keys...]", "Limit to these config keys (e.g. github.token marketplaces.internal-ai.token); default: all").option("--to <destination>", "keychain (move plaintext secrets in) or config (move keychain secrets back out)", "keychain").option("--purge", "With --to config: also delete the keychain entries once config holds the secrets").option("--dry-run", "List what would move without touching the keychain or config").action((keys, cmdOpts) => {
|
|
7821
|
+
const start = Date.now();
|
|
7822
|
+
try {
|
|
7823
|
+
const opts = program2.optsWithGlobals();
|
|
7824
|
+
const dryRun = !!(cmdOpts.dryRun || opts.dryRun);
|
|
7825
|
+
const configPath = getGlobalConfigPath(opts.config);
|
|
7826
|
+
if (cmdOpts.to !== "keychain" && cmdOpts.to !== "config") {
|
|
7827
|
+
throw new KeychainError(`--to must be "keychain" or "config", got "${cmdOpts.to}".`);
|
|
7828
|
+
}
|
|
7829
|
+
if (cmdOpts.purge && cmdOpts.to !== "config") {
|
|
7830
|
+
throw new KeychainError("--purge only applies to --to config.");
|
|
7831
|
+
}
|
|
7832
|
+
if (!fs4.existsSync(configPath)) throw new KeychainError(`No global config at ${configPath} \u2014 nothing to migrate.`);
|
|
7833
|
+
const raw = loadJsonFile(configPath) ?? {};
|
|
7834
|
+
const backend = getKeychainBackend();
|
|
7835
|
+
if (!dryRun && !backend.available()) {
|
|
7836
|
+
throw new KeychainError(`The ${backend.store} backend is not available on this machine.`);
|
|
7837
|
+
}
|
|
7838
|
+
const only = new Set(keys);
|
|
7839
|
+
const result = cmdOpts.to === "keychain" ? migrateSecrets(raw, backend, dryRun, only) : unmigrateSecrets(raw, backend, dryRun, only);
|
|
7840
|
+
let purged = [];
|
|
7841
|
+
if (!dryRun && result.migrated.length > 0) {
|
|
7842
|
+
if (cmdOpts.to === "keychain") {
|
|
7843
|
+
const backup = `${configPath}.pre-keychain.bak`;
|
|
7844
|
+
fs4.copyFileSync(configPath, backup);
|
|
7845
|
+
try {
|
|
7846
|
+
fs4.chmodSync(backup, 384);
|
|
7847
|
+
} catch {
|
|
7848
|
+
}
|
|
7849
|
+
warn(`Plaintext backup written to ${backup} \u2014 delete it once \`pncli config check\` passes.`);
|
|
7850
|
+
}
|
|
7851
|
+
writeGlobalConfig(raw, opts.config);
|
|
7852
|
+
if (cmdOpts.purge) purged = purgeEntries(raw, backend, result.migrated.map((m) => m.account));
|
|
7853
|
+
}
|
|
7854
|
+
resetKeychainCache();
|
|
7855
|
+
success({
|
|
7856
|
+
to: cmdOpts.to,
|
|
7857
|
+
dryRun,
|
|
7858
|
+
store: backend.store,
|
|
7859
|
+
...result,
|
|
7860
|
+
...cmdOpts.to === "config" ? { purged } : {}
|
|
7861
|
+
}, "config", "keychain-migrate", start);
|
|
7862
|
+
} catch (err) {
|
|
7863
|
+
fail(err, "config", "keychain-migrate", start);
|
|
7864
|
+
}
|
|
7865
|
+
});
|
|
6511
7866
|
}
|
|
6512
7867
|
|
|
6513
7868
|
// src/services/skills/commands.ts
|
|
6514
|
-
import
|
|
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(", ")}`);
|
|
@@ -7404,15 +9086,18 @@ function installAllPlugins(m, resolvedPath, targets) {
|
|
|
7404
9086
|
}
|
|
7405
9087
|
return targets.map((t) => installMarketplaceToTarget(m, resolvedPath, pluginChoices, "all", t, { updated: true, force: true, installedOnly: false }));
|
|
7406
9088
|
}
|
|
9089
|
+
function isBareMarketplaceSync(plugin, opts) {
|
|
9090
|
+
return plugin === void 0 && !opts.marketplace && !opts.force;
|
|
9091
|
+
}
|
|
7407
9092
|
function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
|
|
7408
9093
|
const marketplaceName = marketplaceLabel(m);
|
|
7409
9094
|
try {
|
|
7410
9095
|
const marketplacePath = m.localPath;
|
|
7411
|
-
if (!marketplacePath || !
|
|
9096
|
+
if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
|
|
7412
9097
|
warn(`Marketplace "${marketplaceName}" local path not found \u2014 skipping.`);
|
|
7413
9098
|
return { marketplace: marketplaceName, skipped: true, message: "Local path not found." };
|
|
7414
9099
|
}
|
|
7415
|
-
const { updated } = pullMarketplace(marketplacePath, m
|
|
9100
|
+
const { updated } = pullMarketplace(marketplacePath, m, marketplaceName);
|
|
7416
9101
|
const pluginChoices = resolvePluginChoices(marketplacePath);
|
|
7417
9102
|
if (pluginChoices.length === 0) {
|
|
7418
9103
|
warn(`No plugins found in marketplace "${marketplaceName}" \u2014 skipping.`);
|
|
@@ -7431,19 +9116,40 @@ function syncMarketplacePlugins(m, targets, pluginFilter, opts) {
|
|
|
7431
9116
|
return { marketplace: marketplaceName, skipped: true, error: message };
|
|
7432
9117
|
}
|
|
7433
9118
|
}
|
|
9119
|
+
function parseProviderOption(value) {
|
|
9120
|
+
if (value === void 0) return void 0;
|
|
9121
|
+
if (!PROVIDERS.includes(value)) {
|
|
9122
|
+
throw new Error(`--provider must be one of: ${PROVIDERS.join(", ")} (got "${value}").`);
|
|
9123
|
+
}
|
|
9124
|
+
return value;
|
|
9125
|
+
}
|
|
9126
|
+
function safeProviderConfig() {
|
|
9127
|
+
try {
|
|
9128
|
+
return loadConfig();
|
|
9129
|
+
} catch {
|
|
9130
|
+
return {};
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
7434
9133
|
function performMarketplaceAdd(url, localPath, opts) {
|
|
7435
|
-
const resolvedPath =
|
|
9134
|
+
const resolvedPath = path11.resolve(localPath ?? defaultMarketplacePath(url));
|
|
7436
9135
|
const marketplaceName = opts.name ?? repoNameFromUrl(url);
|
|
7437
|
-
|
|
9136
|
+
const provider = parseProviderOption(opts.provider);
|
|
9137
|
+
cloneOrReuseMarketplace(url, resolvedPath, { ...opts, provider }, marketplaceName);
|
|
7438
9138
|
const configPath = getGlobalConfigPath();
|
|
7439
9139
|
const { existing, all } = loadMarketplacesConfig(configPath);
|
|
9140
|
+
if (opts.keychain && !opts.token) warn("--keychain has no effect without --token.");
|
|
9141
|
+
const account = `marketplaces.${marketplaceName}.token`;
|
|
9142
|
+
const storeInKeychain = !!(opts.token && opts.keychain);
|
|
7440
9143
|
const entry = {
|
|
7441
9144
|
name: marketplaceName,
|
|
7442
9145
|
repoUrl: url,
|
|
7443
9146
|
localPath: resolvedPath,
|
|
7444
|
-
...opts.token ? { token: opts.token } : {}
|
|
9147
|
+
...opts.token ? { token: storeInKeychain ? keychainRef(account) : opts.token } : {},
|
|
9148
|
+
...opts.username ? { username: opts.username } : {},
|
|
9149
|
+
...provider ? { provider } : {}
|
|
7445
9150
|
};
|
|
7446
9151
|
upsertMarketplace(all, entry);
|
|
9152
|
+
if (storeInKeychain) getKeychainBackend().set(account, opts.token);
|
|
7447
9153
|
saveMarketplaces(configPath, existing, all);
|
|
7448
9154
|
const targets = resolveInstallTargets(opts);
|
|
7449
9155
|
const targetResults = installAllPlugins(entry, resolvedPath, targets);
|
|
@@ -7454,6 +9160,8 @@ function performMarketplaceAdd(url, localPath, opts) {
|
|
|
7454
9160
|
localPath: resolvedPath,
|
|
7455
9161
|
branch: opts.branch ?? null,
|
|
7456
9162
|
tokenConfigured: !!opts.token,
|
|
9163
|
+
tokenStorage: !opts.token ? null : opts.keychain ? "keychain" : "config",
|
|
9164
|
+
provider: detectProvider(url, safeProviderConfig(), provider),
|
|
7457
9165
|
// One target keeps the flat shape earlier versions emitted; several nest under `targets`.
|
|
7458
9166
|
...targetResults.length === 1 ? { plugins: targetResults[0].plugins, total: targetResults[0].total, target: targetResults[0].target } : { targets: targetResults, total: targetResults.reduce((sum, r) => sum + r.total, 0) },
|
|
7459
9167
|
...instructions ? { instructions } : {}
|
|
@@ -7468,12 +9176,22 @@ function performMarketplaceRemove(name) {
|
|
|
7468
9176
|
}
|
|
7469
9177
|
const removed = all.splice(idx, 1)[0];
|
|
7470
9178
|
saveMarketplaces(configPath, existing, all);
|
|
9179
|
+
let keychainEntryDeleted = false;
|
|
9180
|
+
if (isKeychainRef(removed.token)) {
|
|
9181
|
+
try {
|
|
9182
|
+
const account = removed.token.slice("keychain:".length);
|
|
9183
|
+
keychainEntryDeleted = purgeEntries(loadJsonFile(configPath) ?? {}, getKeychainBackend(), [account]).length > 0;
|
|
9184
|
+
} catch (err) {
|
|
9185
|
+
warn(`Could not delete the marketplace's keychain entry: ${err instanceof Error ? err.message : String(err)}`);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
7471
9188
|
return {
|
|
7472
9189
|
removed: {
|
|
7473
9190
|
name: marketplaceLabel(removed),
|
|
7474
9191
|
repoUrl: removed.repoUrl,
|
|
7475
9192
|
localPath: removed.localPath
|
|
7476
9193
|
},
|
|
9194
|
+
keychainEntryDeleted,
|
|
7477
9195
|
remaining: all.length
|
|
7478
9196
|
};
|
|
7479
9197
|
}
|
|
@@ -7490,8 +9208,8 @@ function listBundledSkillDirs() {
|
|
|
7490
9208
|
let skillDirs = [];
|
|
7491
9209
|
if (bundledDir !== "") {
|
|
7492
9210
|
try {
|
|
7493
|
-
skillDirs =
|
|
7494
|
-
(name) =>
|
|
9211
|
+
skillDirs = fs6.readdirSync(bundledDir).filter(
|
|
9212
|
+
(name) => fs6.existsSync(path11.join(bundledDir, name, "SKILL.md"))
|
|
7495
9213
|
);
|
|
7496
9214
|
} catch {
|
|
7497
9215
|
}
|
|
@@ -7502,30 +9220,30 @@ function listBundledSkillDirs() {
|
|
|
7502
9220
|
return skillDirs;
|
|
7503
9221
|
}
|
|
7504
9222
|
function installBundledSkillsTo(targetDir, skillDirs) {
|
|
7505
|
-
const resolvedTarget =
|
|
9223
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
7506
9224
|
const bundledDir = getBundledSkillsDir();
|
|
7507
9225
|
const installed = [];
|
|
7508
9226
|
const failed = [];
|
|
7509
9227
|
for (const skillName of skillDirs) {
|
|
7510
|
-
const existingDir =
|
|
7511
|
-
if (!existingDir.startsWith(resolvedTarget +
|
|
7512
|
-
if (
|
|
7513
|
-
|
|
9228
|
+
const existingDir = path11.resolve(targetDir, skillName);
|
|
9229
|
+
if (!existingDir.startsWith(resolvedTarget + path11.sep)) continue;
|
|
9230
|
+
if (fs6.existsSync(existingDir)) {
|
|
9231
|
+
fs6.rmSync(existingDir, { recursive: true, force: true });
|
|
7514
9232
|
}
|
|
7515
9233
|
}
|
|
7516
9234
|
log(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
|
|
7517
9235
|
for (const skillName of skillDirs) {
|
|
7518
|
-
const skillDir =
|
|
7519
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
9236
|
+
const skillDir = path11.resolve(targetDir, skillName);
|
|
9237
|
+
if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
|
|
7520
9238
|
failed.push(skillName);
|
|
7521
9239
|
continue;
|
|
7522
9240
|
}
|
|
7523
9241
|
try {
|
|
7524
|
-
const mdFiles =
|
|
7525
|
-
|
|
9242
|
+
const mdFiles = fs6.readdirSync(path11.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
|
|
9243
|
+
fs6.mkdirSync(skillDir, { recursive: true });
|
|
7526
9244
|
for (const mdFile of mdFiles) {
|
|
7527
|
-
const content =
|
|
7528
|
-
|
|
9245
|
+
const content = fs6.readFileSync(path11.join(bundledDir, skillName, mdFile), "utf8");
|
|
9246
|
+
fs6.writeFileSync(path11.join(skillDir, mdFile), content, "utf8");
|
|
7529
9247
|
}
|
|
7530
9248
|
installed.push(skillName);
|
|
7531
9249
|
} catch {
|
|
@@ -7560,6 +9278,12 @@ Org plugins (skills and AGENTS.md/CLAUDE.md from a git-hosted marketplace):
|
|
|
7560
9278
|
|
|
7561
9279
|
Check what is installed:
|
|
7562
9280
|
pncli skills status | pncli skills locations | pncli doctor
|
|
9281
|
+
|
|
9282
|
+
Private marketplaces and credentials:
|
|
9283
|
+
pncli skills git-auth enable # let git (and agent hosts) authenticate to marketplace hosts
|
|
9284
|
+
pncli config keychain migrate # move tokens out of plaintext config into the OS keychain
|
|
9285
|
+
|
|
9286
|
+
New to this? pncli skills guide # how skills management fits together (or: guide <section>)
|
|
7563
9287
|
`);
|
|
7564
9288
|
skills.command("install").description("Install the bundled pncli skills (project scope by default; --scope user for every repo)").option("--agent <agent>", `Target agent host: ${AGENT_CHOICES}`, DEFAULT_AGENT).option("--scope <scope>", "Installation scope: project | user", "project").option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts, cmd) => {
|
|
7565
9289
|
const start = Date.now();
|
|
@@ -7584,7 +9308,7 @@ Check what is installed:
|
|
|
7584
9308
|
return;
|
|
7585
9309
|
}
|
|
7586
9310
|
const agentName = resolveAgentName(opts);
|
|
7587
|
-
const targetDir = opts.target ?
|
|
9311
|
+
const targetDir = opts.target ? path11.resolve(opts.target) : resolveScopedPath(resolveAgentPaths(agentName), scope);
|
|
7588
9312
|
const { installed, failed } = installBundledSkillsTo(targetDir, skillDirs);
|
|
7589
9313
|
let tracked = false;
|
|
7590
9314
|
if (opts.target && installed.length > 0) {
|
|
@@ -7613,12 +9337,12 @@ Check what is installed:
|
|
|
7613
9337
|
try {
|
|
7614
9338
|
let targetDir;
|
|
7615
9339
|
if (opts.target) {
|
|
7616
|
-
targetDir =
|
|
9340
|
+
targetDir = path11.resolve(opts.target);
|
|
7617
9341
|
} else {
|
|
7618
9342
|
const agentConfig = resolveAgentPaths(resolveAgentName(opts));
|
|
7619
9343
|
targetDir = resolveScopedPath(agentConfig, opts.scope === "user" ? "user" : "project");
|
|
7620
9344
|
}
|
|
7621
|
-
if (!
|
|
9345
|
+
if (!fs6.existsSync(targetDir)) {
|
|
7622
9346
|
success({
|
|
7623
9347
|
skills: [],
|
|
7624
9348
|
total: 0,
|
|
@@ -7629,13 +9353,13 @@ Check what is installed:
|
|
|
7629
9353
|
return;
|
|
7630
9354
|
}
|
|
7631
9355
|
const meta = readInstalledMeta(targetDir);
|
|
7632
|
-
const skillDirs =
|
|
9356
|
+
const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
|
|
7633
9357
|
if (name.startsWith(".")) return false;
|
|
7634
|
-
const skillPath =
|
|
7635
|
-
return
|
|
9358
|
+
const skillPath = path11.join(targetDir, name, "SKILL.md");
|
|
9359
|
+
return fs6.existsSync(skillPath);
|
|
7636
9360
|
});
|
|
7637
9361
|
const skillsList = skillDirs.map((name) => {
|
|
7638
|
-
const content =
|
|
9362
|
+
const content = fs6.readFileSync(path11.join(targetDir, name, "SKILL.md"), "utf8");
|
|
7639
9363
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7640
9364
|
const data = {};
|
|
7641
9365
|
const metadata = {};
|
|
@@ -7688,24 +9412,24 @@ Check what is installed:
|
|
|
7688
9412
|
try {
|
|
7689
9413
|
let targetDir;
|
|
7690
9414
|
if (opts.target) {
|
|
7691
|
-
targetDir =
|
|
9415
|
+
targetDir = path11.resolve(opts.target);
|
|
7692
9416
|
} else {
|
|
7693
9417
|
const agentConfig = resolveAgentPaths(resolveAgentName(opts));
|
|
7694
9418
|
targetDir = resolveScopedPath(agentConfig, opts.scope === "project" ? "project" : "user");
|
|
7695
9419
|
}
|
|
7696
|
-
const resolvedTarget =
|
|
7697
|
-
const skillDir =
|
|
7698
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
9420
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
9421
|
+
const skillDir = path11.resolve(targetDir, name);
|
|
9422
|
+
if (!skillDir.startsWith(resolvedTarget + path11.sep)) {
|
|
7699
9423
|
throw new Error(`Invalid skill name: "${name}"`);
|
|
7700
9424
|
}
|
|
7701
|
-
if (!
|
|
9425
|
+
if (!fs6.existsSync(skillDir)) {
|
|
7702
9426
|
throw new Error(`Skill "${name}" not found at ${skillDir}`);
|
|
7703
9427
|
}
|
|
7704
9428
|
const meta = readInstalledMeta(targetDir);
|
|
7705
9429
|
const record = meta.skills[name];
|
|
7706
|
-
|
|
9430
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
7707
9431
|
delete meta.skills[name];
|
|
7708
|
-
|
|
9432
|
+
fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
|
|
7709
9433
|
success({
|
|
7710
9434
|
uninstalled: name,
|
|
7711
9435
|
target: targetDir,
|
|
@@ -7776,7 +9500,7 @@ Check what is installed:
|
|
|
7776
9500
|
const start = Date.now();
|
|
7777
9501
|
try {
|
|
7778
9502
|
const configPath = getGlobalConfigPath();
|
|
7779
|
-
const resolved =
|
|
9503
|
+
const resolved = path11.resolve(dir);
|
|
7780
9504
|
const forgotten = forgetCustomTarget(configPath, resolved);
|
|
7781
9505
|
if (!forgotten) {
|
|
7782
9506
|
warn(`${resolved} is not a tracked custom target \u2014 nothing to forget.`);
|
|
@@ -7790,6 +9514,44 @@ Check what is installed:
|
|
|
7790
9514
|
fail(err, "skills", "forget-target", start);
|
|
7791
9515
|
}
|
|
7792
9516
|
});
|
|
9517
|
+
skills.command("guide").description("Explain how skills management works: bundled skill vs marketplace plugins, agent hosts, scopes, sync, and auth").argument("[section]", "Print only one section (e.g. hosts, sync, auth, troubleshooting)").option("--sections", "List the section names as JSON instead of printing the guide").action((section, cmdOpts) => {
|
|
9518
|
+
const start = Date.now();
|
|
9519
|
+
try {
|
|
9520
|
+
const guidePath = findGuidePath();
|
|
9521
|
+
if (!guidePath) throw new Error(`${GUIDE_FILE} is missing from this pncli install \u2014 reinstall pncli, or read it online at https://kolatts.github.io/pncli/skills-guide/`);
|
|
9522
|
+
const markdown = fs6.readFileSync(guidePath, "utf8").replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
9523
|
+
if (cmdOpts.sections) {
|
|
9524
|
+
success({ sections: listGuideSections(markdown), path: guidePath }, "skills", "guide", start);
|
|
9525
|
+
return;
|
|
9526
|
+
}
|
|
9527
|
+
if (section) {
|
|
9528
|
+
const text = extractGuideSection(markdown, section);
|
|
9529
|
+
if (!text) {
|
|
9530
|
+
throw new Error(`No guide section matches "${section}". Sections: ${listGuideSections(markdown).map((s) => s.slug).join(", ")}`);
|
|
9531
|
+
}
|
|
9532
|
+
writeRawOutput(text);
|
|
9533
|
+
return;
|
|
9534
|
+
}
|
|
9535
|
+
writeRawOutput(markdown);
|
|
9536
|
+
} catch (err) {
|
|
9537
|
+
fail(err, "skills", "guide", start);
|
|
9538
|
+
}
|
|
9539
|
+
});
|
|
9540
|
+
registerGitAuthCommands(skills);
|
|
9541
|
+
skills.command("git-credential", { hidden: true }).description("Git credential helper (used by git after `skills git-auth enable`; not for direct use)").option("--marketplace <name>", "Answer with this marketplace's credential (written by git-auth enable)").argument("<operation>", "get | store | erase (supplied by git)").action(async (operation, cmdOpts) => {
|
|
9542
|
+
if (operation !== "get") return;
|
|
9543
|
+
try {
|
|
9544
|
+
const chunks = [];
|
|
9545
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
9546
|
+
const request = parseCredentialRequest(Buffer.concat(chunks).toString("utf8"));
|
|
9547
|
+
const configPath = skills.optsWithGlobals().config;
|
|
9548
|
+
const globalConfig = loadJsonFile(getGlobalConfigPath(configPath)) ?? {};
|
|
9549
|
+
const cfg = loadConfig({ configPath, keychainPaths: ["github.token", "bitbucket.pat", "ado.pat"] });
|
|
9550
|
+
const answer = resolveCredential(request, globalConfig, cfg, void 0, cmdOpts.marketplace);
|
|
9551
|
+
if (answer) process.stdout.write(formatCredentialAnswer(answer));
|
|
9552
|
+
} catch {
|
|
9553
|
+
}
|
|
9554
|
+
});
|
|
7793
9555
|
const marketplace = skills.command("marketplace").description("Org plugins from git-hosted marketplaces: add, sync, enable/disable, and shipped AGENTS.md/CLAUDE.md");
|
|
7794
9556
|
marketplace.addHelpText("after", `
|
|
7795
9557
|
Workflow:
|
|
@@ -7807,7 +9569,7 @@ Shipped instructions:
|
|
|
7807
9569
|
|
|
7808
9570
|
Plugin skills always install at user scope. --agent picks the host (default: ${DEFAULT_AGENT}); --all-agents covers all of them.
|
|
7809
9571
|
`);
|
|
7810
|
-
marketplace.command("add").alias("setup").description("Register a new marketplace, clone it, install all its plugins, and apply its shipped AGENTS.md / CLAUDE.md (`setup` is an alias)").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action((url, localPath, opts) => {
|
|
9572
|
+
marketplace.command("add").alias("setup").description("Register a new marketplace, clone it, install all its plugins, and apply its shipped AGENTS.md / CLAUDE.md (`setup` is an alias)").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--name <name>", "Human-readable name for this marketplace (default: derived from URL)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").option("--keychain", "Store --token in the OS keychain instead of plaintext config (see: pncli config keychain --help)").option("--username <username>", "Username sent with the token (default: x-access-token on github.com, x-token-auth elsewhere; Bitbucket Data Center personal tokens usually need your username)").option("--provider <provider>", "Git host type, when detection guesses wrong: github | bitbucket | ado | git").option("--agent <agent>", `Target agent host for plugin install: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action((url, localPath, opts) => {
|
|
7811
9573
|
const marketplaceIdx = process.argv.indexOf("marketplace");
|
|
7812
9574
|
const invokedAs = marketplaceIdx !== -1 && process.argv[marketplaceIdx + 1] === "setup" ? "marketplace-setup" : "marketplace-add";
|
|
7813
9575
|
return marketplaceAddAction(url, localPath, opts, invokedAs);
|
|
@@ -7817,13 +9579,17 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7817
9579
|
try {
|
|
7818
9580
|
const configPath = getGlobalConfigPath();
|
|
7819
9581
|
const all = loadMarketplaces(configPath);
|
|
9582
|
+
const providerCfg = safeProviderConfig();
|
|
7820
9583
|
success({
|
|
7821
9584
|
marketplaces: all.map((m) => ({
|
|
7822
9585
|
name: marketplaceLabel(m),
|
|
7823
9586
|
repoUrl: m.repoUrl,
|
|
7824
9587
|
localPath: m.localPath,
|
|
7825
9588
|
tokenConfigured: !!m.token,
|
|
7826
|
-
|
|
9589
|
+
tokenStorage: !m.token ? null : isKeychainRef(m.token) ? "keychain" : "config",
|
|
9590
|
+
provider: detectProvider(m.repoUrl, providerCfg, m.provider),
|
|
9591
|
+
username: m.username ?? null,
|
|
9592
|
+
upstreamRemote: m.localPath && fs6.existsSync(m.localPath) ? getRepoRemoteUrl(m.localPath) : null
|
|
7827
9593
|
})),
|
|
7828
9594
|
total: all.length
|
|
7829
9595
|
}, "skills", "marketplace-list", start);
|
|
@@ -7831,6 +9597,54 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7831
9597
|
fail(err, "skills", "marketplace-list", start);
|
|
7832
9598
|
}
|
|
7833
9599
|
});
|
|
9600
|
+
marketplace.command("update").description("Change a registered marketplace's token, username, or provider without re-adding it (rotate a token here)").argument("<name>", "Marketplace name (or repo URL)").option("--token <token>", "New access token for clone and pull").option("--keychain", "Store --token in the OS keychain instead of plaintext config").option("--username <username>", "Username sent with the token (Bitbucket Data Center personal tokens usually need your username)").option("--provider <provider>", "Git host type: github | bitbucket | ado | git").option("--clear-token", "Remove the marketplace's own token (falls back to the provider token pncli has, if any)").option("--clear-username", "Go back to the default username").option("--clear-provider", "Go back to automatic provider detection").action((name, opts) => {
|
|
9601
|
+
const start = Date.now();
|
|
9602
|
+
try {
|
|
9603
|
+
if (opts.token && opts.clearToken) throw new Error("Pass --token or --clear-token, not both.");
|
|
9604
|
+
if (opts.keychain && !opts.token) throw new Error("--keychain needs --token.");
|
|
9605
|
+
const provider = parseProviderOption(opts.provider);
|
|
9606
|
+
const configPath = getGlobalConfigPath();
|
|
9607
|
+
const { existing, all } = loadMarketplacesConfig(configPath);
|
|
9608
|
+
const idx = all.findIndex((m) => m.name === name || m.repoUrl === name);
|
|
9609
|
+
if (idx === -1) throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
|
|
9610
|
+
const entry = { ...all[idx] };
|
|
9611
|
+
const oldToken = entry.token;
|
|
9612
|
+
const account = `marketplaces.${marketplaceLabel(entry)}.token`;
|
|
9613
|
+
if (opts.token) {
|
|
9614
|
+
if (opts.keychain) {
|
|
9615
|
+
getKeychainBackend().set(account, opts.token);
|
|
9616
|
+
entry.token = keychainRef(account);
|
|
9617
|
+
} else {
|
|
9618
|
+
entry.token = opts.token;
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
if (opts.clearToken) delete entry.token;
|
|
9622
|
+
if (opts.username) entry.username = opts.username;
|
|
9623
|
+
if (opts.clearUsername) delete entry.username;
|
|
9624
|
+
if (provider) entry.provider = provider;
|
|
9625
|
+
if (opts.clearProvider) delete entry.provider;
|
|
9626
|
+
all[idx] = entry;
|
|
9627
|
+
saveMarketplaces(configPath, existing, all);
|
|
9628
|
+
if (isKeychainRef(oldToken) && oldToken !== entry.token) {
|
|
9629
|
+
try {
|
|
9630
|
+
purgeEntries(loadJsonFile(configPath) ?? {}, getKeychainBackend(), [oldToken.slice("keychain:".length)]);
|
|
9631
|
+
} catch (err) {
|
|
9632
|
+
warn(`Could not delete the old keychain entry: ${err instanceof Error ? err.message : String(err)}`);
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
success({
|
|
9636
|
+
name: marketplaceLabel(entry),
|
|
9637
|
+
repoUrl: entry.repoUrl,
|
|
9638
|
+
tokenConfigured: !!entry.token,
|
|
9639
|
+
tokenStorage: !entry.token ? null : isKeychainRef(entry.token) ? "keychain" : "config",
|
|
9640
|
+
username: entry.username ?? null,
|
|
9641
|
+
provider: detectProvider(entry.repoUrl, safeProviderConfig(), entry.provider),
|
|
9642
|
+
next: opts.username || opts.provider || opts.clearUsername || opts.clearProvider ? `If git-auth is enabled for it, refresh that too: pncli skills git-auth enable --marketplace ${marketplaceLabel(entry)}. Then verify with: pncli doctor` : "Verify with: pncli doctor"
|
|
9643
|
+
}, "skills", "marketplace-update", start);
|
|
9644
|
+
} catch (err) {
|
|
9645
|
+
fail(err, "skills", "marketplace-update", start);
|
|
9646
|
+
}
|
|
9647
|
+
});
|
|
7834
9648
|
marketplace.command("plugins").description("List the plugins available in a registered marketplace").argument("<name>", "Marketplace name (or repo URL) to inspect").action((name) => {
|
|
7835
9649
|
const start = Date.now();
|
|
7836
9650
|
try {
|
|
@@ -7840,7 +9654,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7840
9654
|
if (!found) {
|
|
7841
9655
|
throw new Error(`Marketplace "${name}" not found. Run: pncli skills marketplace list`);
|
|
7842
9656
|
}
|
|
7843
|
-
if (!found.localPath || !
|
|
9657
|
+
if (!found.localPath || !fs6.existsSync(found.localPath)) {
|
|
7844
9658
|
throw new Error(`Marketplace "${marketplaceLabel(found)}" local path not found at ${found.localPath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
|
|
7845
9659
|
}
|
|
7846
9660
|
const plugins = resolvePluginChoices(found.localPath);
|
|
@@ -7861,61 +9675,62 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7861
9675
|
fail(err, "skills", "marketplace-remove", start);
|
|
7862
9676
|
}
|
|
7863
9677
|
});
|
|
7864
|
-
marketplace.command("sync").description("
|
|
9678
|
+
marketplace.command("sync").description("With no arguments, refreshes every already-installed plugin from every marketplace (same as --marketplace all --installed-only). Pass --force, a plugin, or --marketplace to get the interactive picker instead.").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--marketplace <name>", 'Marketplace name to sync, or "all" to sync every registered marketplace (skips interactive selection)').option("--agent <agent>", `Target agent host: ${AGENT_CHOICES} (default: ${DEFAULT_AGENT})`).option("--claude", "Shorthand for --agent claude-code").option("--all-agents", "Install to every supported agent host in one run").option("--force", 'Reinstall even if a marketplace has no new changes (applies to single-plugin and "all" installs alike)').option("--installed-only", "Only sync plugins that are already installed \u2014 skip plugins newly added to the marketplace").option("--no-instructions", "Do not apply the shipped AGENTS.md / CLAUDE.md to user-level instructions files").action(async (plugin, opts) => {
|
|
7865
9679
|
const start = Date.now();
|
|
7866
9680
|
try {
|
|
7867
9681
|
const configPath = getGlobalConfigPath();
|
|
7868
|
-
const
|
|
7869
|
-
if (
|
|
9682
|
+
const allMarketplaces2 = loadMarketplaces(configPath);
|
|
9683
|
+
if (allMarketplaces2.length === 0) {
|
|
7870
9684
|
throw new Error("No marketplaces configured. Run: pncli skills marketplace add <url>");
|
|
7871
9685
|
}
|
|
9686
|
+
const bareSync = isBareMarketplaceSync(plugin, opts);
|
|
7872
9687
|
const targets = resolveInstallTargets(opts);
|
|
7873
|
-
const syncOpts = { force: opts.force ?? false, installedOnly: opts.installedOnly ?? false, instructions: opts.instructions !== false };
|
|
9688
|
+
const syncOpts = { force: opts.force ?? false, installedOnly: bareSync ? true : opts.installedOnly ?? false, instructions: opts.instructions !== false };
|
|
7874
9689
|
const targetSummary = targets.length === 1 ? { target: targets[0].target } : { targets: targets.map((t) => t.target) };
|
|
7875
|
-
if (opts.marketplace === "all") {
|
|
7876
|
-
const results =
|
|
9690
|
+
if (opts.marketplace === "all" || bareSync) {
|
|
9691
|
+
const results = allMarketplaces2.map((m) => syncMarketplacePlugins(m, targets, plugin ?? "all", syncOpts));
|
|
7877
9692
|
success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
|
|
7878
9693
|
return;
|
|
7879
9694
|
}
|
|
7880
9695
|
let selectedMarketplace;
|
|
7881
9696
|
let selectedPlugin = plugin;
|
|
7882
|
-
const canGoBack = !opts.marketplace &&
|
|
9697
|
+
const canGoBack = !opts.marketplace && allMarketplaces2.length > 1;
|
|
7883
9698
|
if (opts.marketplace) {
|
|
7884
|
-
const found =
|
|
9699
|
+
const found = allMarketplaces2.find((m) => m.name === opts.marketplace || m.repoUrl === opts.marketplace);
|
|
7885
9700
|
if (!found) {
|
|
7886
9701
|
throw new Error(`Marketplace "${opts.marketplace}" not found. Run: pncli skills marketplace list`);
|
|
7887
9702
|
}
|
|
7888
9703
|
selectedMarketplace = found;
|
|
7889
|
-
} else if (
|
|
7890
|
-
selectedMarketplace =
|
|
9704
|
+
} else if (allMarketplaces2.length === 1) {
|
|
9705
|
+
selectedMarketplace = allMarketplaces2[0];
|
|
7891
9706
|
}
|
|
7892
9707
|
for (; ; ) {
|
|
7893
9708
|
if (!selectedMarketplace) {
|
|
7894
|
-
assertInteractive(`Multiple marketplaces are registered (${
|
|
9709
|
+
assertInteractive(`Multiple marketplaces are registered (${allMarketplaces2.map(marketplaceLabel).join(", ")}). Pass --marketplace <name> (or --marketplace all) to run this non-interactively.`);
|
|
7895
9710
|
const chosen = await select({
|
|
7896
9711
|
message: "Select a marketplace to sync:",
|
|
7897
9712
|
choices: [
|
|
7898
9713
|
{ value: ALL_MARKETPLACES, name: "All marketplaces \u2014 sync every plugin from every marketplace" },
|
|
7899
9714
|
// Index-based values: labels alone aren't guaranteed unique (two marketplaces can share a derived name).
|
|
7900
|
-
...
|
|
9715
|
+
...allMarketplaces2.map((m2, i) => ({ value: String(i), name: `${marketplaceLabel(m2)} \u2014 ${m2.repoUrl ?? ""}` }))
|
|
7901
9716
|
]
|
|
7902
9717
|
});
|
|
7903
9718
|
if (chosen === ALL_MARKETPLACES) {
|
|
7904
|
-
const results =
|
|
9719
|
+
const results = allMarketplaces2.map((m2) => syncMarketplacePlugins(m2, targets, selectedPlugin ?? "all", syncOpts));
|
|
7905
9720
|
success({ allMarketplaces: true, marketplaces: results, ...targetSummary }, "skills", "marketplace-sync", start);
|
|
7906
9721
|
return;
|
|
7907
9722
|
}
|
|
7908
|
-
selectedMarketplace =
|
|
9723
|
+
selectedMarketplace = allMarketplaces2[Number(chosen)];
|
|
7909
9724
|
if (!selectedMarketplace) {
|
|
7910
9725
|
throw new Error(`Marketplace "${chosen}" not found.`);
|
|
7911
9726
|
}
|
|
7912
9727
|
}
|
|
7913
9728
|
const marketplaceName = marketplaceLabel(selectedMarketplace);
|
|
7914
9729
|
const marketplacePath = selectedMarketplace.localPath;
|
|
7915
|
-
if (!marketplacePath || !
|
|
9730
|
+
if (!marketplacePath || !fs6.existsSync(marketplacePath)) {
|
|
7916
9731
|
throw new Error(`Marketplace "${marketplaceName}" local path not found at ${marketplacePath ?? "(not set)"}. Run: pncli skills marketplace add <url>`);
|
|
7917
9732
|
}
|
|
7918
|
-
const { updated } = pullMarketplace(marketplacePath, selectedMarketplace
|
|
9733
|
+
const { updated } = pullMarketplace(marketplacePath, selectedMarketplace, marketplaceName);
|
|
7919
9734
|
const pluginChoices = resolvePluginChoices(marketplacePath);
|
|
7920
9735
|
if (pluginChoices.length === 0) {
|
|
7921
9736
|
throw new Error(`No plugins found in marketplace "${marketplaceName}". Check the marketplace repository structure.`);
|
|
@@ -7969,7 +9784,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7969
9784
|
const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
|
|
7970
9785
|
const agents = Object.keys(AGENT_PATHS);
|
|
7971
9786
|
const marketplaces = selected.map((m) => {
|
|
7972
|
-
const shipped = m.localPath &&
|
|
9787
|
+
const shipped = m.localPath && fs6.existsSync(m.localPath) ? findMarketplaceInstructions(m.localPath).map((s) => s.file) : [];
|
|
7973
9788
|
return {
|
|
7974
9789
|
marketplace: marketplaceLabel(m),
|
|
7975
9790
|
localPath: m.localPath ?? null,
|
|
@@ -7995,7 +9810,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
7995
9810
|
const selected = opts.marketplace ? [findMarketplaceOrThrow(all, opts.marketplace)] : all;
|
|
7996
9811
|
const results = selected.map((m) => {
|
|
7997
9812
|
const name = marketplaceLabel(m);
|
|
7998
|
-
if (!m.localPath || !
|
|
9813
|
+
if (!m.localPath || !fs6.existsSync(m.localPath)) {
|
|
7999
9814
|
return { marketplace: name, skipped: true, message: "Local path not found. Run: pncli skills marketplace add <url>" };
|
|
8000
9815
|
}
|
|
8001
9816
|
if (findMarketplaceInstructions(m.localPath).length === 0) {
|
|
@@ -8033,31 +9848,31 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8033
9848
|
try {
|
|
8034
9849
|
let targetDir;
|
|
8035
9850
|
if (opts.target) {
|
|
8036
|
-
targetDir =
|
|
9851
|
+
targetDir = path11.resolve(opts.target);
|
|
8037
9852
|
} else {
|
|
8038
9853
|
const agentConfig = resolveAgentPaths(resolveAgentName(opts));
|
|
8039
9854
|
targetDir = resolveScopedPath(agentConfig, opts.scope ?? "user");
|
|
8040
9855
|
}
|
|
8041
|
-
const resolvedTarget =
|
|
8042
|
-
if (!
|
|
9856
|
+
const resolvedTarget = path11.resolve(targetDir);
|
|
9857
|
+
if (!fs6.existsSync(targetDir)) {
|
|
8043
9858
|
success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
|
|
8044
9859
|
return;
|
|
8045
9860
|
}
|
|
8046
9861
|
const meta = readInstalledMeta(targetDir);
|
|
8047
9862
|
const removed = [];
|
|
8048
9863
|
const skipped = [];
|
|
8049
|
-
const skillDirs =
|
|
9864
|
+
const skillDirs = fs6.readdirSync(targetDir).filter((name) => {
|
|
8050
9865
|
if (name.startsWith(".")) return false;
|
|
8051
|
-
const p =
|
|
9866
|
+
const p = path11.join(targetDir, name);
|
|
8052
9867
|
try {
|
|
8053
|
-
return
|
|
9868
|
+
return fs6.statSync(p).isDirectory();
|
|
8054
9869
|
} catch {
|
|
8055
9870
|
return false;
|
|
8056
9871
|
}
|
|
8057
9872
|
});
|
|
8058
9873
|
for (const skillName of skillDirs) {
|
|
8059
|
-
const skillDir =
|
|
8060
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
9874
|
+
const skillDir = path11.resolve(targetDir, skillName);
|
|
9875
|
+
if (!skillDir.startsWith(resolvedTarget + path11.sep)) continue;
|
|
8061
9876
|
let record = meta.skills[skillName] ?? null;
|
|
8062
9877
|
if (!record) {
|
|
8063
9878
|
const perSkill = readSkillOrigin(skillDir);
|
|
@@ -8082,12 +9897,12 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8082
9897
|
skipped.push(skillName);
|
|
8083
9898
|
continue;
|
|
8084
9899
|
}
|
|
8085
|
-
|
|
9900
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
8086
9901
|
delete meta.skills[skillName];
|
|
8087
9902
|
removed.push(skillName);
|
|
8088
9903
|
}
|
|
8089
9904
|
if (removed.length > 0) {
|
|
8090
|
-
|
|
9905
|
+
fs6.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
|
|
8091
9906
|
}
|
|
8092
9907
|
success({
|
|
8093
9908
|
removed,
|
|
@@ -8105,7 +9920,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8105
9920
|
const start = Date.now();
|
|
8106
9921
|
try {
|
|
8107
9922
|
const targetDir = resolveTargetDir(opts);
|
|
8108
|
-
if (!
|
|
9923
|
+
if (!fs6.existsSync(targetDir)) {
|
|
8109
9924
|
success({ disabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-disable", start);
|
|
8110
9925
|
return;
|
|
8111
9926
|
}
|
|
@@ -8117,7 +9932,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8117
9932
|
total: result.disabled.length,
|
|
8118
9933
|
target: targetDir,
|
|
8119
9934
|
plugin,
|
|
8120
|
-
stash:
|
|
9935
|
+
stash: path11.join(targetDir, DISABLED_SUBDIR),
|
|
8121
9936
|
...opts.marketplace ? { marketplace: opts.marketplace } : {}
|
|
8122
9937
|
}, "skills", "marketplace-disable", start);
|
|
8123
9938
|
} catch (err) {
|
|
@@ -8128,7 +9943,7 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8128
9943
|
const start = Date.now();
|
|
8129
9944
|
try {
|
|
8130
9945
|
const targetDir = resolveTargetDir(opts);
|
|
8131
|
-
if (!
|
|
9946
|
+
if (!fs6.existsSync(targetDir)) {
|
|
8132
9947
|
success({ enabled: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-enable", start);
|
|
8133
9948
|
return;
|
|
8134
9949
|
}
|
|
@@ -8167,9 +9982,9 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8167
9982
|
const appliedInstructions = /* @__PURE__ */ new Map();
|
|
8168
9983
|
const instructionAgents = opts.target ? [] : [resolveAgentName(opts)];
|
|
8169
9984
|
for (; ; ) {
|
|
8170
|
-
const states =
|
|
9985
|
+
const states = fs6.existsSync(targetDir) ? listPluginStates(targetDir) : [];
|
|
8171
9986
|
const registered = loadMarketplaces(getGlobalConfigPath());
|
|
8172
|
-
const shipping = registered.filter((m) => m.localPath &&
|
|
9987
|
+
const shipping = registered.filter((m) => m.localPath && fs6.existsSync(m.localPath) && findMarketplaceInstructions(m.localPath).length > 0);
|
|
8173
9988
|
const action = await select({
|
|
8174
9989
|
message: `Manage skills marketplaces (target: ${targetDir}):`,
|
|
8175
9990
|
choices: [
|
|
@@ -8283,15 +10098,15 @@ Plugin skills always install at user scope. --agent picks the host (default: ${D
|
|
|
8283
10098
|
const start = Date.now();
|
|
8284
10099
|
try {
|
|
8285
10100
|
const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
|
|
8286
|
-
if (!
|
|
10101
|
+
if (!fs6.existsSync(userDir)) {
|
|
8287
10102
|
success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
|
|
8288
10103
|
return;
|
|
8289
10104
|
}
|
|
8290
|
-
const skillDirs =
|
|
10105
|
+
const skillDirs = fs6.readdirSync(userDir).filter((name) => {
|
|
8291
10106
|
if (name.startsWith(".")) return false;
|
|
8292
|
-
const p =
|
|
10107
|
+
const p = path11.join(userDir, name);
|
|
8293
10108
|
try {
|
|
8294
|
-
return
|
|
10109
|
+
return fs6.statSync(p).isDirectory();
|
|
8295
10110
|
} catch {
|
|
8296
10111
|
return false;
|
|
8297
10112
|
}
|
|
@@ -8307,13 +10122,13 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
|
|
|
8307
10122
|
);
|
|
8308
10123
|
}
|
|
8309
10124
|
for (const name of skillDirs) {
|
|
8310
|
-
const skillDir =
|
|
8311
|
-
if (!skillDir.startsWith(
|
|
8312
|
-
|
|
10125
|
+
const skillDir = path11.resolve(userDir, name);
|
|
10126
|
+
if (!skillDir.startsWith(path11.resolve(userDir) + path11.sep)) continue;
|
|
10127
|
+
fs6.rmSync(skillDir, { recursive: true, force: true });
|
|
8313
10128
|
}
|
|
8314
10129
|
const metaPath = getInstalledMetaPath(userDir);
|
|
8315
|
-
if (
|
|
8316
|
-
|
|
10130
|
+
if (fs6.existsSync(metaPath)) {
|
|
10131
|
+
fs6.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
|
|
8317
10132
|
}
|
|
8318
10133
|
success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
|
|
8319
10134
|
} catch (err) {
|
|
@@ -8322,46 +10137,46 @@ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\`
|
|
|
8322
10137
|
});
|
|
8323
10138
|
}
|
|
8324
10139
|
function resolvePluginChoices(marketplacePath) {
|
|
8325
|
-
const marketplaceJsonPath =
|
|
8326
|
-
if (
|
|
10140
|
+
const marketplaceJsonPath = path11.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
10141
|
+
if (fs6.existsSync(marketplaceJsonPath)) {
|
|
8327
10142
|
try {
|
|
8328
|
-
const meta = JSON.parse(
|
|
10143
|
+
const meta = JSON.parse(fs6.readFileSync(marketplaceJsonPath, "utf8"));
|
|
8329
10144
|
if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
|
|
8330
10145
|
return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
|
|
8331
10146
|
}
|
|
8332
10147
|
} catch {
|
|
8333
10148
|
}
|
|
8334
10149
|
}
|
|
8335
|
-
const pluginsDir =
|
|
8336
|
-
if (
|
|
8337
|
-
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: "" }));
|
|
8338
10153
|
}
|
|
8339
10154
|
return [];
|
|
8340
10155
|
}
|
|
8341
10156
|
function resolveSkillsSrc(marketplacePath, selectedPlugin) {
|
|
8342
|
-
const pluginsBase =
|
|
8343
|
-
const skillsSrc =
|
|
8344
|
-
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)) {
|
|
8345
10160
|
throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
|
|
8346
10161
|
}
|
|
8347
10162
|
return skillsSrc;
|
|
8348
10163
|
}
|
|
8349
10164
|
function copyPluginSkills(skillsSrc, targetDir, meta) {
|
|
8350
|
-
|
|
8351
|
-
const resolvedTarget =
|
|
8352
|
-
const skillNames =
|
|
8353
|
-
(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()
|
|
8354
10169
|
);
|
|
8355
10170
|
const installed = [];
|
|
8356
10171
|
const failed = [];
|
|
8357
10172
|
for (const skillName of skillNames) {
|
|
8358
|
-
const dest =
|
|
8359
|
-
if (!dest.startsWith(resolvedTarget +
|
|
10173
|
+
const dest = path11.resolve(targetDir, skillName);
|
|
10174
|
+
if (!dest.startsWith(resolvedTarget + path11.sep)) {
|
|
8360
10175
|
failed.push(skillName);
|
|
8361
10176
|
continue;
|
|
8362
10177
|
}
|
|
8363
|
-
|
|
8364
|
-
|
|
10178
|
+
fs6.rmSync(dest, { recursive: true, force: true });
|
|
10179
|
+
fs6.cpSync(path11.join(skillsSrc, skillName), dest, { recursive: true });
|
|
8365
10180
|
installed.push(skillName);
|
|
8366
10181
|
}
|
|
8367
10182
|
if (meta) {
|
|
@@ -8378,9 +10193,10 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
|
|
|
8378
10193
|
|
|
8379
10194
|
// src/services/config/commands.ts
|
|
8380
10195
|
import chalk from "chalk";
|
|
8381
|
-
import
|
|
10196
|
+
import fs7 from "fs";
|
|
8382
10197
|
function registerConfigCommands(program2) {
|
|
8383
10198
|
const config = program2.command("config").description("Manage pncli configuration");
|
|
10199
|
+
registerKeychainCommands(config, program2);
|
|
8384
10200
|
config.command("init").description("Setup wizard for service credentials (interactive)").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
|
|
8385
10201
|
const start = Date.now();
|
|
8386
10202
|
try {
|
|
@@ -8492,8 +10308,8 @@ function registerConfigCommands(program2) {
|
|
|
8492
10308
|
if (cfg.ado.baseUrl) {
|
|
8493
10309
|
try {
|
|
8494
10310
|
const collection = cfg.defaults.ado?.collection;
|
|
8495
|
-
const
|
|
8496
|
-
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);
|
|
8497
10313
|
results.ado = { ok: true, message: "connected" };
|
|
8498
10314
|
} catch (err) {
|
|
8499
10315
|
results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
@@ -8786,7 +10602,7 @@ async function initGlobalConfig(start) {
|
|
|
8786
10602
|
message: "Jira base URL (e.g. jira.imagile.dev):",
|
|
8787
10603
|
default: ""
|
|
8788
10604
|
});
|
|
8789
|
-
const jiraApiToken = await
|
|
10605
|
+
const jiraApiToken = await password2({
|
|
8790
10606
|
message: "Jira personal access token:"
|
|
8791
10607
|
});
|
|
8792
10608
|
process.stderr.write("\n\u2500\u2500 Bitbucket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -8794,7 +10610,7 @@ async function initGlobalConfig(start) {
|
|
|
8794
10610
|
message: "Bitbucket Server base URL (e.g. bitbucket.imagile.dev):",
|
|
8795
10611
|
default: ""
|
|
8796
10612
|
});
|
|
8797
|
-
const bitbucketPat = await
|
|
10613
|
+
const bitbucketPat = await password2({
|
|
8798
10614
|
message: "Bitbucket personal access token:"
|
|
8799
10615
|
});
|
|
8800
10616
|
process.stderr.write("\n\u2500\u2500 GitHub \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -8809,7 +10625,7 @@ async function initGlobalConfig(start) {
|
|
|
8809
10625
|
message: "GitHub API base URL\n GitHub.com: api.github.com\n GitHub Enterprise: <host>/api/v3\n URL: ",
|
|
8810
10626
|
default: ""
|
|
8811
10627
|
});
|
|
8812
|
-
githubToken = await
|
|
10628
|
+
githubToken = await password2({
|
|
8813
10629
|
message: "GitHub personal access token (classic or fine-grained):"
|
|
8814
10630
|
});
|
|
8815
10631
|
if (githubBaseUrl && githubToken) {
|
|
@@ -8836,7 +10652,7 @@ async function initGlobalConfig(start) {
|
|
|
8836
10652
|
message: "Confluence base URL (e.g. confluence.imagile.dev):",
|
|
8837
10653
|
default: ""
|
|
8838
10654
|
});
|
|
8839
|
-
const confluenceApiToken = await
|
|
10655
|
+
const confluenceApiToken = await password2({
|
|
8840
10656
|
message: "Confluence personal access token:"
|
|
8841
10657
|
});
|
|
8842
10658
|
process.stderr.write("\n\u2500\u2500 Artifactory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
@@ -8854,7 +10670,7 @@ async function initGlobalConfig(start) {
|
|
|
8854
10670
|
message: "Artifactory base URL (e.g. artifactory.imagile.dev):",
|
|
8855
10671
|
default: ""
|
|
8856
10672
|
});
|
|
8857
|
-
artifactoryToken = await
|
|
10673
|
+
artifactoryToken = await password2({
|
|
8858
10674
|
message: "Artifactory API token:"
|
|
8859
10675
|
});
|
|
8860
10676
|
process.stderr.write("\nConfigure which ecosystems you use (skip any that don't apply):\n");
|
|
@@ -8883,7 +10699,7 @@ async function initGlobalConfig(start) {
|
|
|
8883
10699
|
message: "SonarQube Server base URL (e.g. sonar.imagile.dev):",
|
|
8884
10700
|
default: ""
|
|
8885
10701
|
});
|
|
8886
|
-
sonarToken = await
|
|
10702
|
+
sonarToken = await password2({
|
|
8887
10703
|
message: "SonarQube personal access token:"
|
|
8888
10704
|
});
|
|
8889
10705
|
}
|
|
@@ -8901,7 +10717,7 @@ async function initGlobalConfig(start) {
|
|
|
8901
10717
|
default: false
|
|
8902
10718
|
});
|
|
8903
10719
|
if (useSdeConnectionString) {
|
|
8904
|
-
const sdeConnection = await
|
|
10720
|
+
const sdeConnection = await password2({
|
|
8905
10721
|
message: "SDElements connection string (api-token@hostname, e.g. mytoken@myorg.sdelements.com):"
|
|
8906
10722
|
});
|
|
8907
10723
|
const atIdx = sdeConnection.indexOf("@");
|
|
@@ -8914,7 +10730,7 @@ async function initGlobalConfig(start) {
|
|
|
8914
10730
|
message: "SDElements hostname\n Cloud-hosted: imagile.sdelements.com\n On-premise: sde.imagile.dev\n Host: ",
|
|
8915
10731
|
default: ""
|
|
8916
10732
|
});
|
|
8917
|
-
sdeToken = await
|
|
10733
|
+
sdeToken = await password2({
|
|
8918
10734
|
message: "SDElements API token:"
|
|
8919
10735
|
});
|
|
8920
10736
|
}
|
|
@@ -8936,7 +10752,7 @@ async function initGlobalConfig(start) {
|
|
|
8936
10752
|
message: "Azure DevOps Server base URL (e.g. tfs.imagile.dev or devops.imagile.dev/tfs):",
|
|
8937
10753
|
default: ""
|
|
8938
10754
|
});
|
|
8939
|
-
adoPat = await
|
|
10755
|
+
adoPat = await password2({
|
|
8940
10756
|
message: "Azure DevOps personal access token:"
|
|
8941
10757
|
});
|
|
8942
10758
|
adoCollection = await input2({
|
|
@@ -9012,7 +10828,7 @@ async function initGlobalConfig(start) {
|
|
|
9012
10828
|
message: "Jenkins username:",
|
|
9013
10829
|
default: ""
|
|
9014
10830
|
});
|
|
9015
|
-
jenkinsApiToken = await
|
|
10831
|
+
jenkinsApiToken = await password2({
|
|
9016
10832
|
message: "Jenkins API token:"
|
|
9017
10833
|
});
|
|
9018
10834
|
}
|
|
@@ -9035,7 +10851,7 @@ async function initGlobalConfig(start) {
|
|
|
9035
10851
|
message: "Checkmarx One tenant name (IAM realm, e.g. imagile):",
|
|
9036
10852
|
default: ""
|
|
9037
10853
|
});
|
|
9038
|
-
checkmarxApiKey = await
|
|
10854
|
+
checkmarxApiKey = await password2({
|
|
9039
10855
|
message: "Checkmarx One API key (leave blank to use an OAuth client):"
|
|
9040
10856
|
});
|
|
9041
10857
|
if (!checkmarxApiKey) {
|
|
@@ -9043,7 +10859,7 @@ async function initGlobalConfig(start) {
|
|
|
9043
10859
|
message: "Checkmarx One OAuth client ID:",
|
|
9044
10860
|
default: ""
|
|
9045
10861
|
});
|
|
9046
|
-
checkmarxClientSecret = await
|
|
10862
|
+
checkmarxClientSecret = await password2({
|
|
9047
10863
|
message: "Checkmarx One OAuth client secret:",
|
|
9048
10864
|
validate: (v) => v.length > 0 || "Client secret cannot be blank"
|
|
9049
10865
|
});
|
|
@@ -9088,7 +10904,7 @@ async function initGlobalConfig(start) {
|
|
|
9088
10904
|
message: "User Code (from User Token):",
|
|
9089
10905
|
default: ""
|
|
9090
10906
|
});
|
|
9091
|
-
sonatypeIqPasscode = await
|
|
10907
|
+
sonatypeIqPasscode = await password2({
|
|
9092
10908
|
message: "Passcode (from User Token):"
|
|
9093
10909
|
});
|
|
9094
10910
|
if (sonatypeIqBaseUrl && sonatypeIqUserCode && sonatypeIqPasscode) {
|
|
@@ -9136,10 +10952,10 @@ async function initGlobalConfig(start) {
|
|
|
9136
10952
|
message: "Username (email):",
|
|
9137
10953
|
default: ""
|
|
9138
10954
|
});
|
|
9139
|
-
contrastApiKey = await
|
|
10955
|
+
contrastApiKey = await password2({
|
|
9140
10956
|
message: "API key:"
|
|
9141
10957
|
});
|
|
9142
|
-
contrastServiceKey = await
|
|
10958
|
+
contrastServiceKey = await password2({
|
|
9143
10959
|
message: "Service key:"
|
|
9144
10960
|
});
|
|
9145
10961
|
const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
|
|
@@ -9178,7 +10994,7 @@ async function initGlobalConfig(start) {
|
|
|
9178
10994
|
message: "OpenShift API server URL (e.g. https://api.cluster.imagile.dev:6443):",
|
|
9179
10995
|
default: ""
|
|
9180
10996
|
});
|
|
9181
|
-
openShiftToken = await
|
|
10997
|
+
openShiftToken = await password2({
|
|
9182
10998
|
message: "Service account token:"
|
|
9183
10999
|
});
|
|
9184
11000
|
if (openShiftBaseUrl && openShiftToken) {
|
|
@@ -9214,7 +11030,7 @@ async function initGlobalConfig(start) {
|
|
|
9214
11030
|
message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
|
|
9215
11031
|
default: ""
|
|
9216
11032
|
});
|
|
9217
|
-
dynatraceApiToken = await
|
|
11033
|
+
dynatraceApiToken = await password2({
|
|
9218
11034
|
message: "Environment API token (entities.read and problems.read):"
|
|
9219
11035
|
});
|
|
9220
11036
|
const configureTraces = await confirm({
|
|
@@ -9226,7 +11042,7 @@ async function initGlobalConfig(start) {
|
|
|
9226
11042
|
message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
|
|
9227
11043
|
default: ""
|
|
9228
11044
|
});
|
|
9229
|
-
dynatracePlatformToken = await
|
|
11045
|
+
dynatracePlatformToken = await password2({
|
|
9230
11046
|
message: "Platform token with Grail span read permissions:"
|
|
9231
11047
|
});
|
|
9232
11048
|
}
|
|
@@ -9265,7 +11081,7 @@ async function initGlobalConfig(start) {
|
|
|
9265
11081
|
message: "LogScale base URL (e.g. https://logscale.imagile.dev):",
|
|
9266
11082
|
default: ""
|
|
9267
11083
|
});
|
|
9268
|
-
logscaleToken = await
|
|
11084
|
+
logscaleToken = await password2({
|
|
9269
11085
|
message: "LogScale personal access token:"
|
|
9270
11086
|
});
|
|
9271
11087
|
if (logscaleBaseUrl && logscaleToken) {
|
|
@@ -9299,7 +11115,7 @@ async function initGlobalConfig(start) {
|
|
|
9299
11115
|
message: "Split.IO Admin API base URL (e.g. https://api.split.io):",
|
|
9300
11116
|
default: ""
|
|
9301
11117
|
});
|
|
9302
|
-
splitioAdminApiKey = await
|
|
11118
|
+
splitioAdminApiKey = await password2({
|
|
9303
11119
|
message: "Split.IO Admin API key:"
|
|
9304
11120
|
});
|
|
9305
11121
|
if (splitioBaseUrl && splitioAdminApiKey) {
|
|
@@ -9329,7 +11145,7 @@ async function initGlobalConfig(start) {
|
|
|
9329
11145
|
let figmaToken = "";
|
|
9330
11146
|
if (useFigma) {
|
|
9331
11147
|
process.stderr.write(" Generate a personal access token in Figma under Account Settings \u2192 Personal access tokens.\n");
|
|
9332
|
-
figmaToken = await
|
|
11148
|
+
figmaToken = await password2({
|
|
9333
11149
|
message: "Figma personal access token:"
|
|
9334
11150
|
});
|
|
9335
11151
|
if (figmaToken) {
|
|
@@ -9366,7 +11182,7 @@ async function initGlobalConfig(start) {
|
|
|
9366
11182
|
message: "Alation base URL (e.g. https://alation.imagile.dev):",
|
|
9367
11183
|
validate: (v) => v.trim().length > 0 || "Required"
|
|
9368
11184
|
});
|
|
9369
|
-
alationRefreshToken = await
|
|
11185
|
+
alationRefreshToken = await password2({
|
|
9370
11186
|
message: "Alation refresh token:"
|
|
9371
11187
|
});
|
|
9372
11188
|
alationUserId = await input2({
|
|
@@ -9422,6 +11238,11 @@ async function initGlobalConfig(start) {
|
|
|
9422
11238
|
const existingOpenshift = existingGlobal?.openshift;
|
|
9423
11239
|
writeGlobalConfig({
|
|
9424
11240
|
jenkinsInstances: existingInstances,
|
|
11241
|
+
// Not prompted for here, so carry them over: dropping gitAuth strands `git-auth disable`
|
|
11242
|
+
// (it restores from the recorded backups), and marketplaces/skillsTargets hold tokens and paths.
|
|
11243
|
+
...existingGlobal?.marketplaces ? { marketplaces: existingGlobal.marketplaces } : {},
|
|
11244
|
+
...existingGlobal?.skillsTargets ? { skillsTargets: existingGlobal.skillsTargets } : {},
|
|
11245
|
+
...existingGlobal?.gitAuth ? { gitAuth: existingGlobal.gitAuth } : {},
|
|
9425
11246
|
user: {
|
|
9426
11247
|
email: userEmail || void 0,
|
|
9427
11248
|
userId: userId || void 0
|
|
@@ -9624,7 +11445,7 @@ async function initRepoConfig(start) {
|
|
|
9624
11445
|
process.exitCode = ExitCode.SUCCESS;
|
|
9625
11446
|
return;
|
|
9626
11447
|
}
|
|
9627
|
-
if (
|
|
11448
|
+
if (fs7.existsSync(".pncli.json")) {
|
|
9628
11449
|
const overwrite = await confirm({
|
|
9629
11450
|
message: ".pncli.json already exists. Overwrite?",
|
|
9630
11451
|
default: false
|
|
@@ -9658,14 +11479,172 @@ async function initRepoConfig(start) {
|
|
|
9658
11479
|
}
|
|
9659
11480
|
|
|
9660
11481
|
// src/services/doctor/commands.ts
|
|
9661
|
-
import
|
|
9662
|
-
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
|
|
9663
11642
|
function checkConfigFile(filePath) {
|
|
9664
|
-
if (!
|
|
11643
|
+
if (!fs9.existsSync(filePath)) {
|
|
9665
11644
|
return { path: filePath, exists: false, valid: false, message: "not present" };
|
|
9666
11645
|
}
|
|
9667
11646
|
try {
|
|
9668
|
-
JSON.parse(
|
|
11647
|
+
JSON.parse(fs9.readFileSync(filePath, "utf8"));
|
|
9669
11648
|
return { path: filePath, exists: true, valid: true, message: "ok" };
|
|
9670
11649
|
} catch (err) {
|
|
9671
11650
|
return {
|
|
@@ -9746,13 +11725,13 @@ function buildProblems(globalFile, repoFile, credentials, skillLocations, market
|
|
|
9746
11725
|
return problems;
|
|
9747
11726
|
}
|
|
9748
11727
|
function registerDoctorCommands(program2) {
|
|
9749
|
-
program2.command("doctor").description("Diagnose pncli setup: config files, credentials, agent skills, and
|
|
11728
|
+
program2.command("doctor").description("Diagnose pncli setup: config files, credentials, keychain references, git auth for marketplace hosts, agent skills, and marketplaces").option("--offline", "Skip credential checks (no network calls)").action(async (cmdOpts) => {
|
|
9750
11729
|
const start = Date.now();
|
|
9751
11730
|
try {
|
|
9752
11731
|
const opts = program2.optsWithGlobals();
|
|
9753
11732
|
const globalFile = checkConfigFile(opts.config ?? getGlobalConfigPath());
|
|
9754
11733
|
const repoRoot = findGitRoot() ?? process.cwd();
|
|
9755
|
-
const repoFile = checkConfigFile(
|
|
11734
|
+
const repoFile = checkConfigFile(path12.join(repoRoot, ".pncli.json"));
|
|
9756
11735
|
let credentials = null;
|
|
9757
11736
|
let credentialsSkipped = cmdOpts.offline ? "--offline" : null;
|
|
9758
11737
|
if (!cmdOpts.offline) {
|
|
@@ -9777,9 +11756,33 @@ function registerDoctorCommands(program2) {
|
|
|
9777
11756
|
name: m.name ?? m.repoUrl ?? "(unnamed)",
|
|
9778
11757
|
repoUrl: m.repoUrl ?? null,
|
|
9779
11758
|
localPath: m.localPath ?? null,
|
|
9780
|
-
cloneExists: !!m.localPath &&
|
|
11759
|
+
cloneExists: !!m.localPath && fs9.existsSync(m.localPath)
|
|
9781
11760
|
}));
|
|
9782
|
-
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
|
+
];
|
|
9783
11786
|
if (credentials) {
|
|
9784
11787
|
const statuses = Object.values(credentials).map((r) => r.status);
|
|
9785
11788
|
if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
|
|
@@ -9802,7 +11805,10 @@ function registerDoctorCommands(program2) {
|
|
|
9802
11805
|
registered: marketplaces,
|
|
9803
11806
|
hint: marketplaces.length === 0 ? "No org marketplaces registered. Add one with: pncli skills marketplace add <git-url> --all-agents" : "Refresh with: pncli skills marketplace sync --marketplace all --all-agents"
|
|
9804
11807
|
},
|
|
9805
|
-
|
|
11808
|
+
keychain,
|
|
11809
|
+
gitAuth,
|
|
11810
|
+
problems,
|
|
11811
|
+
hint: problems.length === 0 ? null : "New to skills and marketplaces? pncli skills guide troubleshooting"
|
|
9806
11812
|
}, "doctor", "check", start);
|
|
9807
11813
|
} catch (err) {
|
|
9808
11814
|
fail(err, "doctor", "check", start);
|
|
@@ -10191,8 +12197,8 @@ function registerAdoProjectCommands(ado) {
|
|
|
10191
12197
|
}
|
|
10192
12198
|
|
|
10193
12199
|
// src/services/ado/commands/work.ts
|
|
10194
|
-
import
|
|
10195
|
-
import
|
|
12200
|
+
import fs10 from "fs";
|
|
12201
|
+
import path13 from "path";
|
|
10196
12202
|
|
|
10197
12203
|
// src/services/ado/input-schema.ts
|
|
10198
12204
|
var ADO_WORK_INPUT_FILE_SCHEMA = {
|
|
@@ -10433,11 +12439,11 @@ function registerAdoWorkCommands(ado) {
|
|
|
10433
12439
|
const attachments = await workClient.listAttachments(collection, parseInt(opts.id, 10));
|
|
10434
12440
|
const attachment = attachments.find((a) => a.id === opts.attachmentId);
|
|
10435
12441
|
if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
|
|
10436
|
-
const outDir = opts.dir ??
|
|
10437
|
-
|
|
10438
|
-
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));
|
|
10439
12445
|
const buffer = await workClient.downloadAttachment(attachment.url);
|
|
10440
|
-
|
|
12446
|
+
fs10.writeFileSync(outPath, buffer);
|
|
10441
12447
|
success({ saved: outPath, name: attachment.name, size: buffer.length }, "ado", "work-download-attachment", start);
|
|
10442
12448
|
} catch (err) {
|
|
10443
12449
|
fail(err, "ado", "work-download-attachment", start);
|
|
@@ -10471,10 +12477,10 @@ function registerAdoWorkCommands(ado) {
|
|
|
10471
12477
|
const types = await discoverTypes(workClient, collection, project);
|
|
10472
12478
|
if (opts.save) {
|
|
10473
12479
|
const globalConfigPath = getGlobalConfigPath(globalOpts.config);
|
|
10474
|
-
const
|
|
10475
|
-
const existing = JSON.parse(
|
|
12480
|
+
const fs13 = await import("fs");
|
|
12481
|
+
const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
10476
12482
|
existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
|
|
10477
|
-
|
|
12483
|
+
fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
10478
12484
|
warn(`Saved ${types.length} types to ${globalConfigPath}`);
|
|
10479
12485
|
}
|
|
10480
12486
|
success(types, "ado", "work-types", start);
|
|
@@ -10503,11 +12509,11 @@ function registerAdoWorkCommands(ado) {
|
|
|
10503
12509
|
) : allFields;
|
|
10504
12510
|
if (opts.save) {
|
|
10505
12511
|
const globalConfigPath = getGlobalConfigPath(globalOpts.config);
|
|
10506
|
-
const
|
|
10507
|
-
const existing = JSON.parse(
|
|
12512
|
+
const fs13 = await import("fs");
|
|
12513
|
+
const existing = JSON.parse(fs13.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
10508
12514
|
const aliases = buildDefaultAliases(allFields);
|
|
10509
12515
|
existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
|
|
10510
|
-
|
|
12516
|
+
fs13.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
10511
12517
|
warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
|
|
10512
12518
|
}
|
|
10513
12519
|
success(fields, "ado", "work-fields", start);
|
|
@@ -10804,7 +12810,7 @@ function registerAdoRepoCommands(ado) {
|
|
|
10804
12810
|
const start = Date.now();
|
|
10805
12811
|
try {
|
|
10806
12812
|
const globalOpts = ado.optsWithGlobals();
|
|
10807
|
-
const { loadConfig: lc } = await import("./config-
|
|
12813
|
+
const { loadConfig: lc } = await import("./config-OH2O5E3P.js");
|
|
10808
12814
|
const config = lc({ configPath: globalOpts.config });
|
|
10809
12815
|
const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
|
|
10810
12816
|
const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
|
|
@@ -11105,6 +13111,10 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
|
|
|
11105
13111
|
}
|
|
11106
13112
|
throw new PncliError(`Timed out waiting for build #${buildNumber} to complete after ${timeoutMs / 1e3}s`, 1);
|
|
11107
13113
|
}
|
|
13114
|
+
function rawJenkinsInstances(configPath) {
|
|
13115
|
+
const stored = loadJsonFile(getGlobalConfigPath(configPath))?.jenkinsInstances;
|
|
13116
|
+
return Array.isArray(stored) ? stored : [];
|
|
13117
|
+
}
|
|
11108
13118
|
function registerJenkinsCommands(program2) {
|
|
11109
13119
|
const jenkins = program2.command("jenkins").description("Jenkins operations").option("--instance <name>", "Named Jenkins instance from global config (overrides default jenkins config)");
|
|
11110
13120
|
const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
|
|
@@ -11128,15 +13138,15 @@ function registerJenkinsCommands(program2) {
|
|
|
11128
13138
|
try {
|
|
11129
13139
|
const opts = jenkins.optsWithGlobals();
|
|
11130
13140
|
const configPath = opts.config;
|
|
11131
|
-
const existing =
|
|
13141
|
+
const existing = rawJenkinsInstances(configPath);
|
|
11132
13142
|
const idx = existing.findIndex((i) => i.name === o.name);
|
|
11133
13143
|
if (idx >= 0 && !o.force) {
|
|
11134
13144
|
throw new PncliError(`Jenkins instance "${o.name}" already exists. Pass --force to overwrite it, or run: pncli jenkins instance remove --name ${o.name}`, 1);
|
|
11135
13145
|
}
|
|
11136
13146
|
let apiToken = o.apiToken;
|
|
11137
13147
|
if (!apiToken && process.stdin.isTTY) {
|
|
11138
|
-
const { default:
|
|
11139
|
-
apiToken = await
|
|
13148
|
+
const { default: password3 } = await import("@inquirer/password");
|
|
13149
|
+
apiToken = await password3({ message: `API token for "${o.name}" (leave blank to skip):` });
|
|
11140
13150
|
}
|
|
11141
13151
|
const entry = {
|
|
11142
13152
|
name: o.name,
|
|
@@ -11156,7 +13166,7 @@ function registerJenkinsCommands(program2) {
|
|
|
11156
13166
|
try {
|
|
11157
13167
|
const opts = jenkins.optsWithGlobals();
|
|
11158
13168
|
const configPath = opts.config;
|
|
11159
|
-
const existing =
|
|
13169
|
+
const existing = rawJenkinsInstances(configPath);
|
|
11160
13170
|
if (!existing.some((i) => i.name === o.name)) {
|
|
11161
13171
|
throw new PncliError(`Jenkins instance "${o.name}" not found. Run: pncli jenkins instance list`, 1);
|
|
11162
13172
|
}
|
|
@@ -11278,18 +13288,18 @@ var ArtifactoryClient = class {
|
|
|
11278
13288
|
}
|
|
11279
13289
|
});
|
|
11280
13290
|
}
|
|
11281
|
-
async getStorageInfo(repo,
|
|
11282
|
-
const cleanPath =
|
|
13291
|
+
async getStorageInfo(repo, path14) {
|
|
13292
|
+
const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
|
|
11283
13293
|
return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`);
|
|
11284
13294
|
}
|
|
11285
|
-
async getProperties(repo,
|
|
11286
|
-
const cleanPath =
|
|
13295
|
+
async getProperties(repo, path14) {
|
|
13296
|
+
const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
|
|
11287
13297
|
return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
|
|
11288
13298
|
params: { properties: "" }
|
|
11289
13299
|
});
|
|
11290
13300
|
}
|
|
11291
|
-
async setProperties(repo,
|
|
11292
|
-
const cleanPath =
|
|
13301
|
+
async setProperties(repo, path14, properties, opts = {}) {
|
|
13302
|
+
const cleanPath = path14.startsWith("/") ? path14.slice(1) : path14;
|
|
11293
13303
|
const propString = Object.entries(properties).map(([k, v]) => `${k}=${v}`).join(";");
|
|
11294
13304
|
await this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
|
|
11295
13305
|
method: "PUT",
|
|
@@ -11371,27 +13381,27 @@ function registerArtifactoryCommands(program2) {
|
|
|
11371
13381
|
fail(err, "artifactory", "repos", start);
|
|
11372
13382
|
}
|
|
11373
13383
|
});
|
|
11374
|
-
art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo,
|
|
13384
|
+
art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path14) => {
|
|
11375
13385
|
const start = Date.now();
|
|
11376
13386
|
try {
|
|
11377
13387
|
const client = getClient8(program2);
|
|
11378
|
-
const data = await client.getStorageInfo(repo,
|
|
13388
|
+
const data = await client.getStorageInfo(repo, path14);
|
|
11379
13389
|
success(data, "artifactory", "artifact-info", start);
|
|
11380
13390
|
} catch (err) {
|
|
11381
13391
|
fail(err, "artifactory", "artifact-info", start);
|
|
11382
13392
|
}
|
|
11383
13393
|
});
|
|
11384
|
-
art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo,
|
|
13394
|
+
art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path14) => {
|
|
11385
13395
|
const start = Date.now();
|
|
11386
13396
|
try {
|
|
11387
13397
|
const client = getClient8(program2);
|
|
11388
|
-
const data = await client.getProperties(repo,
|
|
13398
|
+
const data = await client.getProperties(repo, path14);
|
|
11389
13399
|
success(data, "artifactory", "properties-get", start);
|
|
11390
13400
|
} catch (err) {
|
|
11391
13401
|
fail(err, "artifactory", "properties-get", start);
|
|
11392
13402
|
}
|
|
11393
13403
|
});
|
|
11394
|
-
art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo,
|
|
13404
|
+
art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo, path14, keyvals, opts) => {
|
|
11395
13405
|
const start = Date.now();
|
|
11396
13406
|
try {
|
|
11397
13407
|
if (!keyvals.length) throw new PncliError("Provide at least one key=value pair");
|
|
@@ -11402,8 +13412,8 @@ function registerArtifactoryCommands(program2) {
|
|
|
11402
13412
|
properties[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
11403
13413
|
}
|
|
11404
13414
|
const client = getClient8(program2);
|
|
11405
|
-
await client.setProperties(repo,
|
|
11406
|
-
success({ repo, path:
|
|
13415
|
+
await client.setProperties(repo, path14, properties, { recursive: opts.recursive });
|
|
13416
|
+
success({ repo, path: path14, properties }, "artifactory", "properties-set", start);
|
|
11407
13417
|
} catch (err) {
|
|
11408
13418
|
fail(err, "artifactory", "properties-set", start);
|
|
11409
13419
|
}
|
|
@@ -11721,8 +13731,8 @@ function registerSonatypeIqCommands(program2) {
|
|
|
11721
13731
|
const startTime = Date.now();
|
|
11722
13732
|
try {
|
|
11723
13733
|
const http = getHttpClient(program2);
|
|
11724
|
-
const
|
|
11725
|
-
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);
|
|
11726
13736
|
success(data, "sonatypeiq", "policies list", startTime);
|
|
11727
13737
|
} catch (err) {
|
|
11728
13738
|
fail(err, "sonatypeiq", "policies list", startTime);
|
|
@@ -12417,7 +14427,7 @@ function registerLogscaleCommands(program2) {
|
|
|
12417
14427
|
}
|
|
12418
14428
|
|
|
12419
14429
|
// src/services/splitio/commands.ts
|
|
12420
|
-
import
|
|
14430
|
+
import fs11 from "fs";
|
|
12421
14431
|
import confirm2 from "@inquirer/confirm";
|
|
12422
14432
|
function getHttp4(program2) {
|
|
12423
14433
|
const opts = program2.optsWithGlobals();
|
|
@@ -12429,9 +14439,9 @@ function getHttp4(program2) {
|
|
|
12429
14439
|
function loadInputFile(filePath) {
|
|
12430
14440
|
let raw;
|
|
12431
14441
|
if (filePath === "-") {
|
|
12432
|
-
raw =
|
|
14442
|
+
raw = fs11.readFileSync(process.stdin.fd, "utf8");
|
|
12433
14443
|
} else {
|
|
12434
|
-
raw =
|
|
14444
|
+
raw = fs11.readFileSync(filePath, "utf8");
|
|
12435
14445
|
}
|
|
12436
14446
|
try {
|
|
12437
14447
|
return JSON.parse(raw);
|
|
@@ -12482,7 +14492,7 @@ function registerSplitioCommands(program2) {
|
|
|
12482
14492
|
try {
|
|
12483
14493
|
const http = getHttp4(program2);
|
|
12484
14494
|
const data = await http.splitio(
|
|
12485
|
-
`/internal/api/v2/
|
|
14495
|
+
`/internal/api/v2/environments/ws/${encodeURIComponent(opts.workspace)}`
|
|
12486
14496
|
);
|
|
12487
14497
|
success(
|
|
12488
14498
|
{ count: data.objects.length, environments: data.objects },
|
|
@@ -12500,12 +14510,14 @@ function registerSplitioCommands(program2) {
|
|
|
12500
14510
|
try {
|
|
12501
14511
|
const http = getHttp4(program2);
|
|
12502
14512
|
const params = {
|
|
12503
|
-
wsId: opts.workspace,
|
|
12504
14513
|
limit: parseInt(opts.limit, 10),
|
|
12505
14514
|
offset: parseInt(opts.offset, 10)
|
|
12506
14515
|
};
|
|
12507
14516
|
if (opts.flagSet) params["flagSetName"] = opts.flagSet;
|
|
12508
|
-
const data = await http.splitio(
|
|
14517
|
+
const data = await http.splitio(
|
|
14518
|
+
`/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}`,
|
|
14519
|
+
{ params }
|
|
14520
|
+
);
|
|
12509
14521
|
success(
|
|
12510
14522
|
{ count: data.objects.length, totalCount: data.totalCount, flags: data.objects },
|
|
12511
14523
|
"splitio",
|
|
@@ -12522,14 +14534,12 @@ function registerSplitioCommands(program2) {
|
|
|
12522
14534
|
const http = getHttp4(program2);
|
|
12523
14535
|
if (opts.environment) {
|
|
12524
14536
|
const data = await http.splitio(
|
|
12525
|
-
`/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/
|
|
12526
|
-
{ params: { wsId: opts.workspace } }
|
|
14537
|
+
`/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}/environment/${encodeURIComponent(opts.environment)}`
|
|
12527
14538
|
);
|
|
12528
14539
|
success(data, "splitio", "flags get", start);
|
|
12529
14540
|
} else {
|
|
12530
14541
|
const data = await http.splitio(
|
|
12531
|
-
`/internal/api/v2/splits/${encodeURIComponent(opts.flag)}
|
|
12532
|
-
{ params: { wsId: opts.workspace } }
|
|
14542
|
+
`/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}`
|
|
12533
14543
|
);
|
|
12534
14544
|
success(data, "splitio", "flags get", start);
|
|
12535
14545
|
}
|
|
@@ -12554,7 +14564,7 @@ function registerSplitioCommands(program2) {
|
|
|
12554
14564
|
},
|
|
12555
14565
|
workspace: { id: opts.workspace }
|
|
12556
14566
|
};
|
|
12557
|
-
const data = await http.splitio(
|
|
14567
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12558
14568
|
method: "POST",
|
|
12559
14569
|
body
|
|
12560
14570
|
});
|
|
@@ -12571,8 +14581,7 @@ function registerSplitioCommands(program2) {
|
|
|
12571
14581
|
}
|
|
12572
14582
|
const http = getHttp4(program2);
|
|
12573
14583
|
const current = await http.splitio(
|
|
12574
|
-
`/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/
|
|
12575
|
-
{ params: { wsId: opts.workspace } }
|
|
14584
|
+
`/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}/environment/${encodeURIComponent(opts.environment)}`
|
|
12576
14585
|
);
|
|
12577
14586
|
const rules = current.rules ?? [];
|
|
12578
14587
|
const idx = parseInt(opts.ruleIndex, 10);
|
|
@@ -12596,7 +14605,7 @@ function registerSplitioCommands(program2) {
|
|
|
12596
14605
|
},
|
|
12597
14606
|
workspace: { id: opts.workspace }
|
|
12598
14607
|
};
|
|
12599
|
-
const data = await http.splitio(
|
|
14608
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12600
14609
|
method: "POST",
|
|
12601
14610
|
body
|
|
12602
14611
|
});
|
|
@@ -12610,8 +14619,7 @@ function registerSplitioCommands(program2) {
|
|
|
12610
14619
|
try {
|
|
12611
14620
|
const http = getHttp4(program2);
|
|
12612
14621
|
const current = await http.splitio(
|
|
12613
|
-
`/internal/api/v2/splits/${encodeURIComponent(opts.flag)}/
|
|
12614
|
-
{ params: { wsId: opts.workspace } }
|
|
14622
|
+
`/internal/api/v2/splits/ws/${encodeURIComponent(opts.workspace)}/name/${encodeURIComponent(opts.flag)}/environment/${encodeURIComponent(opts.environment)}`
|
|
12615
14623
|
);
|
|
12616
14624
|
await confirmWrite(opts, `SET_DEFAULT ${opts.treatment} on ${opts.flag} in ${opts.environment}`);
|
|
12617
14625
|
const body = {
|
|
@@ -12627,7 +14635,7 @@ function registerSplitioCommands(program2) {
|
|
|
12627
14635
|
},
|
|
12628
14636
|
workspace: { id: opts.workspace }
|
|
12629
14637
|
};
|
|
12630
|
-
const data = await http.splitio(
|
|
14638
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12631
14639
|
method: "POST",
|
|
12632
14640
|
body
|
|
12633
14641
|
});
|
|
@@ -12649,7 +14657,7 @@ function registerSplitioCommands(program2) {
|
|
|
12649
14657
|
split: { name: opts.flag, environment: { id: opts.environment } },
|
|
12650
14658
|
workspace: { id: opts.workspace }
|
|
12651
14659
|
};
|
|
12652
|
-
const data = await http.splitio(
|
|
14660
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12653
14661
|
method: "POST",
|
|
12654
14662
|
body
|
|
12655
14663
|
});
|
|
@@ -12677,7 +14685,7 @@ function registerSplitioCommands(program2) {
|
|
|
12677
14685
|
split: { name: entry.flag, environment: { id: opts.environment } },
|
|
12678
14686
|
workspace: { id: opts.workspace }
|
|
12679
14687
|
};
|
|
12680
|
-
const cr = await http.splitio(
|
|
14688
|
+
const cr = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12681
14689
|
method: "POST",
|
|
12682
14690
|
body
|
|
12683
14691
|
});
|
|
@@ -12706,7 +14714,7 @@ function registerSplitioCommands(program2) {
|
|
|
12706
14714
|
split: { name: opts.flag, environment: { id: opts.environment } },
|
|
12707
14715
|
workspace: { id: opts.workspace }
|
|
12708
14716
|
};
|
|
12709
|
-
const data = await http.splitio(
|
|
14717
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12710
14718
|
method: "POST",
|
|
12711
14719
|
body
|
|
12712
14720
|
});
|
|
@@ -12726,7 +14734,7 @@ function registerSplitioCommands(program2) {
|
|
|
12726
14734
|
split: { name: opts.flag, environment: { id: opts.environment } },
|
|
12727
14735
|
workspace: { id: opts.workspace }
|
|
12728
14736
|
};
|
|
12729
|
-
const data = await http.splitio(
|
|
14737
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12730
14738
|
method: "POST",
|
|
12731
14739
|
body
|
|
12732
14740
|
});
|
|
@@ -12746,7 +14754,7 @@ function registerSplitioCommands(program2) {
|
|
|
12746
14754
|
split: { name: opts.flag },
|
|
12747
14755
|
workspace: { id: opts.workspace }
|
|
12748
14756
|
};
|
|
12749
|
-
const data = await http.splitio(
|
|
14757
|
+
const data = await http.splitio(`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`, {
|
|
12750
14758
|
method: "POST",
|
|
12751
14759
|
body
|
|
12752
14760
|
});
|
|
@@ -12761,14 +14769,13 @@ function registerSplitioCommands(program2) {
|
|
|
12761
14769
|
try {
|
|
12762
14770
|
const http = getHttp4(program2);
|
|
12763
14771
|
const params = {
|
|
12764
|
-
wsId: opts.workspace,
|
|
12765
14772
|
limit: parseInt(opts.limit, 10),
|
|
12766
14773
|
offset: parseInt(opts.offset, 10)
|
|
12767
14774
|
};
|
|
12768
14775
|
if (opts.environment) params["environmentId"] = opts.environment;
|
|
12769
14776
|
if (opts.status) params["status"] = opts.status;
|
|
12770
14777
|
const data = await http.splitio(
|
|
12771
|
-
|
|
14778
|
+
`/internal/api/v2/changeRequests/ws/${encodeURIComponent(opts.workspace)}`,
|
|
12772
14779
|
{ params }
|
|
12773
14780
|
);
|
|
12774
14781
|
success(
|
|
@@ -12966,7 +14973,7 @@ function registerFigmaCommands(program2) {
|
|
|
12966
14973
|
}
|
|
12967
14974
|
|
|
12968
14975
|
// src/services/alation/commands.ts
|
|
12969
|
-
import
|
|
14976
|
+
import fs12 from "fs";
|
|
12970
14977
|
var MAX_PAGE = 1e3;
|
|
12971
14978
|
function parseIntOption(value, flag) {
|
|
12972
14979
|
if (value === void 0) return void 0;
|
|
@@ -13079,8 +15086,8 @@ function getHttp6(program2) {
|
|
|
13079
15086
|
Boolean(opts.dryRun)
|
|
13080
15087
|
);
|
|
13081
15088
|
}
|
|
13082
|
-
async function getOne(http,
|
|
13083
|
-
const rows = await http.alation(
|
|
15089
|
+
async function getOne(http, path14, id, what) {
|
|
15090
|
+
const rows = await http.alation(path14, { params: { id, limit: 1 } });
|
|
13084
15091
|
const row = Array.isArray(rows) ? rows.find((r) => r.id === id) ?? rows[0] : void 0;
|
|
13085
15092
|
if (!row) throw new PncliError(`${what} ${id} not found`, 404);
|
|
13086
15093
|
return row;
|
|
@@ -13130,7 +15137,7 @@ function registerAlationCommands(program2) {
|
|
|
13130
15137
|
const opts = program2.optsWithGlobals();
|
|
13131
15138
|
const config = loadConfig({ configPath: opts.config });
|
|
13132
15139
|
if (opts.dryRun) {
|
|
13133
|
-
|
|
15140
|
+
fs12.writeSync(process.stderr.fd, `DRY RUN: POST ${config.alation.baseUrl ?? "<alation.baseUrl>"}/integration/v1/createAPIAccessToken/ (token exchange skipped)
|
|
13134
15141
|
`);
|
|
13135
15142
|
process.exitCode = ExitCode.SUCCESS;
|
|
13136
15143
|
throw new PncliError("dry-run", 0);
|
|
@@ -13437,17 +15444,22 @@ Services:
|
|
|
13437
15444
|
splitio Split.IO (feature flags, Change Requests)
|
|
13438
15445
|
figma Figma (files, comments, version history)
|
|
13439
15446
|
alation Alation (data sources, schemas, tables, columns, search, Document Hubs)
|
|
13440
|
-
config Manage pncli configuration
|
|
13441
|
-
doctor Diagnose pncli setup (config
|
|
15447
|
+
config Manage pncli configuration (and OS keychain storage for credentials)
|
|
15448
|
+
doctor Diagnose pncli setup (config, credentials, keychain, git auth, agent skills, marketplaces)
|
|
13442
15449
|
skills Install and manage agent skills (Codex, GitHub Copilot, Claude Code)
|
|
13443
15450
|
jwt JWT token utilities (decode header and payload)
|
|
13444
15451
|
|
|
13445
15452
|
Agent skills and org plugins:
|
|
13446
15453
|
pncli skills install --all-agents Bundled pncli skills into this repo, every agent host
|
|
13447
15454
|
pncli skills marketplace add <git-url> --all-agents Org plugins (skills + AGENTS.md/CLAUDE.md) from a git marketplace
|
|
13448
|
-
pncli skills marketplace sync
|
|
15455
|
+
pncli skills marketplace sync Pull and refresh everything installed from marketplaces
|
|
13449
15456
|
pncli skills marketplace manage Interactive: toggle plugins, add/remove marketplaces
|
|
15457
|
+
pncli skills git-auth enable Let git and agent hosts authenticate to private marketplace hosts
|
|
15458
|
+
pncli skills guide How skills management fits together
|
|
13450
15459
|
pncli skills --help Every install location and command
|
|
15460
|
+
|
|
15461
|
+
Credentials:
|
|
15462
|
+
pncli config keychain migrate Move plaintext tokens into the OS keychain (--to config reverses it)
|
|
13451
15463
|
`);
|
|
13452
15464
|
program.parseAsync(process.argv).catch((err) => {
|
|
13453
15465
|
if (process.exitCode !== void 0) return;
|