@kolatts/pncli 1.13.0 → 1.14.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-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,26 @@ 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 mimeType = guessMimeType(filePath);
4448
+ const attachment = await this.http.adoUpload(
4449
+ `/${encodeURIComponent(collection)}/_apis/wit/attachments?fileName=${encodeURIComponent(fileName)}&api-version=${API6}`,
4450
+ fileContent,
4451
+ mimeType
4452
+ );
4453
+ const relationValue = {
4454
+ rel: "AttachedFile",
4455
+ url: attachment.url,
4456
+ ...comment ? { attributes: { comment } } : {}
4457
+ };
4458
+ await this.http.ado(
4459
+ `/${encodeURIComponent(collection)}/_apis/wit/workitems/${workItemId}?api-version=${API6}`,
4460
+ { method: "PATCH", body: [{ op: "add", path: "/relations/-", value: relationValue }], headers: { "Content-Type": "application/json-patch+json" } }
4461
+ );
4462
+ return attachment;
4463
+ }
4437
4464
  };
4438
4465
 
4439
4466
  // src/services/ado/discovery.ts
@@ -4687,6 +4714,16 @@ function registerConfigCommands(program2) {
4687
4714
  } else {
4688
4715
  results.sonatypeiq = { ok: null, message: "not configured" };
4689
4716
  }
4717
+ if (cfg.openshift.baseUrl && cfg.openshift.token) {
4718
+ try {
4719
+ await http.openshift("/api/v1");
4720
+ results.openshift = { ok: true, message: "connected" };
4721
+ } catch (err) {
4722
+ results.openshift = { ok: false, message: err instanceof Error ? err.message : String(err) };
4723
+ }
4724
+ } else {
4725
+ results.openshift = { ok: null, message: "not configured" };
4726
+ }
4690
4727
  success(results, "config", "test", start);
4691
4728
  } catch (err) {
4692
4729
  fail(err, "config", "test", start);
@@ -4870,11 +4907,23 @@ function registerConfigCommands(program2) {
4870
4907
  results.sonatypeiq = categorize2(err);
4871
4908
  }
4872
4909
  }
4910
+ if (!cfg.openshift.token) {
4911
+ results.openshift = { status: "blank", message: "not configured" };
4912
+ } else if (!cfg.openshift.baseUrl) {
4913
+ results.openshift = { status: "error", message: "baseUrl not configured" };
4914
+ } else {
4915
+ try {
4916
+ await http.openshift("/api/v1", { timeoutMs: 1e4 });
4917
+ results.openshift = { status: "valid", message: "ok" };
4918
+ } catch (err) {
4919
+ results.openshift = categorize2(err);
4920
+ }
4921
+ }
4873
4922
  const statuses = Object.values(results).map((r) => r.status);
4874
4923
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
4875
4924
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
4876
4925
  if (cmdOpts.output === "table") {
4877
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
4926
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
4878
4927
  const labelWidth = 14;
4879
4928
  const statusWidth = 9;
4880
4929
  for (const svc of services) {
@@ -4888,7 +4937,7 @@ function registerConfigCommands(program2) {
4888
4937
  process.stdout.write("\n");
4889
4938
  } else {
4890
4939
  if (opts.pretty) {
4891
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
4940
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq", "openshift"];
4892
4941
  const labelWidth = 14;
4893
4942
  const statusWidth = 9;
4894
4943
  for (const svc of services) {
@@ -5376,6 +5425,41 @@ async function initGlobalConfig(start) {
5376
5425
  }
5377
5426
  }
5378
5427
  }
5428
+ 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");
5429
+ const useOpenShift = await confirm({
5430
+ message: "Configure OpenShift / Kubernetes for pod health monitoring?",
5431
+ default: false
5432
+ });
5433
+ let openShiftBaseUrl = "";
5434
+ let openShiftToken = "";
5435
+ if (useOpenShift) {
5436
+ process.stderr.write(" Service account token: cat /var/run/secrets/kubernetes.io/serviceaccount/token\n");
5437
+ openShiftBaseUrl = await input({
5438
+ message: "OpenShift API server URL (e.g. https://api.cluster.example.com:6443):",
5439
+ default: ""
5440
+ });
5441
+ openShiftToken = await password({
5442
+ message: "Service account token:"
5443
+ });
5444
+ if (openShiftBaseUrl && openShiftToken) {
5445
+ process.stderr.write("\n Verifying connection...\n");
5446
+ try {
5447
+ const tempConfig = {
5448
+ ...loadConfig(),
5449
+ openshift: {
5450
+ baseUrl: normalizeBaseUrl(openShiftBaseUrl),
5451
+ token: openShiftToken
5452
+ }
5453
+ };
5454
+ const tempHttp = createHttpClient(tempConfig);
5455
+ await tempHttp.openshift("/api/v1");
5456
+ process.stderr.write(" Connected.\n");
5457
+ } catch (err) {
5458
+ warn(`Could not connect to OpenShift: ${err instanceof Error ? err.message : String(err)}`);
5459
+ warn("Config will be saved anyway. Check your URL and token and re-run pncli config init or pncli config test.");
5460
+ }
5461
+ }
5462
+ }
5379
5463
  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
5464
  const jiraProject = await input({
5381
5465
  message: "Default Jira project key (optional):",
@@ -5493,6 +5577,12 @@ async function initGlobalConfig(start) {
5493
5577
  passcode: sonatypeIqPasscode || void 0
5494
5578
  }
5495
5579
  } : {},
5580
+ ...useOpenShift && openShiftBaseUrl ? {
5581
+ openshift: {
5582
+ baseUrl: normalizeBaseUrl(openShiftBaseUrl),
5583
+ token: openShiftToken || void 0
5584
+ }
5585
+ } : {},
5496
5586
  defaults: {
5497
5587
  jira: {
5498
5588
  project: jiraProject || void 0
@@ -6117,6 +6207,16 @@ function registerAdoWorkCommands(ado) {
6117
6207
  fail(err, "ado", "work-list-attachments", start);
6118
6208
  }
6119
6209
  });
6210
+ 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) => {
6211
+ const start = Date.now();
6212
+ try {
6213
+ const { collection, workClient } = getAdoContext(ado);
6214
+ const data = await workClient.uploadAttachment(collection, parseInt(opts.id, 10), opts.file, opts.comment);
6215
+ success(data, "ado", "work-add-attachment", start);
6216
+ } catch (err) {
6217
+ fail(err, "ado", "work-add-attachment", start);
6218
+ }
6219
+ });
6120
6220
  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
6221
  const start = Date.now();
6122
6222
  try {
@@ -6455,11 +6555,11 @@ function registerAdoRepoCommands(ado) {
6455
6555
  const start = Date.now();
6456
6556
  try {
6457
6557
  const globalOpts = ado.optsWithGlobals();
6458
- const { loadConfig: lc } = await import("./config-OFLPO5DN.js");
6558
+ const { loadConfig: lc } = await import("./config-TL5X47RY.js");
6459
6559
  const config = lc({ configPath: globalOpts.config });
6460
6560
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
6461
6561
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
6462
- const { createHttpClient: createHttpClient2 } = await import("./http-BNCZJQTD.js");
6562
+ const { createHttpClient: createHttpClient2 } = await import("./http-UN6N4WIB.js");
6463
6563
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
6464
6564
  const coreClient = new AdoCoreClient2(http);
6465
6565
  const me = await coreClient.getConnectionData(collection);
@@ -7967,6 +8067,262 @@ function copyPluginSkills(skillsSrc, targetDir) {
7967
8067
  return { installed, failed };
7968
8068
  }
7969
8069
 
8070
+ // src/services/jwt/commands.ts
8071
+ function base64urlDecode(input2) {
8072
+ const base64 = input2.replace(/-/g, "+").replace(/_/g, "/");
8073
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
8074
+ return Buffer.from(padded, "base64").toString("utf8");
8075
+ }
8076
+ function decodeJwt(token) {
8077
+ const parts = token.split(".");
8078
+ if (parts.length !== 3) {
8079
+ throw new PncliError("Invalid JWT: expected 3 dot-separated parts", 1);
8080
+ }
8081
+ const [rawHeader, rawPayload, signature] = parts;
8082
+ let header;
8083
+ let payload;
8084
+ try {
8085
+ header = JSON.parse(base64urlDecode(rawHeader));
8086
+ } catch {
8087
+ throw new PncliError("Invalid JWT: header is not valid JSON", 1);
8088
+ }
8089
+ try {
8090
+ payload = JSON.parse(base64urlDecode(rawPayload));
8091
+ } catch {
8092
+ throw new PncliError("Invalid JWT: payload is not valid JSON", 1);
8093
+ }
8094
+ return { header, payload, signature };
8095
+ }
8096
+ function registerJwtCommands(program2) {
8097
+ const jwt = program2.command("jwt").description("JWT token utilities");
8098
+ jwt.command("decode <token>").description("Decode a JWT token and output header, payload, and signature as JSON").action((token) => {
8099
+ const start = Date.now();
8100
+ try {
8101
+ const data = decodeJwt(token);
8102
+ success(data, "jwt", "decode", start);
8103
+ } catch (err) {
8104
+ fail(err, "jwt", "decode", start);
8105
+ }
8106
+ });
8107
+ }
8108
+
8109
+ // src/services/openshift/commands.ts
8110
+ function containerState(s) {
8111
+ if (!s) return "unknown";
8112
+ if (s.waiting) return s.waiting.reason ?? "Waiting";
8113
+ if (s.terminated) return s.terminated.reason ?? "Terminated";
8114
+ if (s.running) return "Running";
8115
+ return "unknown";
8116
+ }
8117
+ function getHttp2(program2) {
8118
+ const opts = program2.optsWithGlobals();
8119
+ const config = loadConfig({ configPath: opts.config });
8120
+ if (!config.openshift.baseUrl) {
8121
+ throw new PncliError("OpenShift not configured. Run: pncli config init");
8122
+ }
8123
+ return createHttpClient(config, Boolean(opts.dryRun));
8124
+ }
8125
+ function podAge(creationTimestamp) {
8126
+ if (!creationTimestamp) return "unknown";
8127
+ const ageMs = Date.now() - new Date(creationTimestamp).getTime();
8128
+ const ageSeconds = Math.floor(ageMs / 1e3);
8129
+ if (ageSeconds < 60) return `${ageSeconds}s`;
8130
+ const ageMinutes = Math.floor(ageSeconds / 60);
8131
+ if (ageMinutes < 60) return `${ageMinutes}m`;
8132
+ const ageHours = Math.floor(ageMinutes / 60);
8133
+ if (ageHours < 24) return `${ageHours}h`;
8134
+ return `${Math.floor(ageHours / 24)}d`;
8135
+ }
8136
+ function registerOpenShiftCommands(program2) {
8137
+ const oc = program2.command("openshift").description("OpenShift / Kubernetes operations");
8138
+ 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) => {
8139
+ const start = Date.now();
8140
+ try {
8141
+ const http = getHttp2(program2);
8142
+ const params = {};
8143
+ if (opts.labelSelector) params["labelSelector"] = opts.labelSelector;
8144
+ const podList = await http.openshift(
8145
+ `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/pods`,
8146
+ { params }
8147
+ );
8148
+ const summaryCounters = {
8149
+ total: podList.items.length,
8150
+ running: 0,
8151
+ pending: 0,
8152
+ failed: 0,
8153
+ succeeded: 0,
8154
+ unknown: 0,
8155
+ crashLoopBackOff: 0,
8156
+ oomKilled: 0,
8157
+ imagePullBackOff: 0,
8158
+ totalRestarts: 0
8159
+ };
8160
+ const pods = podList.items.map((pod) => {
8161
+ const phase = pod.status.phase ?? "Unknown";
8162
+ switch (phase.toLowerCase()) {
8163
+ case "running":
8164
+ summaryCounters.running++;
8165
+ break;
8166
+ case "pending":
8167
+ summaryCounters.pending++;
8168
+ break;
8169
+ case "failed":
8170
+ summaryCounters.failed++;
8171
+ break;
8172
+ case "succeeded":
8173
+ summaryCounters.succeeded++;
8174
+ break;
8175
+ default:
8176
+ summaryCounters.unknown++;
8177
+ break;
8178
+ }
8179
+ const allStatuses = [
8180
+ ...pod.status.containerStatuses ?? [],
8181
+ ...pod.status.initContainerStatuses ?? []
8182
+ ];
8183
+ let podRestarts = 0;
8184
+ const containers = allStatuses.map((cs) => {
8185
+ podRestarts += cs.restartCount;
8186
+ summaryCounters.totalRestarts += cs.restartCount;
8187
+ const currentState = containerState(cs.state);
8188
+ const lastState = containerState(cs.lastState);
8189
+ if (currentState === "CrashLoopBackOff") summaryCounters.crashLoopBackOff++;
8190
+ if (currentState === "ImagePullBackOff" || currentState === "ErrImagePull") summaryCounters.imagePullBackOff++;
8191
+ if (lastState === "OOMKilled" || cs.lastState?.terminated?.reason === "OOMKilled") summaryCounters.oomKilled++;
8192
+ const entry2 = {
8193
+ name: cs.name,
8194
+ ready: cs.ready,
8195
+ restartCount: cs.restartCount,
8196
+ state: currentState
8197
+ };
8198
+ if (cs.lastState?.terminated) {
8199
+ entry2["lastExitCode"] = cs.lastState.terminated.exitCode;
8200
+ entry2["lastReason"] = cs.lastState.terminated.reason;
8201
+ }
8202
+ return entry2;
8203
+ });
8204
+ const readyCount = allStatuses.filter((cs) => cs.ready).length;
8205
+ const totalCount = pod.spec.containers?.length ?? allStatuses.length;
8206
+ const entry = {
8207
+ name: pod.metadata.name,
8208
+ phase,
8209
+ ready: `${readyCount}/${totalCount}`,
8210
+ restartCount: podRestarts,
8211
+ age: podAge(pod.metadata.creationTimestamp)
8212
+ };
8213
+ const podReason = pod.status.reason ?? pod.status.conditions?.find((c) => c.status === "False")?.reason;
8214
+ if (podReason) entry["reason"] = podReason;
8215
+ if (pod.status.message) entry["message"] = pod.status.message;
8216
+ if (containers.length > 0) entry["containers"] = containers;
8217
+ if (pod.metadata.labels && Object.keys(pod.metadata.labels).length > 0) {
8218
+ entry["labels"] = pod.metadata.labels;
8219
+ }
8220
+ return entry;
8221
+ });
8222
+ pods.sort((a, b) => {
8223
+ const phasePriority = (p) => {
8224
+ if (p === "Failed") return 0;
8225
+ if (p === "Pending") return 1;
8226
+ if (p === "Unknown") return 2;
8227
+ return 3;
8228
+ };
8229
+ const pDiff = phasePriority(a["phase"]) - phasePriority(b["phase"]);
8230
+ if (pDiff !== 0) return pDiff;
8231
+ return (b["restartCount"] ?? 0) - (a["restartCount"] ?? 0);
8232
+ });
8233
+ success({ namespace: opts.namespace, summary: summaryCounters, pods }, "openshift", "pods", start);
8234
+ } catch (err) {
8235
+ fail(err, "openshift", "pods", start);
8236
+ }
8237
+ });
8238
+ 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) => {
8239
+ const start = Date.now();
8240
+ try {
8241
+ const http = getHttp2(program2);
8242
+ const params = {};
8243
+ const selectors = [];
8244
+ if (!opts.all) selectors.push("type=Warning");
8245
+ if (opts.fieldSelector) selectors.push(opts.fieldSelector);
8246
+ if (selectors.length > 0) params["fieldSelector"] = selectors.join(",");
8247
+ const eventList = await http.openshift(
8248
+ `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/events`,
8249
+ { params }
8250
+ );
8251
+ const events = eventList.items.map((ev) => {
8252
+ const obj = ev.involvedObject ?? ev.regarding;
8253
+ const entry = {
8254
+ type: ev.type ?? "Normal",
8255
+ reason: ev.reason,
8256
+ object: obj ? `${obj.kind ?? "Object"}/${obj.name ?? "?"}` : void 0,
8257
+ message: ev.message,
8258
+ count: ev.count ?? 1,
8259
+ firstTime: ev.firstTimestamp ?? ev.eventTime,
8260
+ lastTime: ev.lastTimestamp ?? ev.eventTime
8261
+ };
8262
+ const source = ev.source?.component ?? ev.reportingComponent;
8263
+ if (source) entry["source"] = source;
8264
+ return entry;
8265
+ });
8266
+ events.sort((a, b) => (b["count"] ?? 0) - (a["count"] ?? 0));
8267
+ success({ namespace: opts.namespace, count: events.length, events }, "openshift", "events", start);
8268
+ } catch (err) {
8269
+ fail(err, "openshift", "events", start);
8270
+ }
8271
+ });
8272
+ 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) => {
8273
+ const start = Date.now();
8274
+ try {
8275
+ const http = getHttp2(program2);
8276
+ const lines = opts.lines ? parseInt(opts.lines, 10) : 100;
8277
+ const logText = await http.openshiftText(
8278
+ `/api/v1/namespaces/${encodeURIComponent(opts.namespace)}/pods/${encodeURIComponent(opts.pod)}/log`,
8279
+ {
8280
+ lines,
8281
+ params: {
8282
+ ...opts.container ? { container: opts.container } : {},
8283
+ ...opts.previous ? { previous: "true" } : {}
8284
+ }
8285
+ }
8286
+ );
8287
+ success(
8288
+ {
8289
+ pod: opts.pod,
8290
+ namespace: opts.namespace,
8291
+ container: opts.container ?? "(default)",
8292
+ previous: opts.previous ?? false,
8293
+ lines,
8294
+ log: logText
8295
+ },
8296
+ "openshift",
8297
+ "logs",
8298
+ start
8299
+ );
8300
+ } catch (err) {
8301
+ fail(err, "openshift", "logs", start);
8302
+ }
8303
+ });
8304
+ 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) => {
8305
+ const start = Date.now();
8306
+ try {
8307
+ const http = getHttp2(program2);
8308
+ const metricsList = await http.openshift(
8309
+ `/apis/metrics.k8s.io/v1beta1/namespaces/${encodeURIComponent(opts.namespace)}/pods`
8310
+ );
8311
+ const pods = metricsList.items.map((pm) => ({
8312
+ name: pm.metadata.name,
8313
+ containers: pm.containers.map((c) => ({
8314
+ name: c.name,
8315
+ cpu: c.usage.cpu,
8316
+ memory: c.usage.memory
8317
+ }))
8318
+ }));
8319
+ success({ namespace: opts.namespace, pods }, "openshift", "pod-metrics", start);
8320
+ } catch (err) {
8321
+ fail(err, "openshift", "pod-metrics", start);
8322
+ }
8323
+ });
8324
+ }
8325
+
7970
8326
  // src/cli.ts
7971
8327
  if (!process.env.PNCLI_VERIFY_TLS) {
7972
8328
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -8006,6 +8362,8 @@ registerServiceNowCommands(program);
8006
8362
  registerContrastCommands(program);
8007
8363
  registerSonatypeIqCommands(program);
8008
8364
  registerSkillsCommands(program);
8365
+ registerJwtCommands(program);
8366
+ registerOpenShiftCommands(program);
8009
8367
  program.addHelpText("after", `
8010
8368
  Services:
8011
8369
  git Local git operations (status, diff, log, branch)
@@ -8023,8 +8381,10 @@ Services:
8023
8381
  servicenow ServiceNow (change requests)
8024
8382
  contrast Contrast IAST (applications, vulnerability findings)
8025
8383
  sonatypeiq Sonatype IQ Server (applications, organizations, policies)
8384
+ openshift OpenShift / Kubernetes (pods, events, logs, metrics)
8026
8385
  config Manage pncli configuration
8027
8386
  skills Download and manage Claude Code skills
8387
+ jwt JWT token utilities (decode header and payload)
8028
8388
  `);
8029
8389
  program.parseAsync(process.argv).catch((err) => {
8030
8390
  if (process.exitCode !== void 0) return;