@kolatts/pncli 1.13.0 → 1.14.1

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-AWSXVWRN.js";
11
+ } from "./chunk-CFUUOW7B.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-BNT4SEZD.js";
23
+ } from "./chunk-BNMFIGXF.js";
24
24
  import {
25
25
  AdoCoreClient
26
26
  } from "./chunk-VDL32PSG.js";
@@ -798,7 +798,39 @@ import path from "path";
798
798
 
799
799
  // src/services/jira/client.ts
800
800
  import { readFileSync } from "fs";
801
- import { basename, extname } from "path";
801
+ import { basename } from "path";
802
+
803
+ // src/lib/mime.ts
804
+ import { extname } from "path";
805
+ function guessMimeType(filePath) {
806
+ const ext = extname(filePath).toLowerCase();
807
+ const map = {
808
+ ".jpg": "image/jpeg",
809
+ ".jpeg": "image/jpeg",
810
+ ".png": "image/png",
811
+ ".gif": "image/gif",
812
+ ".svg": "image/svg+xml",
813
+ ".pdf": "application/pdf",
814
+ ".txt": "text/plain",
815
+ ".log": "text/plain",
816
+ ".csv": "text/csv",
817
+ ".json": "application/json",
818
+ ".xml": "application/xml",
819
+ ".zip": "application/zip",
820
+ ".tar": "application/x-tar",
821
+ ".gz": "application/gzip",
822
+ ".md": "text/markdown",
823
+ ".html": "text/html",
824
+ ".htm": "text/html",
825
+ ".doc": "application/msword",
826
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
827
+ ".xls": "application/vnd.ms-excel",
828
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
829
+ };
830
+ return map[ext] ?? "application/octet-stream";
831
+ }
832
+
833
+ // src/services/jira/client.ts
802
834
  var API2 = "/rest/api/2";
803
835
  var JiraClient = class {
804
836
  constructor(http) {
@@ -996,33 +1028,6 @@ var JiraClient = class {
996
1028
  });
997
1029
  }
998
1030
  };
999
- function guessMimeType(filePath) {
1000
- const ext = extname(filePath).toLowerCase();
1001
- const map = {
1002
- ".jpg": "image/jpeg",
1003
- ".jpeg": "image/jpeg",
1004
- ".png": "image/png",
1005
- ".gif": "image/gif",
1006
- ".svg": "image/svg+xml",
1007
- ".pdf": "application/pdf",
1008
- ".txt": "text/plain",
1009
- ".log": "text/plain",
1010
- ".csv": "text/csv",
1011
- ".json": "application/json",
1012
- ".xml": "application/xml",
1013
- ".zip": "application/zip",
1014
- ".tar": "application/x-tar",
1015
- ".gz": "application/gzip",
1016
- ".md": "text/markdown",
1017
- ".html": "text/html",
1018
- ".htm": "text/html",
1019
- ".doc": "application/msword",
1020
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1021
- ".xls": "application/vnd.ms-excel",
1022
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1023
- };
1024
- return map[ext] ?? "application/octet-stream";
1025
- }
1026
1031
  function schemaToPncliType(schema) {
1027
1032
  if (!schema) return void 0;
1028
1033
  const custom = schema.custom ?? "";
@@ -4331,6 +4336,8 @@ import password from "@inquirer/password";
4331
4336
  import confirm from "@inquirer/confirm";
4332
4337
 
4333
4338
  // src/services/ado/client/work.ts
4339
+ import { readFileSync as readFileSync3 } from "fs";
4340
+ import { basename as basename2 } from "path";
4334
4341
  var API6 = "7.1";
4335
4342
  var API_PREVIEW = "7.1-preview.4";
4336
4343
  var AdoWorkClient = class {
@@ -4434,6 +4441,25 @@ var AdoWorkClient = class {
4434
4441
  async downloadAttachment(absoluteUrl) {
4435
4442
  return this.http.adoBuffer(absoluteUrl);
4436
4443
  }
4444
+ async uploadAttachment(collection, workItemId, filePath, comment) {
4445
+ const fileContent = readFileSync3(filePath);
4446
+ const fileName = basename2(filePath);
4447
+ const attachment = await this.http.adoUpload(
4448
+ `/${encodeURIComponent(collection)}/_apis/wit/attachments?fileName=${encodeURIComponent(fileName)}&api-version=${API6}`,
4449
+ fileContent,
4450
+ "application/octet-stream"
4451
+ );
4452
+ const relationValue = {
4453
+ rel: "AttachedFile",
4454
+ url: attachment.url,
4455
+ ...comment ? { attributes: { comment } } : {}
4456
+ };
4457
+ await this.http.ado(
4458
+ `/${encodeURIComponent(collection)}/_apis/wit/workitems/${workItemId}?api-version=${API6}`,
4459
+ { method: "PATCH", body: [{ op: "add", path: "/relations/-", value: relationValue }], headers: { "Content-Type": "application/json-patch+json" } }
4460
+ );
4461
+ return attachment;
4462
+ }
4437
4463
  };
4438
4464
 
4439
4465
  // src/services/ado/discovery.ts
@@ -4687,6 +4713,16 @@ function registerConfigCommands(program2) {
4687
4713
  } else {
4688
4714
  results.sonatypeiq = { ok: null, message: "not configured" };
4689
4715
  }
4716
+ if (cfg.openshift.baseUrl && cfg.openshift.token) {
4717
+ try {
4718
+ await http.openshift("/api/v1");
4719
+ results.openshift = { ok: true, message: "connected" };
4720
+ } catch (err) {
4721
+ results.openshift = { ok: false, message: err instanceof Error ? err.message : String(err) };
4722
+ }
4723
+ } else {
4724
+ results.openshift = { ok: null, message: "not configured" };
4725
+ }
4690
4726
  success(results, "config", "test", start);
4691
4727
  } catch (err) {
4692
4728
  fail(err, "config", "test", start);
@@ -4870,11 +4906,23 @@ function registerConfigCommands(program2) {
4870
4906
  results.sonatypeiq = categorize2(err);
4871
4907
  }
4872
4908
  }
4909
+ if (!cfg.openshift.token) {
4910
+ results.openshift = { status: "blank", message: "not configured" };
4911
+ } else if (!cfg.openshift.baseUrl) {
4912
+ results.openshift = { status: "error", message: "baseUrl not configured" };
4913
+ } else {
4914
+ try {
4915
+ await http.openshift("/api/v1", { timeoutMs: 1e4 });
4916
+ results.openshift = { status: "valid", message: "ok" };
4917
+ } catch (err) {
4918
+ results.openshift = categorize2(err);
4919
+ }
4920
+ }
4873
4921
  const statuses = Object.values(results).map((r) => r.status);
4874
4922
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
4875
4923
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
4876
4924
  if (cmdOpts.output === "table") {
4877
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
4925
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
4878
4926
  const labelWidth = 14;
4879
4927
  const statusWidth = 9;
4880
4928
  for (const svc of services) {
@@ -4888,7 +4936,7 @@ function registerConfigCommands(program2) {
4888
4936
  process.stdout.write("\n");
4889
4937
  } else {
4890
4938
  if (opts.pretty) {
4891
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
4939
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
4892
4940
  const labelWidth = 14;
4893
4941
  const statusWidth = 9;
4894
4942
  for (const svc of services) {
@@ -5376,6 +5424,41 @@ async function initGlobalConfig(start) {
5376
5424
  }
5377
5425
  }
5378
5426
  }
5427
+ process.stderr.write("\n\u2500\u2500 OpenShift / Kubernetes \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");
5428
+ const useOpenShift = await confirm({
5429
+ message: "Configure OpenShift / Kubernetes for pod health monitoring?",
5430
+ default: false
5431
+ });
5432
+ let openShiftBaseUrl = "";
5433
+ let openShiftToken = "";
5434
+ if (useOpenShift) {
5435
+ process.stderr.write(" Service account token: cat /var/run/secrets/kubernetes.io/serviceaccount/token\n");
5436
+ openShiftBaseUrl = await input({
5437
+ message: "OpenShift API server URL (e.g. https://api.cluster.example.com:6443):",
5438
+ default: ""
5439
+ });
5440
+ openShiftToken = await password({
5441
+ message: "Service account token:"
5442
+ });
5443
+ if (openShiftBaseUrl && openShiftToken) {
5444
+ process.stderr.write("\n Verifying connection...\n");
5445
+ try {
5446
+ const tempConfig = {
5447
+ ...loadConfig(),
5448
+ openshift: {
5449
+ baseUrl: normalizeBaseUrl(openShiftBaseUrl),
5450
+ token: openShiftToken
5451
+ }
5452
+ };
5453
+ const tempHttp = createHttpClient(tempConfig);
5454
+ await tempHttp.openshift("/api/v1");
5455
+ process.stderr.write(" Connected.\n");
5456
+ } catch (err) {
5457
+ warn(`Could not connect to OpenShift: ${err instanceof Error ? err.message : String(err)}`);
5458
+ warn("Config will be saved anyway. Check your URL and token and re-run pncli config init or pncli config test.");
5459
+ }
5460
+ }
5461
+ }
5379
5462
  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");
5380
5463
  const jiraProject = await input({
5381
5464
  message: "Default Jira project key (optional):",
@@ -5493,6 +5576,12 @@ async function initGlobalConfig(start) {
5493
5576
  passcode: sonatypeIqPasscode || void 0
5494
5577
  }
5495
5578
  } : {},
5579
+ ...useOpenShift && openShiftBaseUrl ? {
5580
+ openshift: {
5581
+ baseUrl: normalizeBaseUrl(openShiftBaseUrl),
5582
+ token: openShiftToken || void 0
5583
+ }
5584
+ } : {},
5496
5585
  defaults: {
5497
5586
  jira: {
5498
5587
  project: jiraProject || void 0
@@ -6117,6 +6206,16 @@ function registerAdoWorkCommands(ado) {
6117
6206
  fail(err, "ado", "work-list-attachments", start);
6118
6207
  }
6119
6208
  });
6209
+ work.command("add-attachment").description("Upload a local file and attach it to a work item").requiredOption("--id <n>", "Work item ID").requiredOption("--file <path>", "Path to the file to upload").option("--comment <text>", "Optional comment for the attachment").action(async (opts) => {
6210
+ const start = Date.now();
6211
+ try {
6212
+ const { collection, workClient } = getAdoContext(ado);
6213
+ const data = await workClient.uploadAttachment(collection, parseInt(opts.id, 10), opts.file, opts.comment);
6214
+ success(data, "ado", "work-add-attachment", start);
6215
+ } catch (err) {
6216
+ fail(err, "ado", "work-add-attachment", start);
6217
+ }
6218
+ });
6120
6219
  work.command("download-attachment").description("Download a work item attachment to .pncli/ (or --dir)").requiredOption("--id <n>", "Work item ID").requiredOption("--attachment-id <guid>", "Attachment ID from work list-attachments output").option("--dir <path>", "Output directory (default: .pncli relative to cwd)").action(async (opts) => {
6121
6220
  const start = Date.now();
6122
6221
  try {
@@ -6455,11 +6554,11 @@ function registerAdoRepoCommands(ado) {
6455
6554
  const start = Date.now();
6456
6555
  try {
6457
6556
  const globalOpts = ado.optsWithGlobals();
6458
- const { loadConfig: lc } = await import("./config-OFLPO5DN.js");
6557
+ const { loadConfig: lc } = await import("./config-TL5X47RY.js");
6459
6558
  const config = lc({ configPath: globalOpts.config });
6460
6559
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
6461
6560
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
6462
- const { createHttpClient: createHttpClient2 } = await import("./http-BNCZJQTD.js");
6561
+ const { createHttpClient: createHttpClient2 } = await import("./http-UN6N4WIB.js");
6463
6562
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
6464
6563
  const coreClient = new AdoCoreClient2(http);
6465
6564
  const me = await coreClient.getConnectionData(collection);
@@ -7967,6 +8066,262 @@ function copyPluginSkills(skillsSrc, targetDir) {
7967
8066
  return { installed, failed };
7968
8067
  }
7969
8068
 
8069
+ // src/services/jwt/commands.ts
8070
+ function base64urlDecode(input2) {
8071
+ const base64 = input2.replace(/-/g, "+").replace(/_/g, "/");
8072
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
8073
+ return Buffer.from(padded, "base64").toString("utf8");
8074
+ }
8075
+ function decodeJwt(token) {
8076
+ const parts = token.split(".");
8077
+ if (parts.length !== 3) {
8078
+ throw new PncliError("Invalid JWT: expected 3 dot-separated parts", 1);
8079
+ }
8080
+ const [rawHeader, rawPayload, signature] = parts;
8081
+ let header;
8082
+ let payload;
8083
+ try {
8084
+ header = JSON.parse(base64urlDecode(rawHeader));
8085
+ } catch {
8086
+ throw new PncliError("Invalid JWT: header is not valid JSON", 1);
8087
+ }
8088
+ try {
8089
+ payload = JSON.parse(base64urlDecode(rawPayload));
8090
+ } catch {
8091
+ throw new PncliError("Invalid JWT: payload is not valid JSON", 1);
8092
+ }
8093
+ return { header, payload, signature };
8094
+ }
8095
+ function registerJwtCommands(program2) {
8096
+ const jwt = program2.command("jwt").description("JWT token utilities");
8097
+ jwt.command("decode <token>").description("Decode a JWT token and output header, payload, and signature as JSON").action((token) => {
8098
+ const start = Date.now();
8099
+ try {
8100
+ const data = decodeJwt(token);
8101
+ success(data, "jwt", "decode", start);
8102
+ } catch (err) {
8103
+ fail(err, "jwt", "decode", start);
8104
+ }
8105
+ });
8106
+ }
8107
+
8108
+ // src/services/openshift/commands.ts
8109
+ function containerState(s) {
8110
+ if (!s) return "unknown";
8111
+ if (s.waiting) return s.waiting.reason ?? "Waiting";
8112
+ if (s.terminated) return s.terminated.reason ?? "Terminated";
8113
+ if (s.running) return "Running";
8114
+ return "unknown";
8115
+ }
8116
+ function getHttp2(program2) {
8117
+ const opts = program2.optsWithGlobals();
8118
+ const config = loadConfig({ configPath: opts.config });
8119
+ if (!config.openshift.baseUrl) {
8120
+ throw new PncliError("OpenShift not configured. Run: pncli config init");
8121
+ }
8122
+ return createHttpClient(config, Boolean(opts.dryRun));
8123
+ }
8124
+ function podAge(creationTimestamp) {
8125
+ if (!creationTimestamp) return "unknown";
8126
+ const ageMs = Date.now() - new Date(creationTimestamp).getTime();
8127
+ const ageSeconds = Math.floor(ageMs / 1e3);
8128
+ if (ageSeconds < 60) return `${ageSeconds}s`;
8129
+ const ageMinutes = Math.floor(ageSeconds / 60);
8130
+ if (ageMinutes < 60) return `${ageMinutes}m`;
8131
+ const ageHours = Math.floor(ageMinutes / 60);
8132
+ if (ageHours < 24) return `${ageHours}h`;
8133
+ return `${Math.floor(ageHours / 24)}d`;
8134
+ }
8135
+ function registerOpenShiftCommands(program2) {
8136
+ const oc = program2.command("openshift").description("OpenShift / Kubernetes operations");
8137
+ oc.command("pods").description("List pod health summary for a namespace \u2014 pre-processed for LLM analysis").requiredOption("--namespace <ns>", "Kubernetes namespace").option("--label-selector <selector>", "Label selector (e.g. app=my-app)").action(async (opts) => {
8138
+ const start = Date.now();
8139
+ try {
8140
+ const http = getHttp2(program2);
8141
+ const params = {};
8142
+ if (opts.labelSelector) params["labelSelector"] = opts.labelSelector;
8143
+ const podList = await http.openshift(
8144
+ `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/pods`,
8145
+ { params }
8146
+ );
8147
+ const summaryCounters = {
8148
+ total: podList.items.length,
8149
+ running: 0,
8150
+ pending: 0,
8151
+ failed: 0,
8152
+ succeeded: 0,
8153
+ unknown: 0,
8154
+ crashLoopBackOff: 0,
8155
+ oomKilled: 0,
8156
+ imagePullBackOff: 0,
8157
+ totalRestarts: 0
8158
+ };
8159
+ const pods = podList.items.map((pod) => {
8160
+ const phase = pod.status.phase ?? "Unknown";
8161
+ switch (phase.toLowerCase()) {
8162
+ case "running":
8163
+ summaryCounters.running++;
8164
+ break;
8165
+ case "pending":
8166
+ summaryCounters.pending++;
8167
+ break;
8168
+ case "failed":
8169
+ summaryCounters.failed++;
8170
+ break;
8171
+ case "succeeded":
8172
+ summaryCounters.succeeded++;
8173
+ break;
8174
+ default:
8175
+ summaryCounters.unknown++;
8176
+ break;
8177
+ }
8178
+ const allStatuses = [
8179
+ ...pod.status.containerStatuses ?? [],
8180
+ ...pod.status.initContainerStatuses ?? []
8181
+ ];
8182
+ let podRestarts = 0;
8183
+ const containers = allStatuses.map((cs) => {
8184
+ podRestarts += cs.restartCount;
8185
+ summaryCounters.totalRestarts += cs.restartCount;
8186
+ const currentState = containerState(cs.state);
8187
+ const lastState = containerState(cs.lastState);
8188
+ if (currentState === "CrashLoopBackOff") summaryCounters.crashLoopBackOff++;
8189
+ if (currentState === "ImagePullBackOff" || currentState === "ErrImagePull") summaryCounters.imagePullBackOff++;
8190
+ if (lastState === "OOMKilled" || cs.lastState?.terminated?.reason === "OOMKilled") summaryCounters.oomKilled++;
8191
+ const entry2 = {
8192
+ name: cs.name,
8193
+ ready: cs.ready,
8194
+ restartCount: cs.restartCount,
8195
+ state: currentState
8196
+ };
8197
+ if (cs.lastState?.terminated) {
8198
+ entry2["lastExitCode"] = cs.lastState.terminated.exitCode;
8199
+ entry2["lastReason"] = cs.lastState.terminated.reason;
8200
+ }
8201
+ return entry2;
8202
+ });
8203
+ const readyCount = allStatuses.filter((cs) => cs.ready).length;
8204
+ const totalCount = pod.spec.containers?.length ?? allStatuses.length;
8205
+ const entry = {
8206
+ name: pod.metadata.name,
8207
+ phase,
8208
+ ready: `${readyCount}/${totalCount}`,
8209
+ restartCount: podRestarts,
8210
+ age: podAge(pod.metadata.creationTimestamp)
8211
+ };
8212
+ const podReason = pod.status.reason ?? pod.status.conditions?.find((c) => c.status === "False")?.reason;
8213
+ if (podReason) entry["reason"] = podReason;
8214
+ if (pod.status.message) entry["message"] = pod.status.message;
8215
+ if (containers.length > 0) entry["containers"] = containers;
8216
+ if (pod.metadata.labels && Object.keys(pod.metadata.labels).length > 0) {
8217
+ entry["labels"] = pod.metadata.labels;
8218
+ }
8219
+ return entry;
8220
+ });
8221
+ pods.sort((a, b) => {
8222
+ const phasePriority = (p) => {
8223
+ if (p === "Failed") return 0;
8224
+ if (p === "Pending") return 1;
8225
+ if (p === "Unknown") return 2;
8226
+ return 3;
8227
+ };
8228
+ const pDiff = phasePriority(a["phase"]) - phasePriority(b["phase"]);
8229
+ if (pDiff !== 0) return pDiff;
8230
+ return (b["restartCount"] ?? 0) - (a["restartCount"] ?? 0);
8231
+ });
8232
+ success({ namespace: opts.namespace, summary: summaryCounters, pods }, "openshift", "pods", start);
8233
+ } catch (err) {
8234
+ fail(err, "openshift", "pods", start);
8235
+ }
8236
+ });
8237
+ oc.command("events").description("List Warning events for a namespace \u2014 pre-filtered for LLM analysis").requiredOption("--namespace <ns>", "Kubernetes namespace").option("--field-selector <selector>", "Additional field selector (e.g. involvedObject.name=my-pod)").option("--all", "Include Normal events in addition to Warning events", false).action(async (opts) => {
8238
+ const start = Date.now();
8239
+ try {
8240
+ const http = getHttp2(program2);
8241
+ const params = {};
8242
+ const selectors = [];
8243
+ if (!opts.all) selectors.push("type=Warning");
8244
+ if (opts.fieldSelector) selectors.push(opts.fieldSelector);
8245
+ if (selectors.length > 0) params["fieldSelector"] = selectors.join(",");
8246
+ const eventList = await http.openshift(
8247
+ `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/events`,
8248
+ { params }
8249
+ );
8250
+ const events = eventList.items.map((ev) => {
8251
+ const obj = ev.involvedObject ?? ev.regarding;
8252
+ const entry = {
8253
+ type: ev.type ?? "Normal",
8254
+ reason: ev.reason,
8255
+ object: obj ? `${obj.kind ?? "Object"}/${obj.name ?? "?"}` : void 0,
8256
+ message: ev.message,
8257
+ count: ev.count ?? 1,
8258
+ firstTime: ev.firstTimestamp ?? ev.eventTime,
8259
+ lastTime: ev.lastTimestamp ?? ev.eventTime
8260
+ };
8261
+ const source = ev.source?.component ?? ev.reportingComponent;
8262
+ if (source) entry["source"] = source;
8263
+ return entry;
8264
+ });
8265
+ events.sort((a, b) => (b["count"] ?? 0) - (a["count"] ?? 0));
8266
+ success({ namespace: opts.namespace, count: events.length, events }, "openshift", "events", start);
8267
+ } catch (err) {
8268
+ fail(err, "openshift", "events", start);
8269
+ }
8270
+ });
8271
+ oc.command("logs").description("Get recent log lines from a pod container").requiredOption("--namespace <ns>", "Kubernetes namespace").requiredOption("--pod <name>", "Pod name").option("--container <name>", "Container name (defaults to first container)").option("--lines <n>", "Number of most-recent lines to return", "100").option("--previous", "Return logs from the previous container instance", false).action(async (opts) => {
8272
+ const start = Date.now();
8273
+ try {
8274
+ const http = getHttp2(program2);
8275
+ const lines = opts.lines ? parseInt(opts.lines, 10) : 100;
8276
+ const logText = await http.openshiftText(
8277
+ `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/pods/${encodeURIComponent(opts.pod)}/log`,
8278
+ {
8279
+ lines,
8280
+ params: {
8281
+ ...opts.container ? { container: opts.container } : {},
8282
+ ...opts.previous ? { previous: "true" } : {}
8283
+ }
8284
+ }
8285
+ );
8286
+ success(
8287
+ {
8288
+ pod: opts.pod,
8289
+ namespace: opts.namespace,
8290
+ container: opts.container ?? "(default)",
8291
+ previous: opts.previous ?? false,
8292
+ lines,
8293
+ log: logText
8294
+ },
8295
+ "openshift",
8296
+ "logs",
8297
+ start
8298
+ );
8299
+ } catch (err) {
8300
+ fail(err, "openshift", "logs", start);
8301
+ }
8302
+ });
8303
+ oc.command("pod-metrics").description("Get CPU and memory usage for pods in a namespace (requires metrics-server)").requiredOption("--namespace <ns>", "Kubernetes namespace").action(async (opts) => {
8304
+ const start = Date.now();
8305
+ try {
8306
+ const http = getHttp2(program2);
8307
+ const metricsList = await http.openshift(
8308
+ `/apis/metrics.k8s.io/v1beta1/namespaces/${encodeURIComponent(opts.namespace)}/pods`
8309
+ );
8310
+ const pods = metricsList.items.map((pm) => ({
8311
+ name: pm.metadata.name,
8312
+ containers: pm.containers.map((c) => ({
8313
+ name: c.name,
8314
+ cpu: c.usage.cpu,
8315
+ memory: c.usage.memory
8316
+ }))
8317
+ }));
8318
+ success({ namespace: opts.namespace, pods }, "openshift", "pod-metrics", start);
8319
+ } catch (err) {
8320
+ fail(err, "openshift", "pod-metrics", start);
8321
+ }
8322
+ });
8323
+ }
8324
+
7970
8325
  // src/cli.ts
7971
8326
  if (!process.env.PNCLI_VERIFY_TLS) {
7972
8327
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -8006,6 +8361,8 @@ registerServiceNowCommands(program);
8006
8361
  registerContrastCommands(program);
8007
8362
  registerSonatypeIqCommands(program);
8008
8363
  registerSkillsCommands(program);
8364
+ registerJwtCommands(program);
8365
+ registerOpenShiftCommands(program);
8009
8366
  program.addHelpText("after", `
8010
8367
  Services:
8011
8368
  git Local git operations (status, diff, log, branch)
@@ -8023,8 +8380,10 @@ Services:
8023
8380
  servicenow ServiceNow (change requests)
8024
8381
  contrast Contrast IAST (applications, vulnerability findings)
8025
8382
  sonatypeiq Sonatype IQ Server (applications, organizations, policies)
8383
+ openshift OpenShift / Kubernetes (pods, events, logs, metrics)
8026
8384
  config Manage pncli configuration
8027
8385
  skills Download and manage Claude Code skills
8386
+ jwt JWT token utilities (decode header and payload)
8028
8387
  `);
8029
8388
  program.parseAsync(process.argv).catch((err) => {
8030
8389
  if (process.exitCode !== void 0) return;