@kolatts/pncli 1.18.0 → 1.19.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
@@ -8,7 +8,7 @@ import {
8
8
  setRepoConfigValue,
9
9
  writeGlobalConfig,
10
10
  writeRepoConfig
11
- } from "./chunk-IVUDXPDP.js";
11
+ } from "./chunk-FXY262BR.js";
12
12
  import {
13
13
  ExitCode,
14
14
  PncliError,
@@ -20,7 +20,7 @@ import {
20
20
  success,
21
21
  warn,
22
22
  writeRawOutput
23
- } from "./chunk-WX4ZRHIC.js";
23
+ } from "./chunk-WM5NYVEO.js";
24
24
  import {
25
25
  AdoCoreClient
26
26
  } from "./chunk-VDL32PSG.js";
@@ -683,9 +683,9 @@ var BitbucketClient = class {
683
683
  async getDiff(project, repo, prId, file, contextLines) {
684
684
  const params = {};
685
685
  if (contextLines !== void 0) params.contextLines = contextLines;
686
- if (file) params.path = file;
686
+ const diffPath = file ? `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff/${file}` : `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff`;
687
687
  const result = await this.http.bitbucket(
688
- `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/diff`,
688
+ diffPath,
689
689
  { params }
690
690
  );
691
691
  return typeof result === "string" ? result : JSON.stringify(result);
@@ -5639,9 +5639,9 @@ function registerConfigCommands(program2) {
5639
5639
  } else {
5640
5640
  results.udeploy = { ok: null, message: "not configured" };
5641
5641
  }
5642
- if (cfg.checkmarx.baseUrl && cfg.checkmarx.username && cfg.checkmarx.password) {
5642
+ if (cfg.checkmarx.baseUrl && cfg.checkmarx.tenantName && (cfg.checkmarx.apiKey || cfg.checkmarx.clientId && cfg.checkmarx.clientSecret)) {
5643
5643
  try {
5644
- await http.checkmarx("/cxrestapi/projects");
5644
+ await http.checkmarx("/api/projects", { params: { limit: 1 } });
5645
5645
  results.checkmarx = { ok: true, message: "connected" };
5646
5646
  } catch (err) {
5647
5647
  results.checkmarx = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -5690,6 +5690,37 @@ function registerConfigCommands(program2) {
5690
5690
  } else {
5691
5691
  results.openshift = { ok: null, message: "not configured" };
5692
5692
  }
5693
+ if (cfg.dynatrace.baseUrl && cfg.dynatrace.apiToken) {
5694
+ try {
5695
+ await http.dynatrace("/api/v2/entities", {
5696
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
5697
+ });
5698
+ results.dynatrace = { ok: true, message: "connected" };
5699
+ } catch (err) {
5700
+ results.dynatrace = { ok: false, message: err instanceof Error ? err.message : String(err) };
5701
+ }
5702
+ } else {
5703
+ results.dynatrace = { ok: null, message: "not configured" };
5704
+ }
5705
+ if (cfg.dynatrace.platformUrl && cfg.dynatrace.platformToken) {
5706
+ try {
5707
+ await http.dynatracePlatform("/platform/storage/query/v1/query:execute", {
5708
+ method: "POST",
5709
+ body: {
5710
+ query: "fetch spans | limit 1",
5711
+ requestTimeoutMilliseconds: 5e3
5712
+ },
5713
+ timeoutMs: 1e4
5714
+ });
5715
+ results.dynatrace_platform = { ok: true, message: "connected" };
5716
+ } catch (err) {
5717
+ results.dynatrace_platform = { ok: false, message: err instanceof Error ? err.message : String(err) };
5718
+ }
5719
+ } else if (cfg.dynatrace.platformUrl || cfg.dynatrace.platformToken) {
5720
+ results.dynatrace_platform = { ok: false, message: "platformUrl and platformToken must both be configured" };
5721
+ } else {
5722
+ results.dynatrace_platform = { ok: null, message: "not configured" };
5723
+ }
5693
5724
  success(results, "config", "test", start);
5694
5725
  } catch (err) {
5695
5726
  fail(err, "config", "test", start);
@@ -5836,13 +5867,13 @@ function registerConfigCommands(program2) {
5836
5867
  } else {
5837
5868
  results.artifactory = { status: "valid", message: "ok" };
5838
5869
  }
5839
- if (!cfg.checkmarx.username || !cfg.checkmarx.password) {
5870
+ if (!cfg.checkmarx.tenantName || !cfg.checkmarx.apiKey && (!cfg.checkmarx.clientId || !cfg.checkmarx.clientSecret)) {
5840
5871
  results.checkmarx = { status: "blank", message: "not configured" };
5841
5872
  } else if (!cfg.checkmarx.baseUrl) {
5842
5873
  results.checkmarx = { status: "error", message: "baseUrl not configured" };
5843
5874
  } else {
5844
5875
  try {
5845
- await http.checkmarx("/cxrestapi/projects", { timeoutMs: 1e4 });
5876
+ await http.checkmarx("/api/projects", { params: { limit: 1 }, timeoutMs: 1e4 });
5846
5877
  results.checkmarx = { status: "valid", message: "ok" };
5847
5878
  } catch (err) {
5848
5879
  results.checkmarx = categorize2(err);
@@ -5897,11 +5928,48 @@ function registerConfigCommands(program2) {
5897
5928
  results.openshift = categorize2(err);
5898
5929
  }
5899
5930
  }
5931
+ if (!cfg.dynatrace.apiToken) {
5932
+ results.dynatrace = { status: "blank", message: "not configured" };
5933
+ } else if (!cfg.dynatrace.baseUrl) {
5934
+ results.dynatrace = { status: "error", message: "baseUrl not configured" };
5935
+ } else {
5936
+ try {
5937
+ await http.dynatrace("/api/v2/entities", {
5938
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 },
5939
+ timeoutMs: 1e4
5940
+ });
5941
+ results.dynatrace = { status: "valid", message: "ok" };
5942
+ } catch (err) {
5943
+ results.dynatrace = categorize2(err);
5944
+ }
5945
+ }
5946
+ if (!cfg.dynatrace.platformUrl && !cfg.dynatrace.platformToken) {
5947
+ results.dynatrace_platform = { status: "blank", message: "not configured" };
5948
+ } else if (!cfg.dynatrace.platformUrl || !cfg.dynatrace.platformToken) {
5949
+ results.dynatrace_platform = {
5950
+ status: "error",
5951
+ message: "platformUrl and platformToken must both be configured"
5952
+ };
5953
+ } else {
5954
+ try {
5955
+ await http.dynatracePlatform("/platform/storage/query/v1/query:execute", {
5956
+ method: "POST",
5957
+ body: {
5958
+ query: "fetch spans | limit 1",
5959
+ requestTimeoutMilliseconds: 5e3
5960
+ },
5961
+ timeoutMs: 1e4
5962
+ });
5963
+ results.dynatrace_platform = { status: "valid", message: "ok" };
5964
+ } catch (err) {
5965
+ results.dynatrace_platform = categorize2(err);
5966
+ }
5967
+ }
5900
5968
  const statuses = Object.values(results).map((r) => r.status);
5901
5969
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
5902
5970
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
5903
5971
  if (cmdOpts.output === "table") {
5904
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
5972
+ const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform"];
5905
5973
  const labelWidth = 14;
5906
5974
  const statusWidth = 9;
5907
5975
  for (const svc of services) {
@@ -5915,7 +5983,7 @@ function registerConfigCommands(program2) {
5915
5983
  process.stdout.write("\n");
5916
5984
  } else {
5917
5985
  if (opts.pretty) {
5918
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
5986
+ const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform"];
5919
5987
  const labelWidth = 14;
5920
5988
  const statusWidth = 9;
5921
5989
  for (const svc of services) {
@@ -6248,38 +6316,51 @@ async function initGlobalConfig(start) {
6248
6316
  }
6249
6317
  process.stderr.write("\n\u2500\u2500 Checkmarx \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");
6250
6318
  const useCheckmarx = await confirm({
6251
- message: "Configure Checkmarx CxSAST for vulnerability scanning?",
6319
+ message: "Configure Checkmarx One for vulnerability scanning?",
6252
6320
  default: false
6253
6321
  });
6254
6322
  let checkmarxBaseUrl = "";
6255
- let checkmarxUsername = "";
6256
- let checkmarxPassword = "";
6323
+ let checkmarxTenantName = "";
6324
+ let checkmarxApiKey = "";
6325
+ let checkmarxClientId = "";
6326
+ let checkmarxClientSecret = "";
6257
6327
  if (useCheckmarx) {
6258
6328
  checkmarxBaseUrl = await input({
6259
- message: "Checkmarx base URL (e.g. cx.company.com):",
6329
+ message: "Checkmarx One API base URL (e.g. https://ast.checkmarx.net):",
6260
6330
  default: ""
6261
6331
  });
6262
- checkmarxUsername = await input({
6263
- message: "Checkmarx username:",
6332
+ checkmarxTenantName = await input({
6333
+ message: "Checkmarx One tenant name (IAM realm, e.g. mycompany):",
6264
6334
  default: ""
6265
6335
  });
6266
- checkmarxPassword = await password({
6267
- message: "Checkmarx password:",
6268
- validate: (v) => v.length > 0 || "Password cannot be blank"
6336
+ checkmarxApiKey = await password({
6337
+ message: "Checkmarx One API key (leave blank to use an OAuth client):"
6269
6338
  });
6270
- if (checkmarxBaseUrl && checkmarxUsername && checkmarxPassword) {
6339
+ if (!checkmarxApiKey) {
6340
+ checkmarxClientId = await input({
6341
+ message: "Checkmarx One OAuth client ID:",
6342
+ default: ""
6343
+ });
6344
+ checkmarxClientSecret = await password({
6345
+ message: "Checkmarx One OAuth client secret:",
6346
+ validate: (v) => v.length > 0 || "Client secret cannot be blank"
6347
+ });
6348
+ }
6349
+ if (checkmarxBaseUrl && checkmarxTenantName && (checkmarxApiKey || checkmarxClientId && checkmarxClientSecret)) {
6271
6350
  process.stderr.write("\n Verifying connection...\n");
6272
6351
  try {
6273
6352
  const tempConfig = {
6274
6353
  ...loadConfig(),
6275
6354
  checkmarx: {
6276
6355
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
6277
- username: checkmarxUsername,
6278
- password: checkmarxPassword
6356
+ tenantName: checkmarxTenantName,
6357
+ apiKey: checkmarxApiKey || void 0,
6358
+ clientId: checkmarxClientId,
6359
+ clientSecret: checkmarxClientSecret
6279
6360
  }
6280
6361
  };
6281
6362
  const tempHttp = createHttpClient(tempConfig);
6282
- await tempHttp.checkmarx("/cxrestapi/projects");
6363
+ await tempHttp.checkmarx("/api/projects", { params: { limit: 1 } });
6283
6364
  process.stderr.write(" Connected.\n");
6284
6365
  } catch (err) {
6285
6366
  warn(`Could not connect to Checkmarx: ${err instanceof Error ? err.message : String(err)}`);
@@ -6472,6 +6553,59 @@ async function initGlobalConfig(start) {
6472
6553
  }
6473
6554
  }
6474
6555
  }
6556
+ process.stderr.write("\n\u2500\u2500 Dynatrace \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");
6557
+ const useDynatrace = await confirm({
6558
+ message: "Configure Dynatrace for services, problems, workloads, and traces?",
6559
+ default: false
6560
+ });
6561
+ let dynatraceBaseUrl = "";
6562
+ let dynatraceApiToken = "";
6563
+ let dynatracePlatformUrl = "";
6564
+ let dynatracePlatformToken = "";
6565
+ if (useDynatrace) {
6566
+ dynatraceBaseUrl = await input({
6567
+ message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
6568
+ default: ""
6569
+ });
6570
+ dynatraceApiToken = await password({
6571
+ message: "Environment API token (entities.read and problems.read):"
6572
+ });
6573
+ const configureTraces = await confirm({
6574
+ message: "Configure the latest Dynatrace platform for Grail trace queries?",
6575
+ default: false
6576
+ });
6577
+ if (configureTraces) {
6578
+ dynatracePlatformUrl = await input({
6579
+ message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
6580
+ default: ""
6581
+ });
6582
+ dynatracePlatformToken = await password({
6583
+ message: "Platform token with Grail span read permissions:"
6584
+ });
6585
+ }
6586
+ if (dynatraceBaseUrl && dynatraceApiToken) {
6587
+ process.stderr.write("\n Verifying connection...\n");
6588
+ try {
6589
+ const tempConfig = {
6590
+ ...loadConfig(),
6591
+ dynatrace: {
6592
+ baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
6593
+ apiToken: dynatraceApiToken,
6594
+ platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
6595
+ platformToken: dynatracePlatformToken || void 0
6596
+ }
6597
+ };
6598
+ const tempHttp = createHttpClient(tempConfig);
6599
+ await tempHttp.dynatrace("/api/v2/entities", {
6600
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
6601
+ });
6602
+ process.stderr.write(" Connected.\n");
6603
+ } catch (err) {
6604
+ warn(`Could not connect to Dynatrace: ${err instanceof Error ? err.message : String(err)}`);
6605
+ warn("Config will be saved anyway. Check your URLs and tokens and re-run pncli config test.");
6606
+ }
6607
+ }
6608
+ }
6475
6609
  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");
6476
6610
  const jiraProject = await input({
6477
6611
  message: "Default Jira project key (optional):",
@@ -6567,8 +6701,10 @@ async function initGlobalConfig(start) {
6567
6701
  ...useCheckmarx && checkmarxBaseUrl ? {
6568
6702
  checkmarx: {
6569
6703
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
6570
- username: checkmarxUsername || void 0,
6571
- password: checkmarxPassword || void 0
6704
+ tenantName: checkmarxTenantName || void 0,
6705
+ apiKey: checkmarxApiKey || void 0,
6706
+ clientId: checkmarxClientId || void 0,
6707
+ clientSecret: checkmarxClientSecret || void 0
6572
6708
  }
6573
6709
  } : {},
6574
6710
  ...useServiceNow && servicenowBaseUrl ? {
@@ -6601,6 +6737,14 @@ async function initGlobalConfig(start) {
6601
6737
  token: openShiftToken || void 0
6602
6738
  }
6603
6739
  } : {},
6740
+ ...useDynatrace && dynatraceBaseUrl ? {
6741
+ dynatrace: {
6742
+ baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
6743
+ apiToken: dynatraceApiToken || void 0,
6744
+ platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
6745
+ platformToken: dynatracePlatformToken || void 0
6746
+ }
6747
+ } : {},
6604
6748
  defaults: {
6605
6749
  jira: {
6606
6750
  project: jiraProject || void 0
@@ -7688,11 +7832,11 @@ function registerAdoRepoCommands(ado) {
7688
7832
  const start = Date.now();
7689
7833
  try {
7690
7834
  const globalOpts = ado.optsWithGlobals();
7691
- const { loadConfig: lc } = await import("./config-QN7KDHE4.js");
7835
+ const { loadConfig: lc } = await import("./config-CPRXY5SN.js");
7692
7836
  const config = lc({ configPath: globalOpts.config });
7693
7837
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
7694
7838
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
7695
- const { createHttpClient: createHttpClient2 } = await import("./http-DDY5DLF6.js");
7839
+ const { createHttpClient: createHttpClient2 } = await import("./http-FNRE7NQU.js");
7696
7840
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
7697
7841
  const coreClient = new AdoCoreClient2(http);
7698
7842
  const me = await coreClient.getConnectionData(collection);
@@ -8506,24 +8650,35 @@ var CheckmarxClient = class {
8506
8650
  }
8507
8651
  http;
8508
8652
  async listProjects() {
8509
- return this.http.checkmarx("/cxrestapi/projects");
8653
+ const res = await this.http.checkmarx("projects", { params: { limit: 100 } });
8654
+ const projects = res.projects ?? [];
8655
+ if (res.filteredTotalCount > projects.length) {
8656
+ process.stderr.write(`warning: ${res.filteredTotalCount} projects found; only showing first ${projects.length}
8657
+ `);
8658
+ }
8659
+ return projects;
8510
8660
  }
8511
8661
  async getProject(id) {
8512
- return this.http.checkmarx(`/cxrestapi/projects/${id}`);
8662
+ return this.http.checkmarx(`projects/${id}`);
8513
8663
  }
8514
8664
  async listScans(opts = {}) {
8515
- return this.http.checkmarx("/cxrestapi/sast/scans", {
8516
- params: {
8517
- projectId: opts.projectId,
8518
- last: opts.last
8519
- }
8520
- });
8665
+ const params = { limit: opts.last ?? 100 };
8666
+ if (opts.projectId) params["project-id"] = opts.projectId;
8667
+ const res = await this.http.checkmarx("scans", { params });
8668
+ const scans = res.scans ?? [];
8669
+ if (res.filteredTotalCount > scans.length) {
8670
+ process.stderr.write(`warning: ${res.filteredTotalCount} scans found; only showing first ${scans.length}
8671
+ `);
8672
+ }
8673
+ return scans;
8521
8674
  }
8522
8675
  async getScan(id) {
8523
- return this.http.checkmarx(`/cxrestapi/sast/scans/${id}`);
8676
+ return this.http.checkmarx(`scans/${id}`);
8524
8677
  }
8525
8678
  async getScanResultsStatistics(scanId) {
8526
- return this.http.checkmarx(`/cxrestapi/sast/scans/${scanId}/resultsStatistics`);
8679
+ return this.http.checkmarx("results/summary", {
8680
+ params: { "scan-id": scanId }
8681
+ });
8527
8682
  }
8528
8683
  };
8529
8684
 
@@ -8536,7 +8691,7 @@ function getClient10(program2) {
8536
8691
  return new CheckmarxClient(http);
8537
8692
  }
8538
8693
  function registerCheckmarxCommands(program2) {
8539
- const cx = program2.command("checkmarx").description("Checkmarx CxSAST operations");
8694
+ const cx = program2.command("checkmarx").description("Checkmarx One operations");
8540
8695
  const project = cx.command("project").description("Project operations");
8541
8696
  const scan = cx.command("scan").description("Scan operations");
8542
8697
  project.command("list").description("List all projects").action(async () => {
@@ -8548,47 +8703,39 @@ function registerCheckmarxCommands(program2) {
8548
8703
  fail(err, "checkmarx", "project-list", start);
8549
8704
  }
8550
8705
  });
8551
- project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project ID").action(async (opts) => {
8706
+ project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project UUID").action(async (opts) => {
8552
8707
  const start = Date.now();
8553
8708
  try {
8554
- const id = parseInt(opts.id, 10);
8555
- if (isNaN(id)) throw new PncliError(`Invalid --id "${opts.id}". Expected an integer.`, 1);
8556
- const data = await getClient10(program2).getProject(id);
8709
+ const data = await getClient10(program2).getProject(opts.id);
8557
8710
  success(data, "checkmarx", "project-get", start);
8558
8711
  } catch (err) {
8559
8712
  fail(err, "checkmarx", "project-get", start);
8560
8713
  }
8561
8714
  });
8562
- scan.command("list").description("List scans").option("--project <id>", "Filter by project ID").option("--last <n>", "Return only the last N scans per project").action(async (opts) => {
8715
+ scan.command("list").description("List scans").option("--project <id>", "Filter by project UUID").option("--last <n>", "Return only the last N scans").action(async (opts) => {
8563
8716
  const start = Date.now();
8564
8717
  try {
8565
- const projectId = opts.project ? parseInt(opts.project, 10) : void 0;
8566
8718
  const last = opts.last ? parseInt(opts.last, 10) : void 0;
8567
- if (opts.project && isNaN(projectId)) throw new PncliError(`Invalid --project "${opts.project}". Expected an integer.`, 1);
8568
8719
  if (opts.last && isNaN(last)) throw new PncliError(`Invalid --last "${opts.last}". Expected an integer.`, 1);
8569
- const data = await getClient10(program2).listScans({ projectId, last });
8720
+ const data = await getClient10(program2).listScans({ projectId: opts.project, last });
8570
8721
  success(data, "checkmarx", "scan-list", start);
8571
8722
  } catch (err) {
8572
8723
  fail(err, "checkmarx", "scan-list", start);
8573
8724
  }
8574
8725
  });
8575
- scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan ID").action(async (opts) => {
8726
+ scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
8576
8727
  const start = Date.now();
8577
8728
  try {
8578
- const id = parseInt(opts.id, 10);
8579
- if (isNaN(id)) throw new PncliError(`Invalid --id "${opts.id}". Expected an integer.`, 1);
8580
- const data = await getClient10(program2).getScan(id);
8729
+ const data = await getClient10(program2).getScan(opts.id);
8581
8730
  success(data, "checkmarx", "scan-get", start);
8582
8731
  } catch (err) {
8583
8732
  fail(err, "checkmarx", "scan-get", start);
8584
8733
  }
8585
8734
  });
8586
- scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan ID").action(async (opts) => {
8735
+ scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
8587
8736
  const start = Date.now();
8588
8737
  try {
8589
- const id = parseInt(opts.id, 10);
8590
- if (isNaN(id)) throw new PncliError(`Invalid --id "${opts.id}". Expected an integer.`, 1);
8591
- const data = await getClient10(program2).getScanResultsStatistics(id);
8738
+ const data = await getClient10(program2).getScanResultsStatistics(opts.id);
8592
8739
  success(data, "checkmarx", "scan-stats", start);
8593
8740
  } catch (err) {
8594
8741
  fail(err, "checkmarx", "scan-stats", start);
@@ -8899,18 +9046,52 @@ function getBundledSkillsDir() {
8899
9046
  function getInstalledMetaPath(targetDir) {
8900
9047
  return path7.join(targetDir, ".pncli-installed.json");
8901
9048
  }
9049
+ var SKILL_ORIGIN_FILENAME = "pncli-origin.json";
8902
9050
  function readInstalledMeta(targetDir) {
8903
9051
  const metaPath = getInstalledMetaPath(targetDir);
8904
9052
  const raw = loadJsonFile(metaPath);
8905
9053
  if (raw && raw.version === 1 && raw.skills) return raw;
8906
9054
  return { version: 1, skills: {} };
8907
9055
  }
9056
+ function getSkillOriginPath(skillDir) {
9057
+ return path7.join(skillDir, SKILL_ORIGIN_FILENAME);
9058
+ }
9059
+ function readSkillOrigin(skillDir) {
9060
+ const originPath = getSkillOriginPath(skillDir);
9061
+ if (!fs4.existsSync(originPath)) return null;
9062
+ try {
9063
+ const raw = JSON.parse(fs4.readFileSync(originPath, "utf8"));
9064
+ if (raw && raw.version === 1 && raw.source) return raw;
9065
+ } catch {
9066
+ }
9067
+ return null;
9068
+ }
9069
+ function detectRepoBranch(repoPath) {
9070
+ try {
9071
+ const result = execFileSync3("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
9072
+ encoding: "utf8",
9073
+ stdio: ["pipe", "pipe", "pipe"]
9074
+ }).trim();
9075
+ return result && result !== "HEAD" ? result : void 0;
9076
+ } catch {
9077
+ return void 0;
9078
+ }
9079
+ }
8908
9080
  function recordInstalledSkills(targetDir, skillNames, record) {
8909
9081
  if (skillNames.length === 0) return;
8910
9082
  const meta = readInstalledMeta(targetDir);
8911
9083
  const now = (/* @__PURE__ */ new Date()).toISOString();
8912
9084
  for (const skillName of skillNames) {
8913
- meta.skills[skillName] = { ...record, installedAt: now };
9085
+ const fullRecord = { ...record, installedAt: now };
9086
+ meta.skills[skillName] = fullRecord;
9087
+ const skillDir = path7.join(targetDir, skillName);
9088
+ if (fs4.existsSync(skillDir)) {
9089
+ const origin = { version: 1, ...fullRecord };
9090
+ try {
9091
+ fs4.writeFileSync(getSkillOriginPath(skillDir), JSON.stringify(origin, null, 2), "utf8");
9092
+ } catch {
9093
+ }
9094
+ }
8914
9095
  }
8915
9096
  fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8916
9097
  }
@@ -9024,6 +9205,7 @@ function pullMarketplace(marketplacePath, repoUrl, token, marketplaceName) {
9024
9205
  function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl, pluginNames, targetDir) {
9025
9206
  const results = {};
9026
9207
  let totalInstalled = 0;
9208
+ const branch = detectRepoBranch(marketplacePath);
9027
9209
  for (const pluginName of pluginNames) {
9028
9210
  const skillsSrc = resolveSkillsSrc(marketplacePath, pluginName);
9029
9211
  if (!fs4.existsSync(skillsSrc)) {
@@ -9034,7 +9216,8 @@ function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl,
9034
9216
  const { installed, failed } = copyPluginSkills(skillsSrc, targetDir, {
9035
9217
  marketplace: marketplaceName,
9036
9218
  plugin: pluginName,
9037
- installedFrom: repoUrl ?? ""
9219
+ installedFrom: repoUrl ?? "",
9220
+ branch
9038
9221
  });
9039
9222
  results[pluginName] = { installed, failed };
9040
9223
  totalInstalled += installed.length;
@@ -9472,6 +9655,121 @@ function registerSkillsCommands(program2) {
9472
9655
  fail(err, "skills", "marketplace-sync", start);
9473
9656
  }
9474
9657
  });
9658
+ marketplace.command("purge-plugin").description("Remove all skills installed from a specific plugin (or all plugins in a marketplace)").argument("<plugin>", 'Plugin name to purge, or "all" to remove every skill from the marketplace').option("--marketplace <name>", "Restrict purge to skills from this marketplace (by name or repo URL)").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--scope <scope>", "Installation scope: project | user (default: user)").option("--target <dir>", "Override skills directory").action((plugin, opts) => {
9659
+ const start = Date.now();
9660
+ try {
9661
+ let targetDir;
9662
+ if (opts.target) {
9663
+ targetDir = path7.resolve(opts.target);
9664
+ } else {
9665
+ const agentConfig = resolveAgentPaths(resolveAgentName(opts));
9666
+ const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
9667
+ targetDir = path7.resolve(scopePath);
9668
+ }
9669
+ const resolvedTarget = path7.resolve(targetDir);
9670
+ if (!fs4.existsSync(targetDir)) {
9671
+ success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
9672
+ return;
9673
+ }
9674
+ const meta = readInstalledMeta(targetDir);
9675
+ const removed = [];
9676
+ const skipped = [];
9677
+ const skillDirs = fs4.readdirSync(targetDir).filter((name) => {
9678
+ if (name.startsWith(".")) return false;
9679
+ const p = path7.join(targetDir, name);
9680
+ try {
9681
+ return fs4.statSync(p).isDirectory();
9682
+ } catch {
9683
+ return false;
9684
+ }
9685
+ });
9686
+ for (const skillName of skillDirs) {
9687
+ const skillDir = path7.resolve(targetDir, skillName);
9688
+ if (!skillDir.startsWith(resolvedTarget + path7.sep)) continue;
9689
+ let record = meta.skills[skillName] ?? null;
9690
+ if (!record) {
9691
+ const perSkill = readSkillOrigin(skillDir);
9692
+ if (perSkill) {
9693
+ record = {
9694
+ source: perSkill.source,
9695
+ marketplace: perSkill.marketplace,
9696
+ plugin: perSkill.plugin,
9697
+ installedFrom: perSkill.installedFrom,
9698
+ branch: perSkill.branch,
9699
+ installedAt: perSkill.installedAt
9700
+ };
9701
+ }
9702
+ }
9703
+ if (!record || record.source !== "marketplace") {
9704
+ skipped.push(skillName);
9705
+ continue;
9706
+ }
9707
+ const matchesMarketplace = !opts.marketplace || record.marketplace === opts.marketplace || record.installedFrom === opts.marketplace;
9708
+ const matchesPlugin = plugin === "all" || record.plugin === plugin;
9709
+ if (!matchesMarketplace || !matchesPlugin) {
9710
+ skipped.push(skillName);
9711
+ continue;
9712
+ }
9713
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9714
+ delete meta.skills[skillName];
9715
+ removed.push(skillName);
9716
+ }
9717
+ if (removed.length > 0) {
9718
+ fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9719
+ }
9720
+ success({
9721
+ removed,
9722
+ skipped: skipped.length,
9723
+ total: removed.length,
9724
+ target: targetDir,
9725
+ plugin,
9726
+ ...opts.marketplace ? { marketplace: opts.marketplace } : {}
9727
+ }, "skills", "marketplace-purge-plugin", start);
9728
+ } catch (err) {
9729
+ fail(err, "skills", "marketplace-purge-plugin", start);
9730
+ }
9731
+ });
9732
+ skills.command("purge-user").description("Remove all skills from the user-level skills folder for the target agent").option("--agent <agent>", "Target agent host: github-copilot | claude-code (default: github-copilot)").option("--claude", "Shorthand for --agent claude-code").option("--force", "Skip confirmation \u2014 remove all skills without prompting").action((opts) => {
9733
+ const start = Date.now();
9734
+ try {
9735
+ const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
9736
+ if (!fs4.existsSync(userDir)) {
9737
+ success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
9738
+ return;
9739
+ }
9740
+ const skillDirs = fs4.readdirSync(userDir).filter((name) => {
9741
+ if (name.startsWith(".")) return false;
9742
+ const p = path7.join(userDir, name);
9743
+ try {
9744
+ return fs4.statSync(p).isDirectory();
9745
+ } catch {
9746
+ return false;
9747
+ }
9748
+ });
9749
+ if (skillDirs.length === 0) {
9750
+ success({ removed: 0, target: userDir, message: "No skills found in user-level skills directory." }, "skills", "purge-user", start);
9751
+ return;
9752
+ }
9753
+ if (!opts.force) {
9754
+ throw new Error(
9755
+ `This will remove ${skillDirs.length} skill(s) from ${userDir}: ${skillDirs.join(", ")}.
9756
+ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\` to target a specific plugin.`
9757
+ );
9758
+ }
9759
+ for (const name of skillDirs) {
9760
+ const skillDir = path7.resolve(userDir, name);
9761
+ if (!skillDir.startsWith(path7.resolve(userDir) + path7.sep)) continue;
9762
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9763
+ }
9764
+ const metaPath = getInstalledMetaPath(userDir);
9765
+ if (fs4.existsSync(metaPath)) {
9766
+ fs4.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
9767
+ }
9768
+ success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
9769
+ } catch (err) {
9770
+ fail(err, "skills", "purge-user", start);
9771
+ }
9772
+ });
9475
9773
  }
9476
9774
  function resolvePluginChoices(marketplacePath) {
9477
9775
  const marketplaceJsonPath = path7.join(marketplacePath, ".claude-plugin", "marketplace.json");
@@ -9521,7 +9819,8 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
9521
9819
  source: "marketplace",
9522
9820
  marketplace: meta.marketplace,
9523
9821
  plugin: meta.plugin,
9524
- installedFrom: meta.installedFrom
9822
+ installedFrom: meta.installedFrom,
9823
+ ...meta.branch ? { branch: meta.branch } : {}
9525
9824
  });
9526
9825
  }
9527
9826
  return { installed, failed };
@@ -9783,6 +10082,170 @@ function registerOpenShiftCommands(program2) {
9783
10082
  });
9784
10083
  }
9785
10084
 
10085
+ // src/services/dynatrace/commands.ts
10086
+ function getHttp3(program2) {
10087
+ const opts = program2.optsWithGlobals();
10088
+ return createHttpClient(
10089
+ loadConfig({ configPath: opts.config }),
10090
+ Boolean(opts.dryRun)
10091
+ );
10092
+ }
10093
+ async function allPages(fetchPage) {
10094
+ const items = [];
10095
+ let nextPageKey;
10096
+ do {
10097
+ const page = await fetchPage(nextPageKey);
10098
+ items.push(...page.items);
10099
+ nextPageKey = page.nextPageKey;
10100
+ } while (nextPageKey);
10101
+ return { items, totalCount: items.length };
10102
+ }
10103
+ async function listEntities(http, entitySelector, opts) {
10104
+ return allPages(async (nextPageKey) => {
10105
+ const page = await http.dynatrace("/api/v2/entities", {
10106
+ params: nextPageKey ? { nextPageKey } : {
10107
+ entitySelector,
10108
+ from: opts.from,
10109
+ to: opts.to,
10110
+ fields: opts.fields,
10111
+ pageSize: 500
10112
+ }
10113
+ });
10114
+ return {
10115
+ items: page.entities,
10116
+ nextPageKey: page.nextPageKey,
10117
+ totalCount: page.totalCount
10118
+ };
10119
+ });
10120
+ }
10121
+ async function runTraceQuery(http, traceId) {
10122
+ if (!/^[0-9a-f]{16,32}$/i.test(traceId)) {
10123
+ throw new PncliError("Trace ID must be a 16- or 32-character hexadecimal value");
10124
+ }
10125
+ const deadline = Date.now() + 6e4;
10126
+ let response = await http.dynatracePlatform(
10127
+ "/platform/storage/query/v1/query:execute",
10128
+ {
10129
+ method: "POST",
10130
+ body: {
10131
+ query: `fetch spans | filter trace.id == "${traceId.toLowerCase()}" | sort start_time asc`,
10132
+ requestTimeoutMilliseconds: 5e3
10133
+ },
10134
+ timeoutMs: 1e4
10135
+ }
10136
+ );
10137
+ while (response.state === "RUNNING" || response.state === "NOT_STARTED") {
10138
+ if (!response.requestToken) throw new PncliError("Dynatrace did not return a query request token");
10139
+ const remainingMs = deadline - Date.now();
10140
+ if (remainingMs <= 0) throw new PncliError("Dynatrace trace query did not finish within 60 seconds");
10141
+ const pollStarted = Date.now();
10142
+ response = await http.dynatracePlatform(
10143
+ "/platform/storage/query/v1/query:poll",
10144
+ {
10145
+ params: {
10146
+ "request-token": response.requestToken,
10147
+ "request-timeout": Math.min(5e3, remainingMs)
10148
+ },
10149
+ timeoutMs: 1e4
10150
+ }
10151
+ );
10152
+ if (response.state === "RUNNING" || response.state === "NOT_STARTED") {
10153
+ const delayMs = Math.min(1e3 - (Date.now() - pollStarted), deadline - Date.now());
10154
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
10155
+ }
10156
+ }
10157
+ if (response.state !== "SUCCEEDED") {
10158
+ throw new PncliError(response.error?.message ?? `Dynatrace trace query ${response.state.toLowerCase()}`);
10159
+ }
10160
+ return response.result;
10161
+ }
10162
+ function registerDynatraceCommands(program2) {
10163
+ const dynatrace = program2.command("dynatrace").description("Dynatrace observability operations");
10164
+ const entities = dynatrace.command("entities").description("Monitored entity operations");
10165
+ entities.command("list").description("List monitored entities using an entity selector").requiredOption("--selector <selector>", 'Dynatrace entity selector, e.g. type("SERVICE")').option("--from <time>", "Start time (ISO 8601 or relative, e.g. now-2h)").option("--to <time>", "End time (ISO 8601 or relative)").option("--fields <fields>", "Additional entity fields").action(async (opts) => {
10166
+ const start = Date.now();
10167
+ try {
10168
+ const data = await listEntities(getHttp3(program2), opts.selector, opts);
10169
+ success({ totalCount: data.totalCount, entities: data.items }, "dynatrace", "entities list", start);
10170
+ } catch (err) {
10171
+ fail(err, "dynatrace", "entities list", start);
10172
+ }
10173
+ });
10174
+ entities.command("get").description("Get a monitored entity by ID").requiredOption("--id <id>", "Entity ID, e.g. SERVICE-1234567890ABCDEF").option("--from <time>", "Start time").option("--to <time>", "End time").option("--fields <fields>", "Additional entity fields").action(async (opts) => {
10175
+ const start = Date.now();
10176
+ try {
10177
+ const data = await getHttp3(program2).dynatrace(
10178
+ `/api/v2/entities/${encodeURIComponent(opts.id)}`,
10179
+ { params: { from: opts.from, to: opts.to, fields: opts.fields } }
10180
+ );
10181
+ success(data, "dynatrace", "entities get", start);
10182
+ } catch (err) {
10183
+ fail(err, "dynatrace", "entities get", start);
10184
+ }
10185
+ });
10186
+ dynatrace.command("services").description("List monitored services").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
10187
+ const start = Date.now();
10188
+ try {
10189
+ const data = await listEntities(getHttp3(program2), 'type("SERVICE")', opts);
10190
+ success({ totalCount: data.totalCount, services: data.items }, "dynatrace", "services", start);
10191
+ } catch (err) {
10192
+ fail(err, "dynatrace", "services", start);
10193
+ }
10194
+ });
10195
+ dynatrace.command("workloads").description("List monitored Kubernetes workloads").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
10196
+ const start = Date.now();
10197
+ try {
10198
+ const selector = 'type("CLOUD_APPLICATION")';
10199
+ const data = await listEntities(getHttp3(program2), selector, opts);
10200
+ success({ totalCount: data.totalCount, workloads: data.items }, "dynatrace", "workloads", start);
10201
+ } catch (err) {
10202
+ fail(err, "dynatrace", "workloads", start);
10203
+ }
10204
+ });
10205
+ const problems = dynatrace.command("problems").description("Problem operations");
10206
+ problems.command("list").description("List problems").option("--from <time>", "Start time (default: now-2h)").option("--to <time>", "End time (default: now)").option("--problem-selector <selector>", "Dynatrace problem selector").option("--entity-selector <selector>", "Affected entity selector").action(async (opts) => {
10207
+ const start = Date.now();
10208
+ try {
10209
+ const data = await allPages(async (nextPageKey) => {
10210
+ const page = await getHttp3(program2).dynatrace("/api/v2/problems", {
10211
+ params: nextPageKey ? { nextPageKey } : {
10212
+ from: opts.from ?? "now-2h",
10213
+ to: opts.to ?? "now",
10214
+ problemSelector: opts.problemSelector,
10215
+ entitySelector: opts.entitySelector,
10216
+ pageSize: 500
10217
+ }
10218
+ });
10219
+ return { items: page.problems, nextPageKey: page.nextPageKey, totalCount: page.totalCount };
10220
+ });
10221
+ success({ totalCount: data.totalCount, problems: data.items }, "dynatrace", "problems list", start);
10222
+ } catch (err) {
10223
+ fail(err, "dynatrace", "problems list", start);
10224
+ }
10225
+ });
10226
+ problems.command("get").description("Get a problem by ID").requiredOption("--id <id>", "Problem ID").option("--fields <fields>", "Additional fields").action(async (opts) => {
10227
+ const start = Date.now();
10228
+ try {
10229
+ const data = await getHttp3(program2).dynatrace(
10230
+ `/api/v2/problems/${encodeURIComponent(opts.id)}`,
10231
+ { params: { fields: opts.fields } }
10232
+ );
10233
+ success(data, "dynatrace", "problems get", start);
10234
+ } catch (err) {
10235
+ fail(err, "dynatrace", "problems get", start);
10236
+ }
10237
+ });
10238
+ dynatrace.command("trace").description("Get distributed-trace spans from Grail").requiredOption("--id <trace-id>", "16- or 32-character hexadecimal trace ID").action(async (opts) => {
10239
+ const start = Date.now();
10240
+ try {
10241
+ const result = await runTraceQuery(getHttp3(program2), opts.id);
10242
+ success(result, "dynatrace", "trace", start);
10243
+ } catch (err) {
10244
+ fail(err, "dynatrace", "trace", start);
10245
+ }
10246
+ });
10247
+ }
10248
+
9786
10249
  // src/cli.ts
9787
10250
  if (!process.env.PNCLI_VERIFY_TLS) {
9788
10251
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -9826,6 +10289,7 @@ registerSonatypeIqCommands(program);
9826
10289
  registerSkillsCommands(program);
9827
10290
  registerJwtCommands(program);
9828
10291
  registerOpenShiftCommands(program);
10292
+ registerDynatraceCommands(program);
9829
10293
  program.addHelpText("after", `
9830
10294
  Services:
9831
10295
  git Local git operations (status, diff, log, branch)
@@ -9840,11 +10304,12 @@ Services:
9840
10304
  jenkins Jenkins Data Center (jobs, builds, logs)
9841
10305
  artifactory Artifactory (repos, artifact search, build info, properties)
9842
10306
  udeploy IBM UrbanCode Deploy (component versions, deployment processes)
9843
- checkmarx Checkmarx CxSAST (projects, scans, scan statistics)
10307
+ checkmarx Checkmarx One (projects, scans, scan statistics)
9844
10308
  servicenow ServiceNow (change requests)
9845
10309
  contrast Contrast IAST (applications, vulnerability findings)
9846
10310
  sonatypeiq Sonatype IQ Server (applications, organizations, policies)
9847
10311
  openshift OpenShift / Kubernetes (pods, events, logs, metrics)
10312
+ dynatrace Dynatrace (services, entities, problems, traces, Kubernetes workloads)
9848
10313
  config Manage pncli configuration
9849
10314
  skills Download and manage Claude Code skills
9850
10315
  jwt JWT token utilities (decode header and payload)