@kolatts/pncli 1.18.0 → 1.20.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-PC4AHQCN.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);
@@ -697,21 +697,46 @@ var BitbucketClient = class {
697
697
  );
698
698
  return (result.values ?? []).map((v) => typeof v.path === "string" ? v.path : JSON.stringify(v.path));
699
699
  }
700
+ async getCurrentUser() {
701
+ const { headers } = await this.http.bitbucketRaw(
702
+ `${API}/application-properties`
703
+ );
704
+ const username = headers.get("x-ausername");
705
+ if (!username) {
706
+ throw new PncliError(
707
+ "Could not determine current Bitbucket user: X-AUSERNAME header was absent from the server response",
708
+ 1,
709
+ `${API}/application-properties`
710
+ );
711
+ }
712
+ return this.http.bitbucket(`${API}/users/${encodeURIComponent(username)}`);
713
+ }
700
714
  async approvePR(project, repo, prId) {
701
715
  return this.http.bitbucket(
702
- `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/~`,
703
- { method: "PUT", body: { status: "APPROVED" } }
716
+ `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/approve`,
717
+ { method: "POST" }
704
718
  );
705
719
  }
706
720
  async unapprovePR(project, repo, prId) {
707
721
  return this.http.bitbucket(
708
- `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/~`,
722
+ `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/approve`,
709
723
  { method: "DELETE" }
710
724
  );
711
725
  }
712
726
  async needsWorkPR(project, repo, prId) {
727
+ const [currentUser, pr] = await Promise.all([
728
+ this.getCurrentUser(),
729
+ this.getPR(project, repo, prId)
730
+ ]);
731
+ const isReviewer = pr.reviewers.some((r) => r.user.slug === currentUser.slug);
732
+ if (!isReviewer) {
733
+ await this.http.bitbucket(
734
+ `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants`,
735
+ { method: "POST", body: { user: { name: currentUser.name }, role: "REVIEWER" } }
736
+ );
737
+ }
713
738
  return this.http.bitbucket(
714
- `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/~`,
739
+ `${API}/projects/${project}/repos/${repo}/pull-requests/${prId}/participants/${currentUser.slug}`,
715
740
  { method: "PUT", body: { status: "NEEDS_WORK" } }
716
741
  );
717
742
  }
@@ -4753,7 +4778,7 @@ function requireArtifactory(config, command) {
4753
4778
  if (!config.baseUrl && !config.token) {
4754
4779
  throw new PncliError(
4755
4780
  `${command} requires Artifactory. Add the following to ~/.pncli/config.json:
4756
- "artifactory": { "baseUrl": "https://artifactory.company.com", "token": "your-token" }
4781
+ "artifactory": { "baseUrl": "https://artifactory.imagile.dev", "token": "your-token" }
4757
4782
  Or set PNCLI_ARTIFACTORY_BASE_URL and PNCLI_ARTIFACTORY_TOKEN environment variables.`,
4758
4783
  503
4759
4784
  );
@@ -5639,9 +5664,9 @@ function registerConfigCommands(program2) {
5639
5664
  } else {
5640
5665
  results.udeploy = { ok: null, message: "not configured" };
5641
5666
  }
5642
- if (cfg.checkmarx.baseUrl && cfg.checkmarx.username && cfg.checkmarx.password) {
5667
+ if (cfg.checkmarx.baseUrl && cfg.checkmarx.tenantName && (cfg.checkmarx.apiKey || cfg.checkmarx.clientId && cfg.checkmarx.clientSecret)) {
5643
5668
  try {
5644
- await http.checkmarx("/cxrestapi/projects");
5669
+ await http.checkmarx("/api/projects", { params: { limit: 1 } });
5645
5670
  results.checkmarx = { ok: true, message: "connected" };
5646
5671
  } catch (err) {
5647
5672
  results.checkmarx = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -5690,6 +5715,37 @@ function registerConfigCommands(program2) {
5690
5715
  } else {
5691
5716
  results.openshift = { ok: null, message: "not configured" };
5692
5717
  }
5718
+ if (cfg.dynatrace.baseUrl && cfg.dynatrace.apiToken) {
5719
+ try {
5720
+ await http.dynatrace("/api/v2/entities", {
5721
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
5722
+ });
5723
+ results.dynatrace = { ok: true, message: "connected" };
5724
+ } catch (err) {
5725
+ results.dynatrace = { ok: false, message: err instanceof Error ? err.message : String(err) };
5726
+ }
5727
+ } else {
5728
+ results.dynatrace = { ok: null, message: "not configured" };
5729
+ }
5730
+ if (cfg.dynatrace.platformUrl && cfg.dynatrace.platformToken) {
5731
+ try {
5732
+ await http.dynatracePlatform("/platform/storage/query/v1/query:execute", {
5733
+ method: "POST",
5734
+ body: {
5735
+ query: "fetch spans | limit 1",
5736
+ requestTimeoutMilliseconds: 5e3
5737
+ },
5738
+ timeoutMs: 1e4
5739
+ });
5740
+ results.dynatrace_platform = { ok: true, message: "connected" };
5741
+ } catch (err) {
5742
+ results.dynatrace_platform = { ok: false, message: err instanceof Error ? err.message : String(err) };
5743
+ }
5744
+ } else if (cfg.dynatrace.platformUrl || cfg.dynatrace.platformToken) {
5745
+ results.dynatrace_platform = { ok: false, message: "platformUrl and platformToken must both be configured" };
5746
+ } else {
5747
+ results.dynatrace_platform = { ok: null, message: "not configured" };
5748
+ }
5693
5749
  success(results, "config", "test", start);
5694
5750
  } catch (err) {
5695
5751
  fail(err, "config", "test", start);
@@ -5836,13 +5892,13 @@ function registerConfigCommands(program2) {
5836
5892
  } else {
5837
5893
  results.artifactory = { status: "valid", message: "ok" };
5838
5894
  }
5839
- if (!cfg.checkmarx.username || !cfg.checkmarx.password) {
5895
+ if (!cfg.checkmarx.tenantName || !cfg.checkmarx.apiKey && (!cfg.checkmarx.clientId || !cfg.checkmarx.clientSecret)) {
5840
5896
  results.checkmarx = { status: "blank", message: "not configured" };
5841
5897
  } else if (!cfg.checkmarx.baseUrl) {
5842
5898
  results.checkmarx = { status: "error", message: "baseUrl not configured" };
5843
5899
  } else {
5844
5900
  try {
5845
- await http.checkmarx("/cxrestapi/projects", { timeoutMs: 1e4 });
5901
+ await http.checkmarx("/api/projects", { params: { limit: 1 }, timeoutMs: 1e4 });
5846
5902
  results.checkmarx = { status: "valid", message: "ok" };
5847
5903
  } catch (err) {
5848
5904
  results.checkmarx = categorize2(err);
@@ -5897,11 +5953,48 @@ function registerConfigCommands(program2) {
5897
5953
  results.openshift = categorize2(err);
5898
5954
  }
5899
5955
  }
5956
+ if (!cfg.dynatrace.apiToken) {
5957
+ results.dynatrace = { status: "blank", message: "not configured" };
5958
+ } else if (!cfg.dynatrace.baseUrl) {
5959
+ results.dynatrace = { status: "error", message: "baseUrl not configured" };
5960
+ } else {
5961
+ try {
5962
+ await http.dynatrace("/api/v2/entities", {
5963
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 },
5964
+ timeoutMs: 1e4
5965
+ });
5966
+ results.dynatrace = { status: "valid", message: "ok" };
5967
+ } catch (err) {
5968
+ results.dynatrace = categorize2(err);
5969
+ }
5970
+ }
5971
+ if (!cfg.dynatrace.platformUrl && !cfg.dynatrace.platformToken) {
5972
+ results.dynatrace_platform = { status: "blank", message: "not configured" };
5973
+ } else if (!cfg.dynatrace.platformUrl || !cfg.dynatrace.platformToken) {
5974
+ results.dynatrace_platform = {
5975
+ status: "error",
5976
+ message: "platformUrl and platformToken must both be configured"
5977
+ };
5978
+ } else {
5979
+ try {
5980
+ await http.dynatracePlatform("/platform/storage/query/v1/query:execute", {
5981
+ method: "POST",
5982
+ body: {
5983
+ query: "fetch spans | limit 1",
5984
+ requestTimeoutMilliseconds: 5e3
5985
+ },
5986
+ timeoutMs: 1e4
5987
+ });
5988
+ results.dynatrace_platform = { status: "valid", message: "ok" };
5989
+ } catch (err) {
5990
+ results.dynatrace_platform = categorize2(err);
5991
+ }
5992
+ }
5900
5993
  const statuses = Object.values(results).map((r) => r.status);
5901
5994
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
5902
5995
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
5903
5996
  if (cmdOpts.output === "table") {
5904
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
5997
+ const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform"];
5905
5998
  const labelWidth = 14;
5906
5999
  const statusWidth = 9;
5907
6000
  for (const svc of services) {
@@ -5915,7 +6008,7 @@ function registerConfigCommands(program2) {
5915
6008
  process.stdout.write("\n");
5916
6009
  } else {
5917
6010
  if (opts.pretty) {
5918
- const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
6011
+ const services = ["jira", "bitbucket", "github", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift", "dynatrace", "dynatrace_platform"];
5919
6012
  const labelWidth = 14;
5920
6013
  const statusWidth = 9;
5921
6014
  for (const svc of services) {
@@ -5953,7 +6046,7 @@ async function initGlobalConfig(start) {
5953
6046
  });
5954
6047
  process.stderr.write("\n\u2500\u2500 Jira \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
5955
6048
  const jiraBaseUrl = await input({
5956
- message: "Jira base URL (e.g. jira.your-company.com):",
6049
+ message: "Jira base URL (e.g. jira.imagile.dev):",
5957
6050
  default: ""
5958
6051
  });
5959
6052
  const jiraApiToken = await password({
@@ -5961,7 +6054,7 @@ async function initGlobalConfig(start) {
5961
6054
  });
5962
6055
  process.stderr.write("\n\u2500\u2500 Bitbucket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
5963
6056
  const bitbucketBaseUrl = await input({
5964
- message: "Bitbucket Server base URL (e.g. bitbucket.your-company.com):",
6057
+ message: "Bitbucket Server base URL (e.g. bitbucket.imagile.dev):",
5965
6058
  default: ""
5966
6059
  });
5967
6060
  const bitbucketPat = await password({
@@ -6003,7 +6096,7 @@ async function initGlobalConfig(start) {
6003
6096
  }
6004
6097
  process.stderr.write("\n\u2500\u2500 Confluence \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");
6005
6098
  const confluenceBaseUrl = await input({
6006
- message: "Confluence base URL (e.g. confluence.your-company.com):",
6099
+ message: "Confluence base URL (e.g. confluence.imagile.dev):",
6007
6100
  default: ""
6008
6101
  });
6009
6102
  const confluenceApiToken = await password({
@@ -6021,7 +6114,7 @@ async function initGlobalConfig(start) {
6021
6114
  let mavenRepo = "";
6022
6115
  if (useArtifactory) {
6023
6116
  artifactoryBaseUrl = await input({
6024
- message: "Artifactory base URL (e.g. artifactory.company.com):",
6117
+ message: "Artifactory base URL (e.g. artifactory.imagile.dev):",
6025
6118
  default: ""
6026
6119
  });
6027
6120
  artifactoryToken = await password({
@@ -6050,7 +6143,7 @@ async function initGlobalConfig(start) {
6050
6143
  let sonarToken = "";
6051
6144
  if (useSonar) {
6052
6145
  sonarBaseUrl = await input({
6053
- message: "SonarQube Server base URL (e.g. sonar.your-company.com):",
6146
+ message: "SonarQube Server base URL (e.g. sonar.imagile.dev):",
6054
6147
  default: ""
6055
6148
  });
6056
6149
  sonarToken = await password({
@@ -6081,7 +6174,7 @@ async function initGlobalConfig(start) {
6081
6174
  }
6082
6175
  } else {
6083
6176
  sdeBaseUrl = await input({
6084
- message: "SDElements hostname\n Cloud-hosted: your-org.sdelements.com\n On-premise: sde.your-company.com\n Host: ",
6177
+ message: "SDElements hostname\n Cloud-hosted: imagile.sdelements.com\n On-premise: sde.imagile.dev\n Host: ",
6085
6178
  default: ""
6086
6179
  });
6087
6180
  sdeToken = await password({
@@ -6103,7 +6196,7 @@ async function initGlobalConfig(start) {
6103
6196
  let adoDiscoveredTypes = [];
6104
6197
  if (useAdo) {
6105
6198
  adoBaseUrl = await input({
6106
- message: "Azure DevOps Server base URL (e.g. tfs.company.com or devops.company.com/tfs):",
6199
+ message: "Azure DevOps Server base URL (e.g. tfs.imagile.dev or devops.imagile.dev/tfs):",
6107
6200
  default: ""
6108
6201
  });
6109
6202
  adoPat = await password({
@@ -6175,7 +6268,7 @@ async function initGlobalConfig(start) {
6175
6268
  let jenkinsApiToken = "";
6176
6269
  if (useJenkins) {
6177
6270
  jenkinsBaseUrl = await input({
6178
- message: "Jenkins base URL (e.g. jenkins.your-company.com):",
6271
+ message: "Jenkins base URL (e.g. jenkins.imagile.dev):",
6179
6272
  default: ""
6180
6273
  });
6181
6274
  jenkinsUsername = await input({
@@ -6199,7 +6292,7 @@ async function initGlobalConfig(start) {
6199
6292
  let udeployDefaultEnv = "";
6200
6293
  if (useUdeploy) {
6201
6294
  udeployBaseUrl = await input({
6202
- message: "UDeploy base URL (e.g. ucd.company.com:8443):",
6295
+ message: "UDeploy base URL (e.g. ucd.imagile.dev:8443):",
6203
6296
  default: ""
6204
6297
  });
6205
6298
  udeployUsername = await input({
@@ -6248,38 +6341,51 @@ async function initGlobalConfig(start) {
6248
6341
  }
6249
6342
  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
6343
  const useCheckmarx = await confirm({
6251
- message: "Configure Checkmarx CxSAST for vulnerability scanning?",
6344
+ message: "Configure Checkmarx One for vulnerability scanning?",
6252
6345
  default: false
6253
6346
  });
6254
6347
  let checkmarxBaseUrl = "";
6255
- let checkmarxUsername = "";
6256
- let checkmarxPassword = "";
6348
+ let checkmarxTenantName = "";
6349
+ let checkmarxApiKey = "";
6350
+ let checkmarxClientId = "";
6351
+ let checkmarxClientSecret = "";
6257
6352
  if (useCheckmarx) {
6258
6353
  checkmarxBaseUrl = await input({
6259
- message: "Checkmarx base URL (e.g. cx.company.com):",
6354
+ message: "Checkmarx One API base URL (e.g. https://ast.checkmarx.net):",
6260
6355
  default: ""
6261
6356
  });
6262
- checkmarxUsername = await input({
6263
- message: "Checkmarx username:",
6357
+ checkmarxTenantName = await input({
6358
+ message: "Checkmarx One tenant name (IAM realm, e.g. imagile):",
6264
6359
  default: ""
6265
6360
  });
6266
- checkmarxPassword = await password({
6267
- message: "Checkmarx password:",
6268
- validate: (v) => v.length > 0 || "Password cannot be blank"
6361
+ checkmarxApiKey = await password({
6362
+ message: "Checkmarx One API key (leave blank to use an OAuth client):"
6269
6363
  });
6270
- if (checkmarxBaseUrl && checkmarxUsername && checkmarxPassword) {
6364
+ if (!checkmarxApiKey) {
6365
+ checkmarxClientId = await input({
6366
+ message: "Checkmarx One OAuth client ID:",
6367
+ default: ""
6368
+ });
6369
+ checkmarxClientSecret = await password({
6370
+ message: "Checkmarx One OAuth client secret:",
6371
+ validate: (v) => v.length > 0 || "Client secret cannot be blank"
6372
+ });
6373
+ }
6374
+ if (checkmarxBaseUrl && checkmarxTenantName && (checkmarxApiKey || checkmarxClientId && checkmarxClientSecret)) {
6271
6375
  process.stderr.write("\n Verifying connection...\n");
6272
6376
  try {
6273
6377
  const tempConfig = {
6274
6378
  ...loadConfig(),
6275
6379
  checkmarx: {
6276
6380
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
6277
- username: checkmarxUsername,
6278
- password: checkmarxPassword
6381
+ tenantName: checkmarxTenantName,
6382
+ apiKey: checkmarxApiKey || void 0,
6383
+ clientId: checkmarxClientId,
6384
+ clientSecret: checkmarxClientSecret
6279
6385
  }
6280
6386
  };
6281
6387
  const tempHttp = createHttpClient(tempConfig);
6282
- await tempHttp.checkmarx("/cxrestapi/projects");
6388
+ await tempHttp.checkmarx("/api/projects", { params: { limit: 1 } });
6283
6389
  process.stderr.write(" Connected.\n");
6284
6390
  } catch (err) {
6285
6391
  warn(`Could not connect to Checkmarx: ${err instanceof Error ? err.message : String(err)}`);
@@ -6298,7 +6404,7 @@ async function initGlobalConfig(start) {
6298
6404
  let servicenowApiToken = "";
6299
6405
  if (useServiceNow) {
6300
6406
  servicenowBaseUrl = await input({
6301
- message: "ServiceNow instance URL (e.g. mycompany.service-now.com):",
6407
+ message: "ServiceNow instance URL (e.g. imagile.service-now.com):",
6302
6408
  default: ""
6303
6409
  });
6304
6410
  servicenowUsername = await input({
@@ -6353,7 +6459,7 @@ async function initGlobalConfig(start) {
6353
6459
  if (useSonatypeIq) {
6354
6460
  process.stderr.write(" User Token credentials are found in your IQ Server profile under User Token.\n");
6355
6461
  sonatypeIqBaseUrl = await input({
6356
- message: "Sonatype IQ Server base URL (e.g. iq.your-company.com):",
6462
+ message: "Sonatype IQ Server base URL (e.g. iq.imagile.dev):",
6357
6463
  default: ""
6358
6464
  });
6359
6465
  sonatypeIqUserCode = await input({
@@ -6447,7 +6553,7 @@ async function initGlobalConfig(start) {
6447
6553
  if (useOpenShift) {
6448
6554
  process.stderr.write(" Service account token: cat /var/run/secrets/kubernetes.io/serviceaccount/token\n");
6449
6555
  openShiftBaseUrl = await input({
6450
- message: "OpenShift API server URL (e.g. https://api.cluster.example.com:6443):",
6556
+ message: "OpenShift API server URL (e.g. https://api.cluster.imagile.dev:6443):",
6451
6557
  default: ""
6452
6558
  });
6453
6559
  openShiftToken = await password({
@@ -6472,6 +6578,59 @@ async function initGlobalConfig(start) {
6472
6578
  }
6473
6579
  }
6474
6580
  }
6581
+ 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");
6582
+ const useDynatrace = await confirm({
6583
+ message: "Configure Dynatrace for services, problems, workloads, and traces?",
6584
+ default: false
6585
+ });
6586
+ let dynatraceBaseUrl = "";
6587
+ let dynatraceApiToken = "";
6588
+ let dynatracePlatformUrl = "";
6589
+ let dynatracePlatformToken = "";
6590
+ if (useDynatrace) {
6591
+ dynatraceBaseUrl = await input({
6592
+ message: "Dynatrace environment URL (e.g. https://abc12345.live.dynatrace.com):",
6593
+ default: ""
6594
+ });
6595
+ dynatraceApiToken = await password({
6596
+ message: "Environment API token (entities.read and problems.read):"
6597
+ });
6598
+ const configureTraces = await confirm({
6599
+ message: "Configure the latest Dynatrace platform for Grail trace queries?",
6600
+ default: false
6601
+ });
6602
+ if (configureTraces) {
6603
+ dynatracePlatformUrl = await input({
6604
+ message: "Dynatrace platform URL (e.g. https://abc12345.apps.dynatrace.com):",
6605
+ default: ""
6606
+ });
6607
+ dynatracePlatformToken = await password({
6608
+ message: "Platform token with Grail span read permissions:"
6609
+ });
6610
+ }
6611
+ if (dynatraceBaseUrl && dynatraceApiToken) {
6612
+ process.stderr.write("\n Verifying connection...\n");
6613
+ try {
6614
+ const tempConfig = {
6615
+ ...loadConfig(),
6616
+ dynatrace: {
6617
+ baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
6618
+ apiToken: dynatraceApiToken,
6619
+ platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
6620
+ platformToken: dynatracePlatformToken || void 0
6621
+ }
6622
+ };
6623
+ const tempHttp = createHttpClient(tempConfig);
6624
+ await tempHttp.dynatrace("/api/v2/entities", {
6625
+ params: { entitySelector: 'type("SERVICE")', pageSize: 1 }
6626
+ });
6627
+ process.stderr.write(" Connected.\n");
6628
+ } catch (err) {
6629
+ warn(`Could not connect to Dynatrace: ${err instanceof Error ? err.message : String(err)}`);
6630
+ warn("Config will be saved anyway. Check your URLs and tokens and re-run pncli config test.");
6631
+ }
6632
+ }
6633
+ }
6475
6634
  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
6635
  const jiraProject = await input({
6477
6636
  message: "Default Jira project key (optional):",
@@ -6567,8 +6726,10 @@ async function initGlobalConfig(start) {
6567
6726
  ...useCheckmarx && checkmarxBaseUrl ? {
6568
6727
  checkmarx: {
6569
6728
  baseUrl: normalizeBaseUrl(checkmarxBaseUrl),
6570
- username: checkmarxUsername || void 0,
6571
- password: checkmarxPassword || void 0
6729
+ tenantName: checkmarxTenantName || void 0,
6730
+ apiKey: checkmarxApiKey || void 0,
6731
+ clientId: checkmarxClientId || void 0,
6732
+ clientSecret: checkmarxClientSecret || void 0
6572
6733
  }
6573
6734
  } : {},
6574
6735
  ...useServiceNow && servicenowBaseUrl ? {
@@ -6601,6 +6762,14 @@ async function initGlobalConfig(start) {
6601
6762
  token: openShiftToken || void 0
6602
6763
  }
6603
6764
  } : {},
6765
+ ...useDynatrace && dynatraceBaseUrl ? {
6766
+ dynatrace: {
6767
+ baseUrl: normalizeBaseUrl(dynatraceBaseUrl),
6768
+ apiToken: dynatraceApiToken || void 0,
6769
+ platformUrl: normalizeBaseUrl(dynatracePlatformUrl) || void 0,
6770
+ platformToken: dynatracePlatformToken || void 0
6771
+ }
6772
+ } : {},
6604
6773
  defaults: {
6605
6774
  jira: {
6606
6775
  project: jiraProject || void 0
@@ -7688,11 +7857,11 @@ function registerAdoRepoCommands(ado) {
7688
7857
  const start = Date.now();
7689
7858
  try {
7690
7859
  const globalOpts = ado.optsWithGlobals();
7691
- const { loadConfig: lc } = await import("./config-QN7KDHE4.js");
7860
+ const { loadConfig: lc } = await import("./config-CPRXY5SN.js");
7692
7861
  const config = lc({ configPath: globalOpts.config });
7693
7862
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
7694
7863
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
7695
- const { createHttpClient: createHttpClient2 } = await import("./http-DDY5DLF6.js");
7864
+ const { createHttpClient: createHttpClient2 } = await import("./http-IXJA4TMS.js");
7696
7865
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
7697
7866
  const coreClient = new AdoCoreClient2(http);
7698
7867
  const me = await coreClient.getConnectionData(collection);
@@ -8506,24 +8675,35 @@ var CheckmarxClient = class {
8506
8675
  }
8507
8676
  http;
8508
8677
  async listProjects() {
8509
- return this.http.checkmarx("/cxrestapi/projects");
8678
+ const res = await this.http.checkmarx("projects", { params: { limit: 100 } });
8679
+ const projects = res.projects ?? [];
8680
+ if (res.filteredTotalCount > projects.length) {
8681
+ process.stderr.write(`warning: ${res.filteredTotalCount} projects found; only showing first ${projects.length}
8682
+ `);
8683
+ }
8684
+ return projects;
8510
8685
  }
8511
8686
  async getProject(id) {
8512
- return this.http.checkmarx(`/cxrestapi/projects/${id}`);
8687
+ return this.http.checkmarx(`projects/${id}`);
8513
8688
  }
8514
8689
  async listScans(opts = {}) {
8515
- return this.http.checkmarx("/cxrestapi/sast/scans", {
8516
- params: {
8517
- projectId: opts.projectId,
8518
- last: opts.last
8519
- }
8520
- });
8690
+ const params = { limit: opts.last ?? 100 };
8691
+ if (opts.projectId) params["project-id"] = opts.projectId;
8692
+ const res = await this.http.checkmarx("scans", { params });
8693
+ const scans = res.scans ?? [];
8694
+ if (res.filteredTotalCount > scans.length) {
8695
+ process.stderr.write(`warning: ${res.filteredTotalCount} scans found; only showing first ${scans.length}
8696
+ `);
8697
+ }
8698
+ return scans;
8521
8699
  }
8522
8700
  async getScan(id) {
8523
- return this.http.checkmarx(`/cxrestapi/sast/scans/${id}`);
8701
+ return this.http.checkmarx(`scans/${id}`);
8524
8702
  }
8525
8703
  async getScanResultsStatistics(scanId) {
8526
- return this.http.checkmarx(`/cxrestapi/sast/scans/${scanId}/resultsStatistics`);
8704
+ return this.http.checkmarx("results/summary", {
8705
+ params: { "scan-id": scanId }
8706
+ });
8527
8707
  }
8528
8708
  };
8529
8709
 
@@ -8536,7 +8716,7 @@ function getClient10(program2) {
8536
8716
  return new CheckmarxClient(http);
8537
8717
  }
8538
8718
  function registerCheckmarxCommands(program2) {
8539
- const cx = program2.command("checkmarx").description("Checkmarx CxSAST operations");
8719
+ const cx = program2.command("checkmarx").description("Checkmarx One operations");
8540
8720
  const project = cx.command("project").description("Project operations");
8541
8721
  const scan = cx.command("scan").description("Scan operations");
8542
8722
  project.command("list").description("List all projects").action(async () => {
@@ -8548,47 +8728,39 @@ function registerCheckmarxCommands(program2) {
8548
8728
  fail(err, "checkmarx", "project-list", start);
8549
8729
  }
8550
8730
  });
8551
- project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project ID").action(async (opts) => {
8731
+ project.command("get").description("Get a project by ID").requiredOption("--id <id>", "Project UUID").action(async (opts) => {
8552
8732
  const start = Date.now();
8553
8733
  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);
8734
+ const data = await getClient10(program2).getProject(opts.id);
8557
8735
  success(data, "checkmarx", "project-get", start);
8558
8736
  } catch (err) {
8559
8737
  fail(err, "checkmarx", "project-get", start);
8560
8738
  }
8561
8739
  });
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) => {
8740
+ 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
8741
  const start = Date.now();
8564
8742
  try {
8565
- const projectId = opts.project ? parseInt(opts.project, 10) : void 0;
8566
8743
  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
8744
  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 });
8745
+ const data = await getClient10(program2).listScans({ projectId: opts.project, last });
8570
8746
  success(data, "checkmarx", "scan-list", start);
8571
8747
  } catch (err) {
8572
8748
  fail(err, "checkmarx", "scan-list", start);
8573
8749
  }
8574
8750
  });
8575
- scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan ID").action(async (opts) => {
8751
+ scan.command("get").description("Get a scan by ID").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
8576
8752
  const start = Date.now();
8577
8753
  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);
8754
+ const data = await getClient10(program2).getScan(opts.id);
8581
8755
  success(data, "checkmarx", "scan-get", start);
8582
8756
  } catch (err) {
8583
8757
  fail(err, "checkmarx", "scan-get", start);
8584
8758
  }
8585
8759
  });
8586
- scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan ID").action(async (opts) => {
8760
+ scan.command("stats").description("Get results statistics for a scan").requiredOption("--id <id>", "Scan UUID").action(async (opts) => {
8587
8761
  const start = Date.now();
8588
8762
  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);
8763
+ const data = await getClient10(program2).getScanResultsStatistics(opts.id);
8592
8764
  success(data, "checkmarx", "scan-stats", start);
8593
8765
  } catch (err) {
8594
8766
  fail(err, "checkmarx", "scan-stats", start);
@@ -8899,18 +9071,52 @@ function getBundledSkillsDir() {
8899
9071
  function getInstalledMetaPath(targetDir) {
8900
9072
  return path7.join(targetDir, ".pncli-installed.json");
8901
9073
  }
9074
+ var SKILL_ORIGIN_FILENAME = "pncli-origin.json";
8902
9075
  function readInstalledMeta(targetDir) {
8903
9076
  const metaPath = getInstalledMetaPath(targetDir);
8904
9077
  const raw = loadJsonFile(metaPath);
8905
9078
  if (raw && raw.version === 1 && raw.skills) return raw;
8906
9079
  return { version: 1, skills: {} };
8907
9080
  }
9081
+ function getSkillOriginPath(skillDir) {
9082
+ return path7.join(skillDir, SKILL_ORIGIN_FILENAME);
9083
+ }
9084
+ function readSkillOrigin(skillDir) {
9085
+ const originPath = getSkillOriginPath(skillDir);
9086
+ if (!fs4.existsSync(originPath)) return null;
9087
+ try {
9088
+ const raw = JSON.parse(fs4.readFileSync(originPath, "utf8"));
9089
+ if (raw && raw.version === 1 && raw.source) return raw;
9090
+ } catch {
9091
+ }
9092
+ return null;
9093
+ }
9094
+ function detectRepoBranch(repoPath) {
9095
+ try {
9096
+ const result = execFileSync3("git", ["-C", repoPath, "rev-parse", "--abbrev-ref", "HEAD"], {
9097
+ encoding: "utf8",
9098
+ stdio: ["pipe", "pipe", "pipe"]
9099
+ }).trim();
9100
+ return result && result !== "HEAD" ? result : void 0;
9101
+ } catch {
9102
+ return void 0;
9103
+ }
9104
+ }
8908
9105
  function recordInstalledSkills(targetDir, skillNames, record) {
8909
9106
  if (skillNames.length === 0) return;
8910
9107
  const meta = readInstalledMeta(targetDir);
8911
9108
  const now = (/* @__PURE__ */ new Date()).toISOString();
8912
9109
  for (const skillName of skillNames) {
8913
- meta.skills[skillName] = { ...record, installedAt: now };
9110
+ const fullRecord = { ...record, installedAt: now };
9111
+ meta.skills[skillName] = fullRecord;
9112
+ const skillDir = path7.join(targetDir, skillName);
9113
+ if (fs4.existsSync(skillDir)) {
9114
+ const origin = { version: 1, ...fullRecord };
9115
+ try {
9116
+ fs4.writeFileSync(getSkillOriginPath(skillDir), JSON.stringify(origin, null, 2), "utf8");
9117
+ } catch {
9118
+ }
9119
+ }
8914
9120
  }
8915
9121
  fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
8916
9122
  }
@@ -9024,6 +9230,7 @@ function pullMarketplace(marketplacePath, repoUrl, token, marketplaceName) {
9024
9230
  function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl, pluginNames, targetDir) {
9025
9231
  const results = {};
9026
9232
  let totalInstalled = 0;
9233
+ const branch = detectRepoBranch(marketplacePath);
9027
9234
  for (const pluginName of pluginNames) {
9028
9235
  const skillsSrc = resolveSkillsSrc(marketplacePath, pluginName);
9029
9236
  if (!fs4.existsSync(skillsSrc)) {
@@ -9034,7 +9241,8 @@ function installPluginsForMarketplace(marketplacePath, marketplaceName, repoUrl,
9034
9241
  const { installed, failed } = copyPluginSkills(skillsSrc, targetDir, {
9035
9242
  marketplace: marketplaceName,
9036
9243
  plugin: pluginName,
9037
- installedFrom: repoUrl ?? ""
9244
+ installedFrom: repoUrl ?? "",
9245
+ branch
9038
9246
  });
9039
9247
  results[pluginName] = { installed, failed };
9040
9248
  totalInstalled += installed.length;
@@ -9472,6 +9680,121 @@ function registerSkillsCommands(program2) {
9472
9680
  fail(err, "skills", "marketplace-sync", start);
9473
9681
  }
9474
9682
  });
9683
+ 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) => {
9684
+ const start = Date.now();
9685
+ try {
9686
+ let targetDir;
9687
+ if (opts.target) {
9688
+ targetDir = path7.resolve(opts.target);
9689
+ } else {
9690
+ const agentConfig = resolveAgentPaths(resolveAgentName(opts));
9691
+ const scopePath = (opts.scope ?? "user") === "project" ? agentConfig.project : agentConfig.user;
9692
+ targetDir = path7.resolve(scopePath);
9693
+ }
9694
+ const resolvedTarget = path7.resolve(targetDir);
9695
+ if (!fs4.existsSync(targetDir)) {
9696
+ success({ removed: [], total: 0, target: targetDir, message: "No skills directory found." }, "skills", "marketplace-purge-plugin", start);
9697
+ return;
9698
+ }
9699
+ const meta = readInstalledMeta(targetDir);
9700
+ const removed = [];
9701
+ const skipped = [];
9702
+ const skillDirs = fs4.readdirSync(targetDir).filter((name) => {
9703
+ if (name.startsWith(".")) return false;
9704
+ const p = path7.join(targetDir, name);
9705
+ try {
9706
+ return fs4.statSync(p).isDirectory();
9707
+ } catch {
9708
+ return false;
9709
+ }
9710
+ });
9711
+ for (const skillName of skillDirs) {
9712
+ const skillDir = path7.resolve(targetDir, skillName);
9713
+ if (!skillDir.startsWith(resolvedTarget + path7.sep)) continue;
9714
+ let record = meta.skills[skillName] ?? null;
9715
+ if (!record) {
9716
+ const perSkill = readSkillOrigin(skillDir);
9717
+ if (perSkill) {
9718
+ record = {
9719
+ source: perSkill.source,
9720
+ marketplace: perSkill.marketplace,
9721
+ plugin: perSkill.plugin,
9722
+ installedFrom: perSkill.installedFrom,
9723
+ branch: perSkill.branch,
9724
+ installedAt: perSkill.installedAt
9725
+ };
9726
+ }
9727
+ }
9728
+ if (!record || record.source !== "marketplace") {
9729
+ skipped.push(skillName);
9730
+ continue;
9731
+ }
9732
+ const matchesMarketplace = !opts.marketplace || record.marketplace === opts.marketplace || record.installedFrom === opts.marketplace;
9733
+ const matchesPlugin = plugin === "all" || record.plugin === plugin;
9734
+ if (!matchesMarketplace || !matchesPlugin) {
9735
+ skipped.push(skillName);
9736
+ continue;
9737
+ }
9738
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9739
+ delete meta.skills[skillName];
9740
+ removed.push(skillName);
9741
+ }
9742
+ if (removed.length > 0) {
9743
+ fs4.writeFileSync(getInstalledMetaPath(targetDir), JSON.stringify(meta, null, 2), "utf8");
9744
+ }
9745
+ success({
9746
+ removed,
9747
+ skipped: skipped.length,
9748
+ total: removed.length,
9749
+ target: targetDir,
9750
+ plugin,
9751
+ ...opts.marketplace ? { marketplace: opts.marketplace } : {}
9752
+ }, "skills", "marketplace-purge-plugin", start);
9753
+ } catch (err) {
9754
+ fail(err, "skills", "marketplace-purge-plugin", start);
9755
+ }
9756
+ });
9757
+ 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) => {
9758
+ const start = Date.now();
9759
+ try {
9760
+ const userDir = resolveAgentPaths(resolveAgentName(opts)).user;
9761
+ if (!fs4.existsSync(userDir)) {
9762
+ success({ removed: 0, target: userDir, message: "No user-level skills directory found \u2014 nothing to purge." }, "skills", "purge-user", start);
9763
+ return;
9764
+ }
9765
+ const skillDirs = fs4.readdirSync(userDir).filter((name) => {
9766
+ if (name.startsWith(".")) return false;
9767
+ const p = path7.join(userDir, name);
9768
+ try {
9769
+ return fs4.statSync(p).isDirectory();
9770
+ } catch {
9771
+ return false;
9772
+ }
9773
+ });
9774
+ if (skillDirs.length === 0) {
9775
+ success({ removed: 0, target: userDir, message: "No skills found in user-level skills directory." }, "skills", "purge-user", start);
9776
+ return;
9777
+ }
9778
+ if (!opts.force) {
9779
+ throw new Error(
9780
+ `This will remove ${skillDirs.length} skill(s) from ${userDir}: ${skillDirs.join(", ")}.
9781
+ Re-run with --force to confirm, or use \`pncli skills marketplace purge-plugin\` to target a specific plugin.`
9782
+ );
9783
+ }
9784
+ for (const name of skillDirs) {
9785
+ const skillDir = path7.resolve(userDir, name);
9786
+ if (!skillDir.startsWith(path7.resolve(userDir) + path7.sep)) continue;
9787
+ fs4.rmSync(skillDir, { recursive: true, force: true });
9788
+ }
9789
+ const metaPath = getInstalledMetaPath(userDir);
9790
+ if (fs4.existsSync(metaPath)) {
9791
+ fs4.writeFileSync(metaPath, JSON.stringify({ version: 1, skills: {} }, null, 2), "utf8");
9792
+ }
9793
+ success({ removed: skillDirs.length, skills: skillDirs, target: userDir }, "skills", "purge-user", start);
9794
+ } catch (err) {
9795
+ fail(err, "skills", "purge-user", start);
9796
+ }
9797
+ });
9475
9798
  }
9476
9799
  function resolvePluginChoices(marketplacePath) {
9477
9800
  const marketplaceJsonPath = path7.join(marketplacePath, ".claude-plugin", "marketplace.json");
@@ -9521,7 +9844,8 @@ function copyPluginSkills(skillsSrc, targetDir, meta) {
9521
9844
  source: "marketplace",
9522
9845
  marketplace: meta.marketplace,
9523
9846
  plugin: meta.plugin,
9524
- installedFrom: meta.installedFrom
9847
+ installedFrom: meta.installedFrom,
9848
+ ...meta.branch ? { branch: meta.branch } : {}
9525
9849
  });
9526
9850
  }
9527
9851
  return { installed, failed };
@@ -9783,6 +10107,170 @@ function registerOpenShiftCommands(program2) {
9783
10107
  });
9784
10108
  }
9785
10109
 
10110
+ // src/services/dynatrace/commands.ts
10111
+ function getHttp3(program2) {
10112
+ const opts = program2.optsWithGlobals();
10113
+ return createHttpClient(
10114
+ loadConfig({ configPath: opts.config }),
10115
+ Boolean(opts.dryRun)
10116
+ );
10117
+ }
10118
+ async function allPages(fetchPage) {
10119
+ const items = [];
10120
+ let nextPageKey;
10121
+ do {
10122
+ const page = await fetchPage(nextPageKey);
10123
+ items.push(...page.items);
10124
+ nextPageKey = page.nextPageKey;
10125
+ } while (nextPageKey);
10126
+ return { items, totalCount: items.length };
10127
+ }
10128
+ async function listEntities(http, entitySelector, opts) {
10129
+ return allPages(async (nextPageKey) => {
10130
+ const page = await http.dynatrace("/api/v2/entities", {
10131
+ params: nextPageKey ? { nextPageKey } : {
10132
+ entitySelector,
10133
+ from: opts.from,
10134
+ to: opts.to,
10135
+ fields: opts.fields,
10136
+ pageSize: 500
10137
+ }
10138
+ });
10139
+ return {
10140
+ items: page.entities,
10141
+ nextPageKey: page.nextPageKey,
10142
+ totalCount: page.totalCount
10143
+ };
10144
+ });
10145
+ }
10146
+ async function runTraceQuery(http, traceId) {
10147
+ if (!/^[0-9a-f]{16,32}$/i.test(traceId)) {
10148
+ throw new PncliError("Trace ID must be a 16- or 32-character hexadecimal value");
10149
+ }
10150
+ const deadline = Date.now() + 6e4;
10151
+ let response = await http.dynatracePlatform(
10152
+ "/platform/storage/query/v1/query:execute",
10153
+ {
10154
+ method: "POST",
10155
+ body: {
10156
+ query: `fetch spans | filter trace.id == "${traceId.toLowerCase()}" | sort start_time asc`,
10157
+ requestTimeoutMilliseconds: 5e3
10158
+ },
10159
+ timeoutMs: 1e4
10160
+ }
10161
+ );
10162
+ while (response.state === "RUNNING" || response.state === "NOT_STARTED") {
10163
+ if (!response.requestToken) throw new PncliError("Dynatrace did not return a query request token");
10164
+ const remainingMs = deadline - Date.now();
10165
+ if (remainingMs <= 0) throw new PncliError("Dynatrace trace query did not finish within 60 seconds");
10166
+ const pollStarted = Date.now();
10167
+ response = await http.dynatracePlatform(
10168
+ "/platform/storage/query/v1/query:poll",
10169
+ {
10170
+ params: {
10171
+ "request-token": response.requestToken,
10172
+ "request-timeout": Math.min(5e3, remainingMs)
10173
+ },
10174
+ timeoutMs: 1e4
10175
+ }
10176
+ );
10177
+ if (response.state === "RUNNING" || response.state === "NOT_STARTED") {
10178
+ const delayMs = Math.min(1e3 - (Date.now() - pollStarted), deadline - Date.now());
10179
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
10180
+ }
10181
+ }
10182
+ if (response.state !== "SUCCEEDED") {
10183
+ throw new PncliError(response.error?.message ?? `Dynatrace trace query ${response.state.toLowerCase()}`);
10184
+ }
10185
+ return response.result;
10186
+ }
10187
+ function registerDynatraceCommands(program2) {
10188
+ const dynatrace = program2.command("dynatrace").description("Dynatrace observability operations");
10189
+ const entities = dynatrace.command("entities").description("Monitored entity operations");
10190
+ 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) => {
10191
+ const start = Date.now();
10192
+ try {
10193
+ const data = await listEntities(getHttp3(program2), opts.selector, opts);
10194
+ success({ totalCount: data.totalCount, entities: data.items }, "dynatrace", "entities list", start);
10195
+ } catch (err) {
10196
+ fail(err, "dynatrace", "entities list", start);
10197
+ }
10198
+ });
10199
+ 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) => {
10200
+ const start = Date.now();
10201
+ try {
10202
+ const data = await getHttp3(program2).dynatrace(
10203
+ `/api/v2/entities/${encodeURIComponent(opts.id)}`,
10204
+ { params: { from: opts.from, to: opts.to, fields: opts.fields } }
10205
+ );
10206
+ success(data, "dynatrace", "entities get", start);
10207
+ } catch (err) {
10208
+ fail(err, "dynatrace", "entities get", start);
10209
+ }
10210
+ });
10211
+ dynatrace.command("services").description("List monitored services").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
10212
+ const start = Date.now();
10213
+ try {
10214
+ const data = await listEntities(getHttp3(program2), 'type("SERVICE")', opts);
10215
+ success({ totalCount: data.totalCount, services: data.items }, "dynatrace", "services", start);
10216
+ } catch (err) {
10217
+ fail(err, "dynatrace", "services", start);
10218
+ }
10219
+ });
10220
+ dynatrace.command("workloads").description("List monitored Kubernetes workloads").option("--from <time>", "Start time").option("--to <time>", "End time").action(async (opts) => {
10221
+ const start = Date.now();
10222
+ try {
10223
+ const selector = 'type("CLOUD_APPLICATION")';
10224
+ const data = await listEntities(getHttp3(program2), selector, opts);
10225
+ success({ totalCount: data.totalCount, workloads: data.items }, "dynatrace", "workloads", start);
10226
+ } catch (err) {
10227
+ fail(err, "dynatrace", "workloads", start);
10228
+ }
10229
+ });
10230
+ const problems = dynatrace.command("problems").description("Problem operations");
10231
+ 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) => {
10232
+ const start = Date.now();
10233
+ try {
10234
+ const data = await allPages(async (nextPageKey) => {
10235
+ const page = await getHttp3(program2).dynatrace("/api/v2/problems", {
10236
+ params: nextPageKey ? { nextPageKey } : {
10237
+ from: opts.from ?? "now-2h",
10238
+ to: opts.to ?? "now",
10239
+ problemSelector: opts.problemSelector,
10240
+ entitySelector: opts.entitySelector,
10241
+ pageSize: 500
10242
+ }
10243
+ });
10244
+ return { items: page.problems, nextPageKey: page.nextPageKey, totalCount: page.totalCount };
10245
+ });
10246
+ success({ totalCount: data.totalCount, problems: data.items }, "dynatrace", "problems list", start);
10247
+ } catch (err) {
10248
+ fail(err, "dynatrace", "problems list", start);
10249
+ }
10250
+ });
10251
+ problems.command("get").description("Get a problem by ID").requiredOption("--id <id>", "Problem ID").option("--fields <fields>", "Additional fields").action(async (opts) => {
10252
+ const start = Date.now();
10253
+ try {
10254
+ const data = await getHttp3(program2).dynatrace(
10255
+ `/api/v2/problems/${encodeURIComponent(opts.id)}`,
10256
+ { params: { fields: opts.fields } }
10257
+ );
10258
+ success(data, "dynatrace", "problems get", start);
10259
+ } catch (err) {
10260
+ fail(err, "dynatrace", "problems get", start);
10261
+ }
10262
+ });
10263
+ dynatrace.command("trace").description("Get distributed-trace spans from Grail").requiredOption("--id <trace-id>", "16- or 32-character hexadecimal trace ID").action(async (opts) => {
10264
+ const start = Date.now();
10265
+ try {
10266
+ const result = await runTraceQuery(getHttp3(program2), opts.id);
10267
+ success(result, "dynatrace", "trace", start);
10268
+ } catch (err) {
10269
+ fail(err, "dynatrace", "trace", start);
10270
+ }
10271
+ });
10272
+ }
10273
+
9786
10274
  // src/cli.ts
9787
10275
  if (!process.env.PNCLI_VERIFY_TLS) {
9788
10276
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -9826,6 +10314,7 @@ registerSonatypeIqCommands(program);
9826
10314
  registerSkillsCommands(program);
9827
10315
  registerJwtCommands(program);
9828
10316
  registerOpenShiftCommands(program);
10317
+ registerDynatraceCommands(program);
9829
10318
  program.addHelpText("after", `
9830
10319
  Services:
9831
10320
  git Local git operations (status, diff, log, branch)
@@ -9840,11 +10329,12 @@ Services:
9840
10329
  jenkins Jenkins Data Center (jobs, builds, logs)
9841
10330
  artifactory Artifactory (repos, artifact search, build info, properties)
9842
10331
  udeploy IBM UrbanCode Deploy (component versions, deployment processes)
9843
- checkmarx Checkmarx CxSAST (projects, scans, scan statistics)
10332
+ checkmarx Checkmarx One (projects, scans, scan statistics)
9844
10333
  servicenow ServiceNow (change requests)
9845
10334
  contrast Contrast IAST (applications, vulnerability findings)
9846
10335
  sonatypeiq Sonatype IQ Server (applications, organizations, policies)
9847
10336
  openshift OpenShift / Kubernetes (pods, events, logs, metrics)
10337
+ dynatrace Dynatrace (services, entities, problems, traces, Kubernetes workloads)
9848
10338
  config Manage pncli configuration
9849
10339
  skills Download and manage Claude Code skills
9850
10340
  jwt JWT token utilities (decode header and payload)