@kolatts/pncli 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  setRepoConfigValue,
8
8
  writeGlobalConfig,
9
9
  writeRepoConfig
10
- } from "./chunk-S4SJLY4M.js";
10
+ } from "./chunk-O3LWAWSN.js";
11
11
  import {
12
12
  ExitCode,
13
13
  PncliError,
@@ -18,13 +18,13 @@ import {
18
18
  setGlobalUser,
19
19
  success,
20
20
  warn
21
- } from "./chunk-QQ7UKWTU.js";
21
+ } from "./chunk-3XHTRFRX.js";
22
22
  import {
23
23
  AdoCoreClient
24
24
  } from "./chunk-VDL32PSG.js";
25
25
 
26
26
  // src/cli.ts
27
- import { Command } from "commander";
27
+ import { Command as Command2 } from "commander";
28
28
  import { createRequire } from "module";
29
29
 
30
30
  // src/services/git/client.ts
@@ -771,7 +771,7 @@ function registerJiraCommands(program2) {
771
771
  fail(err, "jira", "get-issue", start);
772
772
  }
773
773
  });
774
- jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
774
+ jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", 'Parent issue key \u2014 creates an "is child of" link after creation').option("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
775
775
  const start = Date.now();
776
776
  try {
777
777
  const { client, fieldMap } = getClientAndFields(program2);
@@ -784,6 +784,14 @@ function registerJiraCommands(program2) {
784
784
  const customFieldValues = parseFieldArgs(opts.field, fieldMap);
785
785
  const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, customFieldValues });
786
786
  success(data, "jira", "create-issue", start);
787
+ if (opts.parent) {
788
+ try {
789
+ await client.linkIssue({ key: data.key, linkType: "is child of", target: opts.parent });
790
+ } catch (linkErr) {
791
+ warn(`Created ${data.key} but failed to link to parent ${opts.parent}: ${linkErr instanceof Error ? linkErr.message : linkErr}`);
792
+ }
793
+ }
794
+ return;
787
795
  } catch (err) {
788
796
  fail(err, "jira", "create-issue", start);
789
797
  }
@@ -2039,6 +2047,9 @@ function registerSdeCommands(program2) {
2039
2047
  });
2040
2048
  }
2041
2049
 
2050
+ // src/services/deps/commands.ts
2051
+ import { Option } from "commander";
2052
+
2042
2053
  // src/services/deps/parsers/index.ts
2043
2054
  import fs from "fs";
2044
2055
  import path4 from "path";
@@ -2893,11 +2904,341 @@ async function checkPackagesForVulns(packages) {
2893
2904
  return allVulnerable;
2894
2905
  }
2895
2906
 
2896
- // src/services/deps/clients/artifactory.ts
2897
- var TIMEOUT_MS2 = 15e3;
2898
- async function fetchWithTimeout2(url, token) {
2907
+ // src/services/deps/clients/sonatype.ts
2908
+ var SONATYPE_URL = "https://ossindex.sonatype.org";
2909
+ var BATCH_SIZE2 = 128;
2910
+ var TIMEOUT_MS2 = 3e4;
2911
+ async function fetchWithTimeout2(url, init) {
2899
2912
  const controller = new AbortController();
2900
2913
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
2914
+ try {
2915
+ return await fetch(url, { ...init, signal: controller.signal });
2916
+ } finally {
2917
+ clearTimeout(timer);
2918
+ }
2919
+ }
2920
+ function toPurl(pkg2) {
2921
+ switch (pkg2.ecosystem) {
2922
+ case "npm": {
2923
+ const encodedName = pkg2.name.startsWith("@") ? pkg2.name.replace("@", "%40") : pkg2.name;
2924
+ return `pkg:npm/${encodedName}@${pkg2.version}`;
2925
+ }
2926
+ case "nuget":
2927
+ return `pkg:nuget/${pkg2.name}@${pkg2.version}`;
2928
+ case "maven": {
2929
+ const colonIdx = pkg2.name.indexOf(":");
2930
+ if (colonIdx !== -1) {
2931
+ const groupId = pkg2.name.slice(0, colonIdx);
2932
+ const artifactId = pkg2.name.slice(colonIdx + 1);
2933
+ return `pkg:maven/${encodeURIComponent(groupId)}/${encodeURIComponent(artifactId)}@${pkg2.version}`;
2934
+ }
2935
+ return `pkg:maven/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
2936
+ }
2937
+ default:
2938
+ return `pkg:generic/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
2939
+ }
2940
+ }
2941
+ function inferSeverityFromCvss2(score) {
2942
+ if (score >= 9) return "CRITICAL";
2943
+ if (score >= 7) return "HIGH";
2944
+ if (score >= 4) return "MEDIUM";
2945
+ if (score > 0) return "LOW";
2946
+ return "UNKNOWN";
2947
+ }
2948
+ function mapSonatypeVuln(v, pkg2) {
2949
+ const refs = [v.reference, ...v.externalReferences ?? []].filter(Boolean);
2950
+ const advice = `See ${v.reference} for remediation details for ${pkg2.name}@${pkg2.version}.`;
2951
+ const cvePattern = /CVE-\d{4}-\d+/gi;
2952
+ const searchText = [v.displayName, v.title, v.description].filter(Boolean).join(" ");
2953
+ const aliases = [...new Set([...searchText.matchAll(cvePattern)].map((m) => m[0].toUpperCase()))];
2954
+ return {
2955
+ id: v.displayName || v.id,
2956
+ summary: v.title || v.description,
2957
+ severity: inferSeverityFromCvss2(v.cvssScore),
2958
+ cvss: v.cvssScore > 0 ? v.cvssScore : null,
2959
+ aliases,
2960
+ remediation: {
2961
+ fixAvailable: false,
2962
+ fixedVersions: [],
2963
+ advice
2964
+ },
2965
+ references: refs
2966
+ };
2967
+ }
2968
+ async function queryBatch2(purls, packages) {
2969
+ const res = await fetchWithTimeout2(`${SONATYPE_URL}/api/v3/component-report`, {
2970
+ method: "POST",
2971
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2972
+ body: JSON.stringify({ coordinates: purls })
2973
+ });
2974
+ if (res.status === 429) {
2975
+ throw new Error(
2976
+ "Sonatype OSS Index is rate-limiting requests from this IP. Create a free account at ossindex.sonatype.org and add your credentials to your pncli config to raise the limit."
2977
+ );
2978
+ }
2979
+ if (!res.ok) {
2980
+ throw new Error(`Sonatype OSS Index returned HTTP ${res.status}`);
2981
+ }
2982
+ const reports = await res.json();
2983
+ const vulnerable = [];
2984
+ const purlToPackage = /* @__PURE__ */ new Map();
2985
+ for (let i = 0; i < purls.length; i++) {
2986
+ purlToPackage.set(purls[i], packages[i]);
2987
+ }
2988
+ for (const report of reports) {
2989
+ if (!report?.coordinates) continue;
2990
+ const pkg2 = purlToPackage.get(report.coordinates);
2991
+ if (!pkg2) continue;
2992
+ const vulns = report.vulnerabilities ?? [];
2993
+ if (vulns.length === 0) continue;
2994
+ vulnerable.push({ ...pkg2, vulnerabilities: vulns.map((v) => mapSonatypeVuln(v, pkg2)) });
2995
+ }
2996
+ return vulnerable;
2997
+ }
2998
+ async function checkSonatypeConnectivity() {
2999
+ try {
3000
+ const res = await fetchWithTimeout2(`${SONATYPE_URL}/api/v3/component-report`, {
3001
+ method: "POST",
3002
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
3003
+ body: JSON.stringify({ coordinates: [] })
3004
+ });
3005
+ return { reachable: res.ok || res.status === 400 };
3006
+ } catch (err) {
3007
+ return {
3008
+ reachable: false,
3009
+ error: err instanceof Error ? err.message : String(err)
3010
+ };
3011
+ }
3012
+ }
3013
+ async function checkPackagesForVulnsViaSonatype(packages) {
3014
+ const allVulnerable = [];
3015
+ for (let i = 0; i < packages.length; i += BATCH_SIZE2) {
3016
+ const chunk = packages.slice(i, i + BATCH_SIZE2);
3017
+ const purls = chunk.map(toPurl);
3018
+ const results = await queryBatch2(purls, chunk);
3019
+ allVulnerable.push(...results);
3020
+ }
3021
+ return allVulnerable;
3022
+ }
3023
+
3024
+ // src/services/deps/clients/sonatypeiq.ts
3025
+ var TIMEOUT_MS3 = 6e4;
3026
+ var MAX_POLL_ATTEMPTS = 10;
3027
+ var POLL_INTERVAL_MS = 2e3;
3028
+ var BATCH_SIZE3 = 128;
3029
+ async function fetchWithTimeout3(url, init) {
3030
+ const controller = new AbortController();
3031
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS3);
3032
+ try {
3033
+ return await fetch(url, { ...init, signal: controller.signal });
3034
+ } finally {
3035
+ clearTimeout(timer);
3036
+ }
3037
+ }
3038
+ function buildBasicAuth(userCode, passcode) {
3039
+ return `Basic ${Buffer.from(`${userCode}:${passcode}`).toString("base64")}`;
3040
+ }
3041
+ function threatLevelToSeverity(level) {
3042
+ if (level >= 9) return "CRITICAL";
3043
+ if (level >= 7) return "HIGH";
3044
+ if (level >= 4) return "MEDIUM";
3045
+ if (level > 0) return "LOW";
3046
+ return "UNKNOWN";
3047
+ }
3048
+ function toPurl2(pkg2) {
3049
+ switch (pkg2.ecosystem) {
3050
+ case "npm": {
3051
+ const encodedName = pkg2.name.startsWith("@") ? pkg2.name.replace("@", "%40") : pkg2.name;
3052
+ return `pkg:npm/${encodedName}@${pkg2.version}`;
3053
+ }
3054
+ case "nuget":
3055
+ return `pkg:nuget/${pkg2.name}@${pkg2.version}`;
3056
+ case "maven": {
3057
+ const colonIdx = pkg2.name.indexOf(":");
3058
+ if (colonIdx !== -1) {
3059
+ const groupId = pkg2.name.slice(0, colonIdx);
3060
+ const artifactId = pkg2.name.slice(colonIdx + 1);
3061
+ return `pkg:maven/${encodeURIComponent(groupId)}/${encodeURIComponent(artifactId)}@${pkg2.version}`;
3062
+ }
3063
+ return `pkg:maven/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
3064
+ }
3065
+ default:
3066
+ return `pkg:generic/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
3067
+ }
3068
+ }
3069
+ function mapViolationToVuln(violation) {
3070
+ const cveRefs = violation.constraintViolations.flatMap(
3071
+ (cv) => cv.reasons.filter((r) => r.reference?.type === "VULNERABILITY").map((r) => r.reference.value)
3072
+ );
3073
+ const id = cveRefs[0] ?? violation.policyId;
3074
+ const severity = threatLevelToSeverity(violation.policyThreatLevel);
3075
+ return {
3076
+ id,
3077
+ summary: violation.policyName,
3078
+ severity,
3079
+ cvss: null,
3080
+ aliases: [...new Set(cveRefs)],
3081
+ remediation: {
3082
+ fixAvailable: false,
3083
+ fixedVersions: [],
3084
+ advice: `Policy violation: ${violation.policyName} (threat level ${violation.policyThreatLevel}). Check Sonatype IQ Server for details.`
3085
+ },
3086
+ references: []
3087
+ };
3088
+ }
3089
+ function syntheticVulnsFromCounts(criticalCount, severeCount, moderateCount, pkgName) {
3090
+ const vulns = [];
3091
+ if (criticalCount > 0) {
3092
+ vulns.push({
3093
+ id: `iq-critical-${pkgName}`,
3094
+ summary: `${criticalCount} critical vulnerability(ies) detected by Sonatype IQ`,
3095
+ severity: "CRITICAL",
3096
+ cvss: null,
3097
+ aliases: [],
3098
+ remediation: { fixAvailable: false, fixedVersions: [], advice: "Check Sonatype IQ Server for details." },
3099
+ references: []
3100
+ });
3101
+ }
3102
+ if (severeCount > 0) {
3103
+ vulns.push({
3104
+ id: `iq-high-${pkgName}`,
3105
+ summary: `${severeCount} severe vulnerability(ies) detected by Sonatype IQ`,
3106
+ severity: "HIGH",
3107
+ cvss: null,
3108
+ aliases: [],
3109
+ remediation: { fixAvailable: false, fixedVersions: [], advice: "Check Sonatype IQ Server for details." },
3110
+ references: []
3111
+ });
3112
+ }
3113
+ if (moderateCount > 0) {
3114
+ vulns.push({
3115
+ id: `iq-medium-${pkgName}`,
3116
+ summary: `${moderateCount} moderate vulnerability(ies) detected by Sonatype IQ`,
3117
+ severity: "MEDIUM",
3118
+ cvss: null,
3119
+ aliases: [],
3120
+ remediation: { fixAvailable: false, fixedVersions: [], advice: "Check Sonatype IQ Server for details." },
3121
+ references: []
3122
+ });
3123
+ }
3124
+ return vulns;
3125
+ }
3126
+ async function resolveApplicationInternalId(baseUrl, applicationId, auth) {
3127
+ const url = `${baseUrl.replace(/\/$/, "")}/api/v2/applications?publicId=${encodeURIComponent(applicationId)}`;
3128
+ const res = await fetchWithTimeout3(url, {
3129
+ method: "GET",
3130
+ headers: { "Authorization": auth, "Accept": "application/json" }
3131
+ });
3132
+ if (!res.ok) return applicationId;
3133
+ const data = await res.json();
3134
+ if (data.applications && data.applications.length > 0) {
3135
+ return data.applications[0].id;
3136
+ }
3137
+ return applicationId;
3138
+ }
3139
+ async function pollResults(baseUrl, resultsUrl, auth) {
3140
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
3141
+ if (attempt > 0) {
3142
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS * attempt));
3143
+ }
3144
+ const url = resultsUrl.startsWith("http") ? resultsUrl : `${baseUrl.replace(/\/$/, "")}${resultsUrl}`;
3145
+ const res = await fetchWithTimeout3(url, {
3146
+ method: "GET",
3147
+ headers: { "Authorization": auth, "Accept": "application/json" }
3148
+ });
3149
+ if (!res.ok) {
3150
+ throw new Error(`Sonatype IQ Server returned HTTP ${res.status} polling results`);
3151
+ }
3152
+ const data = await res.json();
3153
+ if (!data.isQueued) return data;
3154
+ }
3155
+ throw new Error("Sonatype IQ Server evaluation timed out waiting for results.");
3156
+ }
3157
+ async function evaluateBatch(purls, packages, baseUrl, applicationId, auth) {
3158
+ const url = `${baseUrl.replace(/\/$/, "")}/api/v2/evaluation/applications/${encodeURIComponent(applicationId)}`;
3159
+ const res = await fetchWithTimeout3(url, {
3160
+ method: "POST",
3161
+ headers: { "Authorization": auth, "Content-Type": "application/json", "Accept": "application/json" },
3162
+ body: JSON.stringify({ components: purls.map((p) => ({ packageUrl: p })) })
3163
+ });
3164
+ if (!res.ok) {
3165
+ throw new Error(`Sonatype IQ Server returned HTTP ${res.status}`);
3166
+ }
3167
+ let data = await res.json();
3168
+ if (data.isQueued && data.resultsUrl) {
3169
+ data = await pollResults(baseUrl, data.resultsUrl, auth);
3170
+ }
3171
+ const components = data.components ?? [];
3172
+ const vulnerable = [];
3173
+ const purlToPackage = /* @__PURE__ */ new Map();
3174
+ for (let i = 0; i < purls.length; i++) {
3175
+ purlToPackage.set(purls[i], packages[i]);
3176
+ }
3177
+ for (const result of components) {
3178
+ if (!result?.component?.packageUrl) continue;
3179
+ const pkg2 = purlToPackage.get(result.component.packageUrl);
3180
+ if (!pkg2) continue;
3181
+ const criticalCount = result.criticalVulnerabilityCount ?? 0;
3182
+ const severeCount = result.severeVulnerabilityCount ?? 0;
3183
+ const moderateCount = result.moderateVulnerabilityCount ?? 0;
3184
+ const totalVulns = criticalCount + severeCount + moderateCount;
3185
+ const violations = result.policyData?.policyViolations ?? [];
3186
+ const securityViolations = violations.filter((v) => v.policyThreatCategory === "SECURITY");
3187
+ if (securityViolations.length === 0 && totalVulns === 0) continue;
3188
+ const vulnerabilities = securityViolations.length > 0 ? securityViolations.map((v) => mapViolationToVuln(v)) : syntheticVulnsFromCounts(criticalCount, severeCount, moderateCount, pkg2.name);
3189
+ vulnerable.push({ ...pkg2, vulnerabilities });
3190
+ }
3191
+ return vulnerable;
3192
+ }
3193
+ async function checkPackagesViaIqServer(packages, applicationId, config) {
3194
+ const { baseUrl, userCode, passcode } = config.sonatypeiq;
3195
+ if (!baseUrl) throw new Error("Sonatype IQ Server baseUrl not configured.");
3196
+ if (!userCode || !passcode) throw new Error("Sonatype IQ Server credentials not configured.");
3197
+ const auth = buildBasicAuth(userCode, passcode);
3198
+ const internalId = await resolveApplicationInternalId(baseUrl, applicationId, auth);
3199
+ const allVulnerable = [];
3200
+ for (let i = 0; i < packages.length; i += BATCH_SIZE3) {
3201
+ const chunk = packages.slice(i, i + BATCH_SIZE3);
3202
+ const purls = chunk.map(toPurl2);
3203
+ const results = await evaluateBatch(purls, chunk, baseUrl, internalId, auth);
3204
+ allVulnerable.push(...results);
3205
+ }
3206
+ return allVulnerable;
3207
+ }
3208
+ async function checkSonatypeIqConnectivity(baseUrl, userCode, passcode) {
3209
+ try {
3210
+ const auth = buildBasicAuth(userCode, passcode);
3211
+ const url = `${baseUrl.replace(/\/$/, "")}/api/v2/applications?limit=1`;
3212
+ const controller = new AbortController();
3213
+ const timer = setTimeout(() => controller.abort(), 1e4);
3214
+ let res;
3215
+ try {
3216
+ res = await fetch(url, {
3217
+ method: "GET",
3218
+ headers: { "Authorization": auth, "Accept": "application/json" },
3219
+ signal: controller.signal
3220
+ });
3221
+ } finally {
3222
+ clearTimeout(timer);
3223
+ }
3224
+ if (res.status === 401 || res.status === 403) {
3225
+ return { reachable: true, authenticated: false, error: `Authentication failed (HTTP ${res.status})` };
3226
+ }
3227
+ return { reachable: true, authenticated: res.ok };
3228
+ } catch (err) {
3229
+ return {
3230
+ reachable: false,
3231
+ authenticated: false,
3232
+ error: err instanceof Error ? err.message : String(err)
3233
+ };
3234
+ }
3235
+ }
3236
+
3237
+ // src/services/deps/clients/artifactory.ts
3238
+ var TIMEOUT_MS4 = 15e3;
3239
+ async function fetchWithTimeout4(url, token) {
3240
+ const controller = new AbortController();
3241
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS4);
2901
3242
  try {
2902
3243
  return await fetch(url, {
2903
3244
  headers: { Authorization: `Bearer ${token}` },
@@ -2933,7 +3274,7 @@ async function checkArtifactoryConnectivity(config) {
2933
3274
  };
2934
3275
  }
2935
3276
  try {
2936
- const res = await fetchWithTimeout2(`${config.baseUrl}/api/system/ping`, config.token);
3277
+ const res = await fetchWithTimeout4(`${config.baseUrl}/api/system/ping`, config.token);
2937
3278
  if (res.status === 401 || res.status === 403) {
2938
3279
  return {
2939
3280
  reachable: true,
@@ -2983,7 +3324,7 @@ function repoForEcosystem(config, eco) {
2983
3324
  async function getLatestNpm(baseUrl, token, repoName, packageName) {
2984
3325
  try {
2985
3326
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
2986
- const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3327
+ const res = await fetchWithTimeout4(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
2987
3328
  if (!res.ok) return null;
2988
3329
  const data = await res.json();
2989
3330
  return data["dist-tags"]?.latest ?? null;
@@ -2994,7 +3335,7 @@ async function getLatestNpm(baseUrl, token, repoName, packageName) {
2994
3335
  async function getLatestNuget(baseUrl, token, repoName, packageName) {
2995
3336
  try {
2996
3337
  const lower = packageName.toLowerCase();
2997
- const res = await fetchWithTimeout2(
3338
+ const res = await fetchWithTimeout4(
2998
3339
  `${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
2999
3340
  token
3000
3341
  );
@@ -3010,7 +3351,7 @@ async function getLatestMaven(baseUrl, token, repoName, packageName) {
3010
3351
  const [groupId, artifactId] = packageName.split(":");
3011
3352
  if (!groupId || !artifactId) return null;
3012
3353
  try {
3013
- const res = await fetchWithTimeout2(
3354
+ const res = await fetchWithTimeout4(
3014
3355
  `${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
3015
3356
  token
3016
3357
  );
@@ -3060,7 +3401,7 @@ async function getOutdatedPackages(packages, config, filterType) {
3060
3401
  async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3061
3402
  try {
3062
3403
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
3063
- const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3404
+ const res = await fetchWithTimeout4(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3064
3405
  if (!res.ok) return null;
3065
3406
  const data = await res.json();
3066
3407
  if (typeof data.license === "string") return data.license;
@@ -3073,7 +3414,7 @@ async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3073
3414
  async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
3074
3415
  try {
3075
3416
  const lower = packageName.toLowerCase();
3076
- const res = await fetchWithTimeout2(
3417
+ const res = await fetchWithTimeout4(
3077
3418
  `${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
3078
3419
  token
3079
3420
  );
@@ -3125,15 +3466,24 @@ async function detectTier(config) {
3125
3466
  cachedTier = { tier, osvReachable, artifactoryReachable };
3126
3467
  return cachedTier;
3127
3468
  }
3469
+ async function checkSonatypeReachable() {
3470
+ const result = await checkSonatypeConnectivity();
3471
+ return result.reachable;
3472
+ }
3128
3473
  async function buildConnectivityData(config) {
3129
- const [osvResult, artResult] = await Promise.all([
3474
+ const iqCfg = config.sonatypeiq;
3475
+ const iqConfigured = !!(iqCfg.baseUrl && iqCfg.userCode && iqCfg.passcode);
3476
+ const checks = await Promise.all([
3130
3477
  checkOsvConnectivity(),
3131
- checkArtifactoryConnectivity(config.artifactory)
3478
+ checkArtifactoryConnectivity(config.artifactory),
3479
+ checkSonatypeConnectivity(),
3480
+ iqConfigured ? checkSonatypeIqConnectivity(iqCfg.baseUrl, iqCfg.userCode, iqCfg.passcode) : Promise.resolve(null)
3132
3481
  ]);
3482
+ const [osvResult, artResult, sonatypeResult, iqResult] = checks;
3133
3483
  const artCfg = config.artifactory;
3134
3484
  let tier = "local";
3135
3485
  if (artResult.reachable && artResult.authenticated) tier = "artifactory";
3136
- if (osvResult.reachable) tier = "full";
3486
+ if (osvResult.reachable || sonatypeResult.reachable) tier = "full";
3137
3487
  cachedTier = {
3138
3488
  tier,
3139
3489
  osvReachable: osvResult.reachable,
@@ -3156,46 +3506,158 @@ async function buildConnectivityData(config) {
3156
3506
  url: "https://api.osv.dev",
3157
3507
  ...osvResult.error ? { error: osvResult.error } : {}
3158
3508
  },
3509
+ sonatype: {
3510
+ reachable: sonatypeResult.reachable,
3511
+ url: "https://ossindex.sonatype.org",
3512
+ ...sonatypeResult.error ? { error: sonatypeResult.error } : {}
3513
+ },
3514
+ ...iqConfigured && iqResult ? {
3515
+ sonatypeiq: {
3516
+ reachable: iqResult.reachable,
3517
+ authenticated: iqResult.authenticated,
3518
+ url: iqCfg.baseUrl,
3519
+ ...iqResult.error ? { error: iqResult.error } : {}
3520
+ }
3521
+ } : {},
3159
3522
  tier,
3160
3523
  capabilities: {
3161
3524
  scan: true,
3162
3525
  diff: true,
3163
3526
  outdated: artResult.reachable && artResult.authenticated,
3164
3527
  licenseCheck: artResult.reachable && artResult.authenticated,
3165
- cveCheck: osvResult.reachable
3528
+ cveCheck: osvResult.reachable || sonatypeResult.reachable || (iqResult?.reachable && iqResult?.authenticated) === true
3166
3529
  }
3167
3530
  };
3168
3531
  }
3169
3532
 
3170
3533
  // src/services/deps/frisk.ts
3171
- async function runFrisk(config, opts) {
3534
+ function toErrorMessage(err) {
3535
+ return err instanceof Error ? err.message : String(err);
3536
+ }
3537
+ async function runFrisk(config, opts, source = "osv", applicationId) {
3172
3538
  const repoRoot = getRepoRoot();
3173
3539
  if (!repoRoot) {
3174
3540
  throw new PncliError("Not inside a git repository.", 1);
3175
3541
  }
3176
- const { tier, osvReachable } = await detectTier(config);
3177
- if (!osvReachable) {
3542
+ if (source === "sonatypeiq") {
3543
+ if (!config.sonatypeiq.baseUrl || !config.sonatypeiq.userCode || !config.sonatypeiq.passcode) {
3544
+ throw new PncliError(
3545
+ "deps frisk --source sonatypeiq requires Sonatype IQ Server to be configured. Run: pncli config init",
3546
+ 1
3547
+ );
3548
+ }
3549
+ if (!applicationId) {
3550
+ throw new PncliError(
3551
+ "deps frisk --source sonatypeiq requires --application-id <id>. Use 'pncli sonatypeiq applications list' to find application IDs.",
3552
+ 1
3553
+ );
3554
+ }
3555
+ }
3556
+ const { tier: baseTier, osvReachable } = await detectTier(config);
3557
+ const sonatypeReachable = source === "sonatype" || source === "all" ? await checkSonatypeReachable() : false;
3558
+ const tier = sonatypeReachable ? "full" : baseTier;
3559
+ if (source === "osv" && !osvReachable) {
3178
3560
  throw new PncliError(
3179
3561
  "deps frisk requires OSV.dev access but api.osv.dev is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
3180
3562
  503
3181
3563
  );
3182
3564
  }
3565
+ if (source === "sonatype" && !sonatypeReachable) {
3566
+ throw new PncliError(
3567
+ "deps frisk --source sonatype requires Sonatype OSS Index access but ossindex.sonatype.org is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
3568
+ 503
3569
+ );
3570
+ }
3571
+ if (source === "all" && !osvReachable && !sonatypeReachable) {
3572
+ throw new PncliError(
3573
+ "deps frisk requires either OSV.dev or Sonatype OSS Index access, but neither is reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
3574
+ 503
3575
+ );
3576
+ }
3577
+ const sourceErrors = [];
3578
+ let sourcesQueried;
3579
+ if (source === "all") {
3580
+ if (!osvReachable) sourceErrors.push({ source: "osv", error: "api.osv.dev is not reachable \u2014 run pncli deps connectivity to diagnose." });
3581
+ if (!sonatypeReachable) sourceErrors.push({ source: "sonatype", error: "ossindex.sonatype.org is not reachable \u2014 run pncli deps connectivity to diagnose." });
3582
+ sourcesQueried = [
3583
+ ...osvReachable ? ["osv"] : [],
3584
+ ...sonatypeReachable ? ["sonatype"] : []
3585
+ ];
3586
+ } else {
3587
+ sourcesQueried = [source];
3588
+ }
3183
3589
  const scanOpts = {
3184
3590
  ...opts,
3185
3591
  includeTransitive: opts.includeTransitive ?? true
3186
3592
  };
3187
3593
  const scan = scanRepo(repoRoot, scanOpts);
3188
3594
  if (scan.packages.length === 0) {
3189
- return { tier, scanned: 0, vulnerable: 0, packages: [] };
3595
+ return { tier, source, sourcesQueried, sourceErrors, scanned: 0, vulnerable: 0, packages: [] };
3596
+ }
3597
+ let vulnerable;
3598
+ if (source === "sonatypeiq") {
3599
+ vulnerable = await checkPackagesViaIqServer(scan.packages, applicationId, config);
3600
+ } else if (source === "sonatype") {
3601
+ vulnerable = await checkPackagesForVulnsViaSonatype(scan.packages);
3602
+ } else if (source === "all") {
3603
+ const [osvSettled, sonatypeSettled] = await Promise.allSettled([
3604
+ osvReachable ? checkPackagesForVulns(scan.packages) : Promise.resolve([]),
3605
+ sonatypeReachable ? checkPackagesForVulnsViaSonatype(scan.packages) : Promise.resolve([])
3606
+ ]);
3607
+ if (osvReachable) {
3608
+ if (osvSettled.status === "rejected") {
3609
+ sourceErrors.push({ source: "osv", error: toErrorMessage(osvSettled.reason) });
3610
+ sourcesQueried = sourcesQueried.filter((s) => s !== "osv");
3611
+ }
3612
+ }
3613
+ if (sonatypeReachable) {
3614
+ if (sonatypeSettled.status === "rejected") {
3615
+ sourceErrors.push({ source: "sonatype", error: toErrorMessage(sonatypeSettled.reason) });
3616
+ sourcesQueried = sourcesQueried.filter((s) => s !== "sonatype");
3617
+ }
3618
+ }
3619
+ if (sourcesQueried.length === 0) {
3620
+ throw new PncliError(
3621
+ "All vulnerability sources failed during scanning. Run 'pncli deps connectivity' to diagnose.",
3622
+ 503
3623
+ );
3624
+ }
3625
+ vulnerable = mergeVulnerablePackages(
3626
+ osvSettled.status === "fulfilled" ? osvSettled.value : [],
3627
+ sonatypeSettled.status === "fulfilled" ? sonatypeSettled.value : []
3628
+ );
3629
+ } else {
3630
+ vulnerable = await checkPackagesForVulns(scan.packages);
3190
3631
  }
3191
- const vulnerable = await checkPackagesForVulns(scan.packages);
3192
3632
  return {
3193
3633
  tier,
3634
+ source,
3635
+ sourcesQueried,
3636
+ sourceErrors,
3194
3637
  scanned: scan.packages.length,
3195
3638
  vulnerable: vulnerable.length,
3196
3639
  packages: vulnerable
3197
3640
  };
3198
3641
  }
3642
+ function mergeVulnerablePackages(osvResults, sonatypeResults) {
3643
+ const byKey = /* @__PURE__ */ new Map();
3644
+ for (const pkg2 of osvResults) {
3645
+ const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}`;
3646
+ byKey.set(key, { ...pkg2 });
3647
+ }
3648
+ for (const pkg2 of sonatypeResults) {
3649
+ const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}`;
3650
+ const existing = byKey.get(key);
3651
+ if (existing) {
3652
+ const existingIds = new Set(existing.vulnerabilities.map((v) => v.id));
3653
+ const newVulns = pkg2.vulnerabilities.filter((v) => !existingIds.has(v.id));
3654
+ existing.vulnerabilities = [...existing.vulnerabilities, ...newVulns];
3655
+ } else {
3656
+ byKey.set(key, { ...pkg2 });
3657
+ }
3658
+ }
3659
+ return [...byKey.values()];
3660
+ }
3199
3661
 
3200
3662
  // src/services/deps/outdated.ts
3201
3663
  async function runOutdated(config, opts, filterType) {
@@ -3270,7 +3732,7 @@ async function runLicenseCheck(config, opts) {
3270
3732
  // src/services/deps/commands.ts
3271
3733
  function registerDepsCommands(program2) {
3272
3734
  const deps = program2.command("deps").description("Dependency scanning, CVE detection, and license auditing");
3273
- deps.command("frisk").description("Scan all dependencies for CVEs and output remediation paths (requires OSV.dev)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--direct-only", "Only scan direct dependencies (default: include transitive)", false).option("--include-dev", "Include dev/test dependencies", false).action(async (opts, cmd) => {
3735
+ deps.command("frisk").description("Scan all dependencies for CVEs and output remediation paths (requires OSV.dev, Sonatype OSS Index, or Sonatype IQ Server)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--direct-only", "Only scan direct dependencies (default: include transitive)", false).option("--include-dev", "Include dev/test dependencies", false).addOption(new Option("--source <source>", "Vulnerability source").choices(["osv", "sonatype", "sonatypeiq", "all"]).default("osv")).option("--application-id <id>", "Sonatype IQ Server application ID (required when --source sonatypeiq)").action(async (opts, cmd) => {
3274
3736
  const startTime = Date.now();
3275
3737
  try {
3276
3738
  const globalOpts = cmd.optsWithGlobals();
@@ -3280,7 +3742,7 @@ function registerDepsCommands(program2) {
3280
3742
  includeTransitive: !opts.directOnly,
3281
3743
  includeDev: opts.includeDev
3282
3744
  };
3283
- const data = await runFrisk(config, scanOpts);
3745
+ const data = await runFrisk(config, scanOpts, opts.source, opts.applicationId);
3284
3746
  success(data, "deps", "frisk", startTime);
3285
3747
  } catch (err) {
3286
3748
  fail(err, "deps", "frisk", startTime);
@@ -3346,7 +3808,7 @@ function registerDepsCommands(program2) {
3346
3808
  fail(err, "deps", "license-check", startTime);
3347
3809
  }
3348
3810
  });
3349
- deps.command("connectivity").description("Test network access to Artifactory and OSV.dev, report available tier").action(async (_opts, cmd) => {
3811
+ deps.command("connectivity").description("Test network access to Artifactory, OSV.dev, and Sonatype OSS Index, report available tier").action(async (_opts, cmd) => {
3350
3812
  const startTime = Date.now();
3351
3813
  try {
3352
3814
  const globalOpts = cmd.optsWithGlobals();
@@ -3652,6 +4114,37 @@ function registerConfigCommands(program2) {
3652
4114
  } else {
3653
4115
  results.checkmarx = { ok: null, message: "not configured" };
3654
4116
  }
4117
+ const snConfigured = cfg.servicenow.baseUrl && cfg.servicenow.username && (cfg.servicenow.password || cfg.servicenow.apiToken);
4118
+ if (snConfigured) {
4119
+ try {
4120
+ await http.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 } });
4121
+ results.servicenow = { ok: true, message: "connected" };
4122
+ } catch (err) {
4123
+ results.servicenow = { ok: false, message: err instanceof Error ? err.message : String(err) };
4124
+ }
4125
+ } else {
4126
+ results.servicenow = { ok: null, message: "not configured" };
4127
+ }
4128
+ if (cfg.contrast.apiKey && cfg.contrast.serviceKey && cfg.contrast.username && cfg.contrast.orgUuid) {
4129
+ try {
4130
+ await http.contrast(`/Contrast/api/ng/${cfg.contrast.orgUuid}/applications`, { params: { limit: 1 } });
4131
+ results.contrast = { ok: true, message: "connected" };
4132
+ } catch (err) {
4133
+ results.contrast = { ok: false, message: err instanceof Error ? err.message : String(err) };
4134
+ }
4135
+ } else {
4136
+ results.contrast = { ok: null, message: "not configured" };
4137
+ }
4138
+ if (cfg.sonatypeiq.baseUrl && cfg.sonatypeiq.userCode && cfg.sonatypeiq.passcode) {
4139
+ try {
4140
+ await http.sonatypeiq("/api/v2/applications", { params: { limit: 1 } });
4141
+ results.sonatypeiq = { ok: true, message: "connected" };
4142
+ } catch (err) {
4143
+ results.sonatypeiq = { ok: false, message: err instanceof Error ? err.message : String(err) };
4144
+ }
4145
+ } else {
4146
+ results.sonatypeiq = { ok: null, message: "not configured" };
4147
+ }
3655
4148
  success(results, "config", "test", start);
3656
4149
  } catch (err) {
3657
4150
  fail(err, "config", "test", start);
@@ -3798,11 +4291,48 @@ function registerConfigCommands(program2) {
3798
4291
  results.checkmarx = categorize2(err);
3799
4292
  }
3800
4293
  }
4294
+ const snCreds = cfg.servicenow.username && (cfg.servicenow.password || cfg.servicenow.apiToken);
4295
+ if (!snCreds) {
4296
+ results.servicenow = { status: "blank", message: "not configured" };
4297
+ } else if (!cfg.servicenow.baseUrl) {
4298
+ results.servicenow = { status: "error", message: "baseUrl not configured" };
4299
+ } else {
4300
+ try {
4301
+ await http.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 }, timeoutMs: 1e4 });
4302
+ results.servicenow = { status: "valid", message: "ok" };
4303
+ } catch (err) {
4304
+ results.servicenow = categorize2(err);
4305
+ }
4306
+ }
4307
+ if (!cfg.contrast.apiKey || !cfg.contrast.serviceKey || !cfg.contrast.username) {
4308
+ results.contrast = { status: "blank", message: "not configured" };
4309
+ } else if (!cfg.contrast.orgUuid) {
4310
+ results.contrast = { status: "error", message: "orgUuid not configured" };
4311
+ } else {
4312
+ try {
4313
+ await http.contrast(`/Contrast/api/ng/${cfg.contrast.orgUuid}/applications`, { params: { limit: 1 }, timeoutMs: 1e4 });
4314
+ results.contrast = { status: "valid", message: "ok" };
4315
+ } catch (err) {
4316
+ results.contrast = categorize2(err);
4317
+ }
4318
+ }
4319
+ if (!cfg.sonatypeiq.userCode || !cfg.sonatypeiq.passcode) {
4320
+ results.sonatypeiq = { status: "blank", message: "not configured" };
4321
+ } else if (!cfg.sonatypeiq.baseUrl) {
4322
+ results.sonatypeiq = { status: "error", message: "baseUrl not configured" };
4323
+ } else {
4324
+ try {
4325
+ await http.sonatypeiq("/api/v2/applications", { params: { limit: 1 }, timeoutMs: 1e4 });
4326
+ results.sonatypeiq = { status: "valid", message: "ok" };
4327
+ } catch (err) {
4328
+ results.sonatypeiq = categorize2(err);
4329
+ }
4330
+ }
3801
4331
  const statuses = Object.values(results).map((r) => r.status);
3802
4332
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
3803
4333
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
3804
4334
  if (cmdOpts.output === "table") {
3805
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx"];
4335
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
3806
4336
  const labelWidth = 14;
3807
4337
  const statusWidth = 9;
3808
4338
  for (const svc of services) {
@@ -3816,7 +4346,7 @@ function registerConfigCommands(program2) {
3816
4346
  process.stdout.write("\n");
3817
4347
  } else {
3818
4348
  if (opts.pretty) {
3819
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx"];
4349
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
3820
4350
  const labelWidth = 14;
3821
4351
  const statusWidth = 9;
3822
4352
  for (const svc of services) {
@@ -4149,6 +4679,155 @@ async function initGlobalConfig(start) {
4149
4679
  }
4150
4680
  }
4151
4681
  }
4682
+ process.stderr.write("\n\u2500\u2500 ServiceNow \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");
4683
+ const useServiceNow = await confirm({
4684
+ message: "Configure ServiceNow for change management?",
4685
+ default: false
4686
+ });
4687
+ let servicenowBaseUrl = "";
4688
+ let servicenowUsername = "";
4689
+ let servicenowPassword = "";
4690
+ let servicenowApiToken = "";
4691
+ if (useServiceNow) {
4692
+ servicenowBaseUrl = await input({
4693
+ message: "ServiceNow instance URL (e.g. https://mycompany.service-now.com):",
4694
+ default: ""
4695
+ });
4696
+ servicenowUsername = await input({
4697
+ message: "ServiceNow username:",
4698
+ default: ""
4699
+ });
4700
+ const snUseToken = await confirm({
4701
+ message: "Authenticate with an API token instead of a password?",
4702
+ default: false
4703
+ });
4704
+ if (snUseToken) {
4705
+ servicenowApiToken = await password({
4706
+ message: "ServiceNow API token:",
4707
+ validate: (v) => v.length > 0 || "Token cannot be blank"
4708
+ });
4709
+ } else {
4710
+ servicenowPassword = await password({
4711
+ message: "ServiceNow password:",
4712
+ validate: (v) => v.length > 0 || "Password cannot be blank"
4713
+ });
4714
+ }
4715
+ const snHasCreds = servicenowBaseUrl && servicenowUsername && (servicenowPassword || servicenowApiToken);
4716
+ if (snHasCreds) {
4717
+ process.stderr.write("\n Verifying connection...\n");
4718
+ try {
4719
+ const tempConfig = {
4720
+ ...loadConfig(),
4721
+ servicenow: {
4722
+ baseUrl: servicenowBaseUrl,
4723
+ username: servicenowUsername,
4724
+ password: servicenowPassword || void 0,
4725
+ apiToken: servicenowApiToken || void 0
4726
+ }
4727
+ };
4728
+ const tempHttp = createHttpClient(tempConfig);
4729
+ await tempHttp.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 } });
4730
+ process.stderr.write(" Connected.\n");
4731
+ } catch (err) {
4732
+ warn(`Could not connect to ServiceNow: ${err instanceof Error ? err.message : String(err)}`);
4733
+ warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
4734
+ }
4735
+ }
4736
+ }
4737
+ process.stderr.write("\n\u2500\u2500 Sonatype IQ Server \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");
4738
+ const useSonatypeIq = await confirm({
4739
+ message: "Configure Sonatype IQ Server for dependency vulnerability scanning?",
4740
+ default: false
4741
+ });
4742
+ let sonatypeIqBaseUrl = "";
4743
+ let sonatypeIqUserCode = "";
4744
+ let sonatypeIqPasscode = "";
4745
+ if (useSonatypeIq) {
4746
+ process.stderr.write(" User Token credentials are found in your IQ Server profile under User Token.\n");
4747
+ sonatypeIqBaseUrl = await input({
4748
+ message: "Sonatype IQ Server base URL (e.g. https://iq.your-company.com):",
4749
+ default: ""
4750
+ });
4751
+ sonatypeIqUserCode = await input({
4752
+ message: "User Code (from User Token):",
4753
+ default: ""
4754
+ });
4755
+ sonatypeIqPasscode = await password({
4756
+ message: "Passcode (from User Token):"
4757
+ });
4758
+ if (sonatypeIqBaseUrl && sonatypeIqUserCode && sonatypeIqPasscode) {
4759
+ process.stderr.write("\n Verifying connection...\n");
4760
+ try {
4761
+ const tempConfig = {
4762
+ ...loadConfig(),
4763
+ sonatypeiq: {
4764
+ baseUrl: sonatypeIqBaseUrl,
4765
+ userCode: sonatypeIqUserCode,
4766
+ passcode: sonatypeIqPasscode
4767
+ }
4768
+ };
4769
+ const tempHttp = createHttpClient(tempConfig);
4770
+ await tempHttp.sonatypeiq("/api/v2/applications", { params: { limit: 1 } });
4771
+ process.stderr.write(" Connected.\n");
4772
+ } catch (err) {
4773
+ warn(`Could not connect to Sonatype IQ Server: ${err instanceof Error ? err.message : String(err)}`);
4774
+ warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
4775
+ }
4776
+ }
4777
+ }
4778
+ process.stderr.write("\n\u2500\u2500 Contrast IAST \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");
4779
+ const useContrast = await confirm({
4780
+ message: "Configure Contrast Security IAST for vulnerability findings?",
4781
+ default: false
4782
+ });
4783
+ let contrastBaseUrl = "";
4784
+ let contrastOrgUuid = "";
4785
+ let contrastUsername = "";
4786
+ let contrastApiKey = "";
4787
+ let contrastServiceKey = "";
4788
+ if (useContrast) {
4789
+ process.stderr.write(" Find your credentials in Contrast under User Settings \u2192 Your Keys.\n");
4790
+ contrastBaseUrl = await input({
4791
+ message: "Contrast base URL (leave blank for cloud: https://app.contrastsecurity.com):",
4792
+ default: ""
4793
+ });
4794
+ contrastOrgUuid = await input({
4795
+ message: "Organization UUID:",
4796
+ default: ""
4797
+ });
4798
+ contrastUsername = await input({
4799
+ message: "Username (email):",
4800
+ default: ""
4801
+ });
4802
+ contrastApiKey = await password({
4803
+ message: "API key:"
4804
+ });
4805
+ contrastServiceKey = await password({
4806
+ message: "Service key:"
4807
+ });
4808
+ const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
4809
+ if (contrastHasCreds) {
4810
+ process.stderr.write("\n Verifying connection...\n");
4811
+ try {
4812
+ const tempConfig = {
4813
+ ...loadConfig(),
4814
+ contrast: {
4815
+ baseUrl: contrastBaseUrl || void 0,
4816
+ orgUuid: contrastOrgUuid,
4817
+ username: contrastUsername,
4818
+ apiKey: contrastApiKey,
4819
+ serviceKey: contrastServiceKey
4820
+ }
4821
+ };
4822
+ const tempHttp = createHttpClient(tempConfig);
4823
+ await tempHttp.contrast(`/Contrast/api/ng/${contrastOrgUuid}/applications`, { params: { limit: 1 } });
4824
+ process.stderr.write(" Connected.\n");
4825
+ } catch (err) {
4826
+ warn(`Could not connect to Contrast: ${err instanceof Error ? err.message : String(err)}`);
4827
+ warn("Config will be saved anyway. Check your credentials and re-run pncli config init or pncli config test.");
4828
+ }
4829
+ }
4830
+ }
4152
4831
  process.stderr.write("\n\u2500\u2500 Defaults \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");
4153
4832
  const jiraProject = await input({
4154
4833
  message: "Default Jira project key (optional):",
@@ -4242,6 +4921,30 @@ async function initGlobalConfig(start) {
4242
4921
  password: checkmarxPassword || void 0
4243
4922
  }
4244
4923
  } : {},
4924
+ ...useServiceNow && servicenowBaseUrl ? {
4925
+ servicenow: {
4926
+ baseUrl: servicenowBaseUrl,
4927
+ username: servicenowUsername || void 0,
4928
+ ...servicenowApiToken ? { apiToken: servicenowApiToken } : {},
4929
+ ...servicenowPassword ? { password: servicenowPassword } : {}
4930
+ }
4931
+ } : {},
4932
+ ...useContrast && contrastOrgUuid ? {
4933
+ contrast: {
4934
+ ...contrastBaseUrl ? { baseUrl: contrastBaseUrl } : {},
4935
+ orgUuid: contrastOrgUuid,
4936
+ username: contrastUsername || void 0,
4937
+ apiKey: contrastApiKey || void 0,
4938
+ serviceKey: contrastServiceKey || void 0
4939
+ }
4940
+ } : {},
4941
+ ...useSonatypeIq && sonatypeIqBaseUrl ? {
4942
+ sonatypeiq: {
4943
+ baseUrl: sonatypeIqBaseUrl,
4944
+ userCode: sonatypeIqUserCode || void 0,
4945
+ passcode: sonatypeIqPasscode || void 0
4946
+ }
4947
+ } : {},
4245
4948
  defaults: {
4246
4949
  jira: {
4247
4950
  project: jiraProject || void 0
@@ -4697,7 +5400,7 @@ function registerAdoWorkCommands(ado) {
4697
5400
  fail(err, "ado", "work-get", start);
4698
5401
  }
4699
5402
  });
4700
- work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
5403
+ work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--parent <id>", "Parent work item ID \u2014 creates a parent link after creation").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
4701
5404
  acc.push(v);
4702
5405
  return acc;
4703
5406
  }, []).action(async (opts) => {
@@ -4719,6 +5422,17 @@ function registerAdoWorkCommands(ado) {
4719
5422
  ];
4720
5423
  const data = await workClient.createWorkItem(collection, project, opts.type, patch);
4721
5424
  success(data, "ado", "work-create", start);
5425
+ if (opts.parent) {
5426
+ try {
5427
+ const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
5428
+ const targetUrl = `${baseUrl}/${encodeURIComponent(collection)}/_apis/wit/workitems/${opts.parent}`;
5429
+ const linkPatch = [{ op: "add", path: "/relations/-", value: { rel: "System.LinkTypes.Hierarchy-Reverse", url: targetUrl } }];
5430
+ await workClient.updateWorkItem(collection, data.id, linkPatch);
5431
+ } catch (linkErr) {
5432
+ warn(`Created #${data.id} but failed to link to parent ${opts.parent}: ${linkErr instanceof Error ? linkErr.message : linkErr}`);
5433
+ }
5434
+ }
5435
+ return;
4722
5436
  } catch (err) {
4723
5437
  fail(err, "ado", "work-create", start);
4724
5438
  }
@@ -5135,11 +5849,11 @@ function registerAdoRepoCommands(ado) {
5135
5849
  const start = Date.now();
5136
5850
  try {
5137
5851
  const globalOpts = ado.optsWithGlobals();
5138
- const { loadConfig: lc } = await import("./config-PXL632ZX.js");
5852
+ const { loadConfig: lc } = await import("./config-NCI2BTQG.js");
5139
5853
  const config = lc({ configPath: globalOpts.config });
5140
5854
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
5141
5855
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
5142
- const { createHttpClient: createHttpClient2 } = await import("./http-TID3W235.js");
5856
+ const { createHttpClient: createHttpClient2 } = await import("./http-L2P23H2D.js");
5143
5857
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
5144
5858
  const coreClient = new AdoCoreClient2(http);
5145
5859
  const me = await coreClient.getConnectionData(collection);
@@ -5751,7 +6465,7 @@ var UdeployClient = class {
5751
6465
  };
5752
6466
 
5753
6467
  // src/services/udeploy/commands.ts
5754
- var POLL_INTERVAL_MS = 5e3;
6468
+ var POLL_INTERVAL_MS2 = 5e3;
5755
6469
  var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
5756
6470
  function getClient8(program2) {
5757
6471
  const opts = program2.optsWithGlobals();
@@ -5862,7 +6576,7 @@ function registerUdeployCommands(program2) {
5862
6576
  if (elapsed >= timeoutMs) {
5863
6577
  throw new PncliError(`Timed out after ${timeoutMs}ms waiting for request ${requestId}`, 1);
5864
6578
  }
5865
- await sleep(POLL_INTERVAL_MS);
6579
+ await sleep(POLL_INTERVAL_MS2);
5866
6580
  const status = await client.getRequestStatus(requestId);
5867
6581
  if (status.status === "CLOSED") {
5868
6582
  const outcome = { requestId, status: status.status, result: status.result, elapsed_ms: Date.now() - start };
@@ -6007,19 +6721,261 @@ function registerCheckmarxCommands(program2) {
6007
6721
  });
6008
6722
  }
6009
6723
 
6724
+ // src/services/servicenow/commands.ts
6725
+ function getHttp(program2) {
6726
+ const opts = program2.optsWithGlobals();
6727
+ const config = loadConfig({ configPath: opts.config });
6728
+ if (!config.servicenow.baseUrl) throw new PncliError("ServiceNow not configured. Run: pncli config init");
6729
+ return createHttpClient(config, Boolean(opts.dryRun));
6730
+ }
6731
+ function registerServiceNowCommands(program2) {
6732
+ const sn = program2.command("servicenow").description("ServiceNow operations");
6733
+ const change = sn.command("change").description("Change request operations");
6734
+ change.command("list").description("List change requests").option("--state <state>", "Filter by state (e.g. -1=Pending, 1=Open, 2=Work in Progress, 3=Closed Complete)").option("--assigned-to <user>", "Filter by assigned user sys_id or display name").option("--limit <n>", "Maximum number of results", "25").option("--fields <fields>", "Comma-separated field names to return").action(async (opts) => {
6735
+ const start = Date.now();
6736
+ try {
6737
+ const http = getHttp(program2);
6738
+ const params = {
6739
+ sysparm_limit: opts.limit ? parseInt(opts.limit, 10) : 25,
6740
+ sysparm_display_value: "true"
6741
+ };
6742
+ const sanitize = (v) => v.replace(/\^/g, "");
6743
+ const queries = [];
6744
+ if (opts.state !== void 0) queries.push(`state=${sanitize(opts.state)}`);
6745
+ if (opts.assignedTo) queries.push(`assigned_to=${sanitize(opts.assignedTo)}`);
6746
+ if (queries.length) params["sysparm_query"] = queries.join("^");
6747
+ if (opts.fields) params["sysparm_fields"] = opts.fields;
6748
+ const data = await http.servicenow("/api/now/table/change_request", { params });
6749
+ success(data.result, "servicenow", "change list", start);
6750
+ } catch (err) {
6751
+ fail(err, "servicenow", "change list", start);
6752
+ }
6753
+ });
6754
+ change.command("get").description("Get a change request by sys_id or number").requiredOption("--id <id>", "Change request sys_id or number (e.g. CHG0001234)").action(async (opts) => {
6755
+ const start = Date.now();
6756
+ try {
6757
+ const http = getHttp(program2);
6758
+ if (opts.id.toUpperCase().startsWith("CHG")) {
6759
+ const list = await http.servicenow("/api/now/table/change_request", {
6760
+ params: { sysparm_query: `number=${opts.id}`, sysparm_display_value: "true", sysparm_limit: 1 }
6761
+ });
6762
+ if (!list.result.length) throw new PncliError(`Change request not found: ${opts.id}`, 1);
6763
+ success(list.result[0], "servicenow", "change get", start);
6764
+ return;
6765
+ }
6766
+ const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
6767
+ params: { sysparm_display_value: "true" }
6768
+ });
6769
+ success(data.result, "servicenow", "change get", start);
6770
+ } catch (err) {
6771
+ fail(err, "servicenow", "change get", start);
6772
+ }
6773
+ });
6774
+ change.command("create").description("Create a change request").requiredOption("--short-description <text>", "Short description (title)").option("--description <text>", "Full description").option("--type <type>", "Change type: normal, standard, or emergency", "normal").option("--priority <n>", "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low").option("--assigned-to <user>", "Assigned user sys_id").option("--assignment-group <group>", "Assignment group sys_id").option("--start-date <datetime>", "Planned start date (yyyy-MM-dd HH:mm:ss)").option("--end-date <datetime>", "Planned end date (yyyy-MM-dd HH:mm:ss)").action(async (opts) => {
6775
+ const start = Date.now();
6776
+ try {
6777
+ const http = getHttp(program2);
6778
+ const body = {
6779
+ short_description: opts.shortDescription,
6780
+ type: opts.type ?? "normal"
6781
+ };
6782
+ if (opts.description) body["description"] = opts.description;
6783
+ if (opts.priority) body["priority"] = opts.priority;
6784
+ if (opts.assignedTo) body["assigned_to"] = opts.assignedTo;
6785
+ if (opts.assignmentGroup) body["assignment_group"] = opts.assignmentGroup;
6786
+ if (opts.startDate) body["start_date"] = opts.startDate;
6787
+ if (opts.endDate) body["end_date"] = opts.endDate;
6788
+ const data = await http.servicenow("/api/now/table/change_request", {
6789
+ method: "POST",
6790
+ body
6791
+ });
6792
+ success(data.result, "servicenow", "change create", start);
6793
+ } catch (err) {
6794
+ fail(err, "servicenow", "change create", start);
6795
+ }
6796
+ });
6797
+ change.command("update").description("Update a change request").requiredOption("--id <sys_id>", "Change request sys_id").option("--short-description <text>", "New short description").option("--description <text>", "New description").option("--state <state>", "New state value").option("--priority <n>", "New priority").option("--assigned-to <user>", "New assigned user sys_id").option("--start-date <datetime>", "New planned start date").option("--end-date <datetime>", "New planned end date").action(async (opts) => {
6798
+ const start = Date.now();
6799
+ try {
6800
+ const http = getHttp(program2);
6801
+ const body = {};
6802
+ if (opts.shortDescription) body["short_description"] = opts.shortDescription;
6803
+ if (opts.description) body["description"] = opts.description;
6804
+ if (opts.state !== void 0) body["state"] = opts.state;
6805
+ if (opts.priority) body["priority"] = opts.priority;
6806
+ if (opts.assignedTo) body["assigned_to"] = opts.assignedTo;
6807
+ if (opts.startDate) body["start_date"] = opts.startDate;
6808
+ if (opts.endDate) body["end_date"] = opts.endDate;
6809
+ if (Object.keys(body).length === 0) throw new PncliError("No fields to update. Provide at least one option besides --id.", 1);
6810
+ const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
6811
+ method: "PATCH",
6812
+ body
6813
+ });
6814
+ success(data.result, "servicenow", "change update", start);
6815
+ } catch (err) {
6816
+ fail(err, "servicenow", "change update", start);
6817
+ }
6818
+ });
6819
+ change.command("close").description("Close a change request (state=3)").requiredOption("--id <sys_id>", "Change request sys_id").option("--close-notes <text>", "Closure notes").option("--close-code <code>", "Close code (e.g. successful, unsuccessful)").action(async (opts) => {
6820
+ const start = Date.now();
6821
+ try {
6822
+ const http = getHttp(program2);
6823
+ const body = { state: 3 };
6824
+ if (opts.closeNotes) body["close_notes"] = opts.closeNotes;
6825
+ if (opts.closeCode) body["close_code"] = opts.closeCode;
6826
+ const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
6827
+ method: "PATCH",
6828
+ body
6829
+ });
6830
+ success(data.result, "servicenow", "change close", start);
6831
+ } catch (err) {
6832
+ fail(err, "servicenow", "change close", start);
6833
+ }
6834
+ });
6835
+ }
6836
+
6837
+ // src/services/contrast/commands.ts
6838
+ function getHttpAndOrg(program2) {
6839
+ const opts = program2.optsWithGlobals();
6840
+ const config = loadConfig({ configPath: opts.config });
6841
+ if (!config.contrast.apiKey || !config.contrast.serviceKey || !config.contrast.username) throw new PncliError("Contrast not configured. Run: pncli config init");
6842
+ if (!config.contrast.orgUuid) throw new PncliError("Contrast org UUID not configured. Run: pncli config set contrast.orgUuid <uuid>");
6843
+ const http = createHttpClient(config, Boolean(opts.dryRun));
6844
+ return { http, orgUuid: config.contrast.orgUuid };
6845
+ }
6846
+ function registerContrastCommands(program2) {
6847
+ const contrast = program2.command("contrast").description("Contrast IAST operations");
6848
+ const apps = contrast.command("apps").description("Application operations");
6849
+ apps.command("list").description("List applications in the organization").option("--limit <n>", "Maximum number of results", "25").option("--offset <n>", "Pagination offset", "0").action(async (opts) => {
6850
+ const start = Date.now();
6851
+ try {
6852
+ const { http, orgUuid } = getHttpAndOrg(program2);
6853
+ const data = await http.contrast(`/Contrast/api/ng/${orgUuid}/applications`, {
6854
+ params: {
6855
+ limit: opts.limit ? parseInt(opts.limit, 10) : 25,
6856
+ offset: opts.offset ? parseInt(opts.offset, 10) : 0
6857
+ }
6858
+ });
6859
+ success(data, "contrast", "apps list", start);
6860
+ } catch (err) {
6861
+ fail(err, "contrast", "apps list", start);
6862
+ }
6863
+ });
6864
+ const findings = contrast.command("findings").description("Vulnerability findings operations");
6865
+ findings.command("list").description("List vulnerability findings for an application").requiredOption("--app <app-id>", "Application ID (UUID)").option("--severity <level>", "Filter by severity: CRITICAL, HIGH, MEDIUM, LOW, NOTE").option("--status <status>", "Filter by status: REPORTED, CONFIRMED, SUSPICIOUS, NOT_A_PROBLEM, REMEDIATED, FIXED").option("--limit <n>", "Maximum number of results", "25").option("--offset <n>", "Pagination offset", "0").action(async (opts) => {
6866
+ const start = Date.now();
6867
+ try {
6868
+ const { http, orgUuid } = getHttpAndOrg(program2);
6869
+ const params = {
6870
+ limit: opts.limit ? parseInt(opts.limit, 10) : 25,
6871
+ offset: opts.offset ? parseInt(opts.offset, 10) : 0
6872
+ };
6873
+ if (opts.severity) params["severities"] = opts.severity;
6874
+ if (opts.status) params["statuses"] = opts.status;
6875
+ const data = await http.contrast(
6876
+ `/Contrast/api/ng/${orgUuid}/traces/${opts.app}/filter`,
6877
+ { params }
6878
+ );
6879
+ success(data, "contrast", "findings list", start);
6880
+ } catch (err) {
6881
+ fail(err, "contrast", "findings list", start);
6882
+ }
6883
+ });
6884
+ findings.command("get").description("Get details for a specific vulnerability finding").requiredOption("--app <app-id>", "Application ID (UUID)").requiredOption("--trace <trace-id>", "Trace/finding UUID").action(async (opts) => {
6885
+ const start = Date.now();
6886
+ try {
6887
+ const { http, orgUuid } = getHttpAndOrg(program2);
6888
+ const data = await http.contrast(
6889
+ `/Contrast/api/ng/${orgUuid}/traces/${opts.app}/trace/${opts.trace}`
6890
+ );
6891
+ success(data, "contrast", "findings get", start);
6892
+ } catch (err) {
6893
+ fail(err, "contrast", "findings get", start);
6894
+ }
6895
+ });
6896
+ }
6897
+
6898
+ // src/services/sonatypeiq/commands.ts
6899
+ function getHttpClient(program2) {
6900
+ const opts = program2.optsWithGlobals();
6901
+ const config = loadConfig({ configPath: opts.config });
6902
+ if (!config.sonatypeiq.baseUrl) throw new PncliError("Sonatype IQ Server baseUrl not configured. Run: pncli config init");
6903
+ if (!config.sonatypeiq.userCode || !config.sonatypeiq.passcode) throw new PncliError("Sonatype IQ Server credentials not configured. Run: pncli config init");
6904
+ const http = createHttpClient(config, Boolean(opts.dryRun));
6905
+ return http;
6906
+ }
6907
+ function registerSonatypeIqCommands(program2) {
6908
+ const sonatypeiq = program2.command("sonatypeiq").description("Sonatype IQ Server operations");
6909
+ const applications = sonatypeiq.command("applications").description("Application operations");
6910
+ applications.command("list").description("List all applications in Sonatype IQ Server").option("--organization-id <id>", "Filter by organization ID").action(async (opts) => {
6911
+ const startTime = Date.now();
6912
+ try {
6913
+ const http = getHttpClient(program2);
6914
+ const params = {};
6915
+ if (opts.organizationId) params["organizationId"] = opts.organizationId;
6916
+ const data = await http.sonatypeiq("/api/v2/applications", { params });
6917
+ success(data, "sonatypeiq", "applications list", startTime);
6918
+ } catch (err) {
6919
+ fail(err, "sonatypeiq", "applications list", startTime);
6920
+ }
6921
+ });
6922
+ applications.command("get").description("Get an application by its public ID").requiredOption("--public-id <id>", "Application public ID").action(async (opts) => {
6923
+ const startTime = Date.now();
6924
+ try {
6925
+ const http = getHttpClient(program2);
6926
+ const data = await http.sonatypeiq("/api/v2/applications", {
6927
+ params: { publicId: opts.publicId }
6928
+ });
6929
+ success(data, "sonatypeiq", "applications get", startTime);
6930
+ } catch (err) {
6931
+ fail(err, "sonatypeiq", "applications get", startTime);
6932
+ }
6933
+ });
6934
+ const organizations = sonatypeiq.command("organizations").description("Organization operations");
6935
+ organizations.command("list").description("List all organizations in Sonatype IQ Server").action(async () => {
6936
+ const startTime = Date.now();
6937
+ try {
6938
+ const http = getHttpClient(program2);
6939
+ const data = await http.sonatypeiq("/api/v2/organizations");
6940
+ success(data, "sonatypeiq", "organizations list", startTime);
6941
+ } catch (err) {
6942
+ fail(err, "sonatypeiq", "organizations list", startTime);
6943
+ }
6944
+ });
6945
+ const policies = sonatypeiq.command("policies").description("Policy operations");
6946
+ policies.command("list").description("List all policies").option("--organization-id <id>", "Filter by organization ID").action(async (opts) => {
6947
+ const startTime = Date.now();
6948
+ try {
6949
+ const http = getHttpClient(program2);
6950
+ const path6 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
6951
+ const data = await http.sonatypeiq(path6);
6952
+ success(data, "sonatypeiq", "policies list", startTime);
6953
+ } catch (err) {
6954
+ fail(err, "sonatypeiq", "policies list", startTime);
6955
+ }
6956
+ });
6957
+ }
6958
+
6010
6959
  // src/services/skills/commands.ts
6011
6960
  import fs3 from "fs";
6012
6961
  import path5 from "path";
6013
6962
  import os from "os";
6014
- var GITHUB_API = "https://api.github.com/repos/kolatts/pncli/contents/skills";
6015
- var RAW_BASE = "https://raw.githubusercontent.com/kolatts/pncli/main/skills";
6963
+ import { fileURLToPath } from "url";
6016
6964
  var AGENT_PATHS = {
6017
6965
  "github-copilot": { project: ".agents/skills", user: path5.join(os.homedir(), ".copilot/skills") },
6018
6966
  "claude-code": { project: ".claude/skills", user: path5.join(os.homedir(), ".claude/skills") }
6019
6967
  };
6968
+ function getBundledSkillsDir() {
6969
+ try {
6970
+ const thisFile = fileURLToPath(import.meta.url);
6971
+ return path5.resolve(path5.dirname(thisFile), "..", "skills");
6972
+ } catch {
6973
+ return "";
6974
+ }
6975
+ }
6020
6976
  function registerSkillsCommands(program2) {
6021
6977
  const skills = program2.command("skills").description("Manage pncli Claude Code skills");
6022
- skills.command("install").description("Download latest pncli skills into the current repo").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action(async (opts) => {
6978
+ skills.command("install").description("Install pncli skills into the current repo").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts) => {
6023
6979
  const start = Date.now();
6024
6980
  try {
6025
6981
  let targetDir;
@@ -6033,19 +6989,22 @@ function registerSkillsCommands(program2) {
6033
6989
  const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
6034
6990
  targetDir = path5.resolve(scopePath);
6035
6991
  }
6036
- warn("Fetching skill list from GitHub...");
6037
- const listRes = await fetch(GITHUB_API, {
6038
- headers: { "Accept": "application/vnd.github.v3+json", "User-Agent": "pncli" }
6039
- });
6040
- if (!listRes.ok) {
6041
- throw new Error(`GitHub API returned ${listRes.status}: ${await listRes.text()}`);
6992
+ const resolvedTarget = path5.resolve(targetDir);
6993
+ const bundledDir = getBundledSkillsDir();
6994
+ let skillDirs = [];
6995
+ if (bundledDir !== "") {
6996
+ try {
6997
+ skillDirs = fs3.readdirSync(bundledDir).filter(
6998
+ (name) => fs3.existsSync(path5.join(bundledDir, name, "SKILL.md"))
6999
+ );
7000
+ } catch {
7001
+ }
6042
7002
  }
6043
- const entries = await listRes.json();
6044
- const skillDirs = entries.filter((e) => e.type === "dir").map((e) => e.name);
6045
7003
  if (skillDirs.length === 0) {
6046
- throw new Error("No skills found in the pncli repository");
7004
+ throw new Error("No bundled skills found. Reinstall pncli to get the latest version: npm install -g @kolatts/pncli");
6047
7005
  }
6048
- const resolvedTarget = path5.resolve(targetDir);
7006
+ const installed = [];
7007
+ const failed = [];
6049
7008
  for (const skillName of skillDirs) {
6050
7009
  const existingDir = path5.resolve(targetDir, skillName);
6051
7010
  if (!existingDir.startsWith(resolvedTarget + path5.sep)) continue;
@@ -6053,25 +7012,15 @@ function registerSkillsCommands(program2) {
6053
7012
  fs3.rmSync(existingDir, { recursive: true, force: true });
6054
7013
  }
6055
7014
  }
6056
- warn(`Found ${skillDirs.length} skills. Downloading to ${targetDir}...`);
6057
- const installed = [];
6058
- const failed = [];
7015
+ warn(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
6059
7016
  for (const skillName of skillDirs) {
6060
7017
  const skillDir = path5.resolve(targetDir, skillName);
6061
7018
  if (!skillDir.startsWith(resolvedTarget + path5.sep)) {
6062
7019
  failed.push(skillName);
6063
7020
  continue;
6064
7021
  }
6065
- const skillUrl = `${RAW_BASE}/${skillName}/SKILL.md`;
6066
7022
  try {
6067
- const res = await fetch(skillUrl, {
6068
- headers: { "User-Agent": "pncli" }
6069
- });
6070
- if (!res.ok) {
6071
- failed.push(skillName);
6072
- continue;
6073
- }
6074
- const content = await res.text();
7023
+ const content = fs3.readFileSync(path5.join(bundledDir, skillName, "SKILL.md"), "utf8");
6075
7024
  fs3.mkdirSync(skillDir, { recursive: true });
6076
7025
  fs3.writeFileSync(path5.join(skillDir, "SKILL.md"), content, "utf8");
6077
7026
  installed.push(skillName);
@@ -6081,7 +7030,7 @@ function registerSkillsCommands(program2) {
6081
7030
  }
6082
7031
  warn(`Installed ${installed.length} skill(s) to ${targetDir}`);
6083
7032
  if (failed.length > 0) {
6084
- warn(`Failed to download: ${failed.join(", ")}`);
7033
+ warn(`Failed to install: ${failed.join(", ")}`);
6085
7034
  }
6086
7035
  success({
6087
7036
  installed,
@@ -6089,7 +7038,8 @@ function registerSkillsCommands(program2) {
6089
7038
  target: targetDir,
6090
7039
  total: installed.length,
6091
7040
  agent: opts.target ? "custom" : opts.agent,
6092
- scope: opts.target ? "custom" : opts.scope
7041
+ scope: opts.target ? "custom" : opts.scope,
7042
+ source: "bundled"
6093
7043
  }, "skills", "install", start);
6094
7044
  } catch (err) {
6095
7045
  fail(err, "skills", "install", start);
@@ -6165,7 +7115,7 @@ if (!process.env.PNCLI_VERIFY_TLS) {
6165
7115
  var require2 = createRequire(import.meta.url);
6166
7116
  var pkg = require2("../package.json");
6167
7117
  var TAGLINE = "One command does what three meetings couldn't.";
6168
- var program = new Command();
7118
+ var program = new Command2();
6169
7119
  program.name("pncli").description(`The Paperwork Nightmare CLI \u2014 ${TAGLINE}`).version(`${pkg.version} \u2014 ${TAGLINE}`, "-v, --version").option("--pretty", "Human-readable formatted output", false).option("--verbose", "Include full response metadata", false).option("--dry-run", "Print API requests without executing", false).option("--config <path>", "Override global config file location").option("--output-file <path>", "Write JSON output to file instead of stdout");
6170
7120
  program.hook("preAction", (thisCommand) => {
6171
7121
  const opts = thisCommand.optsWithGlobals();
@@ -6193,6 +7143,9 @@ registerJenkinsCommands(program);
6193
7143
  registerArtifactoryCommands(program);
6194
7144
  registerUdeployCommands(program);
6195
7145
  registerCheckmarxCommands(program);
7146
+ registerServiceNowCommands(program);
7147
+ registerContrastCommands(program);
7148
+ registerSonatypeIqCommands(program);
6196
7149
  registerSkillsCommands(program);
6197
7150
  program.addHelpText("after", `
6198
7151
  Services:
@@ -6208,6 +7161,9 @@ Services:
6208
7161
  artifactory Artifactory (repos, artifact search, build info, properties)
6209
7162
  udeploy IBM UrbanCode Deploy (component versions, deployment processes)
6210
7163
  checkmarx Checkmarx CxSAST (projects, scans, scan statistics)
7164
+ servicenow ServiceNow (change requests)
7165
+ contrast Contrast IAST (applications, vulnerability findings)
7166
+ sonatypeiq Sonatype IQ Server (applications, organizations, policies)
6211
7167
  config Manage pncli configuration
6212
7168
  skills Download and manage Claude Code skills
6213
7169
  `);