@kolatts/pncli 1.12.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,
@@ -18,8 +18,9 @@ import {
18
18
  setGlobalOptions,
19
19
  setGlobalUser,
20
20
  success,
21
- warn
22
- } from "./chunk-SLQOSGZJ.js";
21
+ warn,
22
+ writeRawOutput
23
+ } from "./chunk-BNMFIGXF.js";
23
24
  import {
24
25
  AdoCoreClient
25
26
  } from "./chunk-VDL32PSG.js";
@@ -276,21 +277,30 @@ function parseBranchReportOutput(raw, branchName, opts) {
276
277
  }
277
278
  }
278
279
  if (current) commits.push(current);
279
- const totalInsertions = commits.reduce((sum, c) => sum + c.insertions, 0);
280
- const totalDeletions = commits.reduce((sum, c) => sum + c.deletions, 0);
280
+ const isAbsoluteDate = (s) => /^\d{4}-\d{2}-\d{2}/.test(s);
281
+ const sinceDate = opts.since && isAbsoluteDate(opts.since) ? opts.since.slice(0, 10) : null;
282
+ const untilDate = opts.until && isAbsoluteDate(opts.until) ? opts.until.slice(0, 10) : null;
283
+ const filteredCommits = sinceDate || untilDate ? commits.filter((c) => {
284
+ const d = c.date.slice(0, 10);
285
+ if (sinceDate && d < sinceDate) return false;
286
+ if (untilDate && d > untilDate) return false;
287
+ return true;
288
+ }) : commits;
289
+ const totalInsertions = filteredCommits.reduce((sum, c) => sum + c.insertions, 0);
290
+ const totalDeletions = filteredCommits.reduce((sum, c) => sum + c.deletions, 0);
281
291
  return {
282
292
  branch: branchName,
283
293
  since: opts.since ?? null,
284
294
  until: opts.until ?? null,
285
- commits,
286
- weeks: buildWeeklyBreakdown(commits),
287
- byAuthor: buildByAuthor(commits),
295
+ commits: filteredCommits,
296
+ weeks: buildWeeklyBreakdown(filteredCommits),
297
+ byAuthor: buildByAuthor(filteredCommits),
288
298
  summary: {
289
- commitCount: commits.length,
299
+ commitCount: filteredCommits.length,
290
300
  totalInsertions,
291
301
  totalDeletions,
292
302
  totalNetLines: totalInsertions - totalDeletions,
293
- totalFilesChanged: commits.reduce((sum, c) => sum + c.filesChanged, 0)
303
+ totalFilesChanged: filteredCommits.reduce((sum, c) => sum + c.filesChanged, 0)
294
304
  }
295
305
  };
296
306
  }
@@ -411,34 +421,34 @@ function parseAdoRemote(remoteUrl, adoBaseUrl) {
411
421
  } catch {
412
422
  baseHostname = adoBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "").split(/[:/]/)[0].toLowerCase();
413
423
  }
414
- let path6 = remoteUrl;
424
+ let path8 = remoteUrl;
415
425
  let remoteHostname = "";
416
- const sshProtoMatch = path6.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
426
+ const sshProtoMatch = path8.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
417
427
  if (sshProtoMatch) {
418
428
  remoteHostname = sshProtoMatch[1].toLowerCase();
419
- path6 = sshProtoMatch[2];
429
+ path8 = sshProtoMatch[2];
420
430
  } else {
421
- const gitAtMatch = path6.match(/^git@([^:]+)(?::\d+)?:(.*)/);
431
+ const gitAtMatch = path8.match(/^git@([^:]+)(?::\d+)?:(.*)/);
422
432
  if (gitAtMatch) {
423
433
  remoteHostname = gitAtMatch[1].toLowerCase();
424
- path6 = "/" + gitAtMatch[2];
434
+ path8 = "/" + gitAtMatch[2];
425
435
  } else {
426
- const httpsMatch = path6.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
436
+ const httpsMatch = path8.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
427
437
  if (httpsMatch) {
428
438
  remoteHostname = httpsMatch[1].toLowerCase();
429
- path6 = httpsMatch[2];
439
+ path8 = httpsMatch[2];
430
440
  }
431
441
  }
432
442
  }
433
443
  if (!remoteHostname || remoteHostname !== baseHostname) return null;
434
- path6 = path6.replace(/\.git$/, "");
435
- const gitIdx = path6.indexOf("/_git/");
436
- const sshIdx = path6.indexOf("/_ssh/");
444
+ path8 = path8.replace(/\.git$/, "");
445
+ const gitIdx = path8.indexOf("/_git/");
446
+ const sshIdx = path8.indexOf("/_ssh/");
437
447
  const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
438
448
  if (markerIdx === -1) return null;
439
- const repo = decodeURIComponent(path6.slice(markerIdx + 6));
449
+ const repo = decodeURIComponent(path8.slice(markerIdx + 6));
440
450
  if (!repo) return null;
441
- const before = path6.slice(1, markerIdx);
451
+ const before = path8.slice(1, markerIdx);
442
452
  const parts = before.split("/").filter(Boolean);
443
453
  if (parts.length < 2) return null;
444
454
  const project = decodeURIComponent(parts[parts.length - 1]);
@@ -742,7 +752,7 @@ function registerGitCommands(program2) {
742
752
  until: opts.until
743
753
  });
744
754
  if (opts.csv) {
745
- process.stdout.write(formatBranchReportCsv(report));
755
+ writeRawOutput(formatBranchReportCsv(report));
746
756
  } else {
747
757
  success(report, "git", "report", start);
748
758
  }
@@ -783,7 +793,42 @@ function registerGitCommands(program2) {
783
793
  }
784
794
 
785
795
  // src/services/jira/commands.ts
796
+ import { readFileSync as readFileSync2, existsSync, writeFileSync, mkdirSync } from "fs";
797
+ import path from "path";
798
+
799
+ // src/services/jira/client.ts
786
800
  import { readFileSync } from "fs";
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
+ }
787
832
 
788
833
  // src/services/jira/client.ts
789
834
  var API2 = "/rest/api/2";
@@ -925,6 +970,36 @@ var JiraClient = class {
925
970
  }
926
971
  return result;
927
972
  }
973
+ async addLabels(key, labels) {
974
+ await this.http.jira(`${API2}/issue/${key}`, {
975
+ method: "PUT",
976
+ body: { update: { labels: labels.map((l) => ({ add: l })) } }
977
+ });
978
+ }
979
+ async removeLabels(key, labels) {
980
+ await this.http.jira(`${API2}/issue/${key}`, {
981
+ method: "PUT",
982
+ body: { update: { labels: labels.map((l) => ({ remove: l })) } }
983
+ });
984
+ }
985
+ async uploadAttachment(key, filePath) {
986
+ const fileContent = readFileSync(filePath);
987
+ const fileName = basename(filePath);
988
+ const mimeType = guessMimeType(filePath);
989
+ const formData = new FormData();
990
+ formData.append("file", new Blob([fileContent], { type: mimeType }), fileName);
991
+ return this.http.jiraUpload(`${API2}/issue/${key}/attachments`, formData);
992
+ }
993
+ async listAttachments(key) {
994
+ const result = await this.http.jira(
995
+ `${API2}/issue/${key}`,
996
+ { params: { fields: "attachment" } }
997
+ );
998
+ return result.fields.attachment ?? [];
999
+ }
1000
+ async downloadAttachment(contentUrl) {
1001
+ return this.http.jiraBuffer(contentUrl);
1002
+ }
928
1003
  async assignIssue(key, accountId) {
929
1004
  await this.http.jira(`${API2}/issue/${key}/assignee`, {
930
1005
  method: "PUT",
@@ -1191,6 +1266,28 @@ function registerJiraCommands(program2) {
1191
1266
  fail(err, "jira", "search", start);
1192
1267
  }
1193
1268
  });
1269
+ jira.command("add-label").description("Add one or more labels to a Jira issue (non-destructive)").requiredOption("--key <issue-key>", "Issue key").requiredOption("--labels <labels>", "Comma-separated labels to add").action(async (opts) => {
1270
+ const start = Date.now();
1271
+ try {
1272
+ const client = getClient(program2);
1273
+ const labels = opts.labels.split(",").map((s) => s.trim()).filter(Boolean);
1274
+ await client.addLabels(opts.key, labels);
1275
+ success({ updated: opts.key, labelsAdded: labels }, "jira", "add-label", start);
1276
+ } catch (err) {
1277
+ fail(err, "jira", "add-label", start);
1278
+ }
1279
+ });
1280
+ jira.command("remove-label").description("Remove one or more labels from a Jira issue (non-destructive)").requiredOption("--key <issue-key>", "Issue key").requiredOption("--labels <labels>", "Comma-separated labels to remove").action(async (opts) => {
1281
+ const start = Date.now();
1282
+ try {
1283
+ const client = getClient(program2);
1284
+ const labels = opts.labels.split(",").map((s) => s.trim()).filter(Boolean);
1285
+ await client.removeLabels(opts.key, labels);
1286
+ success({ updated: opts.key, labelsRemoved: labels }, "jira", "remove-label", start);
1287
+ } catch (err) {
1288
+ fail(err, "jira", "remove-label", start);
1289
+ }
1290
+ });
1194
1291
  jira.command("assign").description("Assign a Jira issue to a user").requiredOption("--key <issue-key>", "Issue key").requiredOption("--assignee <accountId>", "Assignee account ID").action(async (opts) => {
1195
1292
  const start = Date.now();
1196
1293
  try {
@@ -1211,6 +1308,44 @@ function registerJiraCommands(program2) {
1211
1308
  fail(err, "jira", "link-issue", start);
1212
1309
  }
1213
1310
  });
1311
+ jira.command("add-attachment").description("Upload a file as an attachment to a Jira issue").requiredOption("--key <issue-key>", "Issue key (e.g. PROJ-123)").requiredOption("--file <path>", "Path to the file to upload").action(async (opts) => {
1312
+ const start = Date.now();
1313
+ try {
1314
+ if (!existsSync(opts.file)) throw new PncliError(`File not found: ${opts.file}`, 1);
1315
+ const client = getClient(program2);
1316
+ const data = await client.uploadAttachment(opts.key, opts.file);
1317
+ success(data, "jira", "add-attachment", start);
1318
+ } catch (err) {
1319
+ fail(err, "jira", "add-attachment", start);
1320
+ }
1321
+ });
1322
+ jira.command("list-attachments").description("List attachments on a Jira issue").requiredOption("--key <issue-key>", "Issue key (e.g. PROJ-123)").action(async (opts) => {
1323
+ const start = Date.now();
1324
+ try {
1325
+ const client = getClient(program2);
1326
+ const data = await client.listAttachments(opts.key);
1327
+ success(data, "jira", "list-attachments", start);
1328
+ } catch (err) {
1329
+ fail(err, "jira", "list-attachments", start);
1330
+ }
1331
+ });
1332
+ jira.command("download-attachment").description("Download a Jira issue attachment to .pncli/ (or --dir)").requiredOption("--key <issue-key>", "Issue key (e.g. PROJ-123)").requiredOption("--attachment-id <id>", "Attachment ID from list-attachments output").option("--dir <path>", "Output directory (default: .pncli relative to cwd)").action(async (opts) => {
1333
+ const start = Date.now();
1334
+ try {
1335
+ const client = getClient(program2);
1336
+ const attachments = await client.listAttachments(opts.key);
1337
+ const attachment = attachments.find((a) => a.id === opts.attachmentId);
1338
+ if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
1339
+ const outDir = opts.dir ?? path.join(process.cwd(), ".pncli");
1340
+ mkdirSync(outDir, { recursive: true });
1341
+ const outPath = path.join(outDir, path.basename(attachment.filename));
1342
+ const buffer = await client.downloadAttachment(attachment.content);
1343
+ writeFileSync(outPath, buffer);
1344
+ success({ saved: outPath, filename: attachment.filename, size: buffer.length }, "jira", "download-attachment", start);
1345
+ } catch (err) {
1346
+ fail(err, "jira", "download-attachment", start);
1347
+ }
1348
+ });
1214
1349
  jira.command("fields").description("List custom fields (configured or discovered from Jira API) (consider --output-file for large results)").option("--discover", "Fetch field metadata from Jira API").option("--custom-only", "Show only custom fields (requires --discover)").option("--project <key>", "Project key \u2014 fetches allowedValues for select fields via createmeta (requires --discover)").option("--issue-type <type>", "Filter allowedValues to a specific issue type (requires --project)").action(async (opts) => {
1215
1350
  const start = Date.now();
1216
1351
  try {
@@ -1244,7 +1379,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1244
1379
  const filePath = value.slice(1);
1245
1380
  let raw;
1246
1381
  try {
1247
- raw = readFileSync(filePath, "utf8").trim();
1382
+ raw = readFileSync2(filePath, "utf8").trim();
1248
1383
  } catch (e) {
1249
1384
  throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
1250
1385
  }
@@ -1262,7 +1397,7 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1262
1397
  function parseFieldsFile(filePath, fieldMap) {
1263
1398
  let raw;
1264
1399
  try {
1265
- raw = readFileSync(filePath, "utf8");
1400
+ raw = readFileSync2(filePath, "utf8");
1266
1401
  } catch (e) {
1267
1402
  throw new PncliError(`Cannot read fields file "${filePath}": ${e.message}`, 1);
1268
1403
  }
@@ -1562,9 +1697,9 @@ var ConfluenceClient = class {
1562
1697
  const initialStart = opts.start ?? 0;
1563
1698
  return this.http.confluencePaginate(async (start, limit) => {
1564
1699
  return this.http.confluence(`${API3}/content`, {
1565
- params: { spaceKey, type: "page", expand: "version", start: initialStart + start, limit: opts.limit ?? limit }
1700
+ params: { spaceKey, type: "page", expand: "version", start: initialStart + start, limit }
1566
1701
  });
1567
- });
1702
+ }, opts.limit);
1568
1703
  }
1569
1704
  async getPageChildren(id) {
1570
1705
  return this.http.confluencePaginate(async (start, limit) => {
@@ -1667,11 +1802,11 @@ var ConfluenceClient = class {
1667
1802
  return this.http.confluence(`${API3}/space`, {
1668
1803
  params: {
1669
1804
  start: initialStart + start,
1670
- limit: opts.limit ?? limit,
1805
+ limit,
1671
1806
  ...opts.type ? { type: opts.type } : {}
1672
1807
  }
1673
1808
  });
1674
- });
1809
+ }, opts.limit);
1675
1810
  }
1676
1811
  async getSpace(key) {
1677
1812
  return this.http.confluence(`${API3}/space/${key}`, {
@@ -1718,7 +1853,7 @@ function registerConfluenceCommands(program2) {
1718
1853
  fail(err, "confluence", "get-page-by-title", start);
1719
1854
  }
1720
1855
  });
1721
- confluence.command("list-pages").description("List pages in a Confluence space (consider --output-file for large results)").requiredOption("--space <key>", "Space key").option("--limit <n>", "Max results per page (default: all)").option("--start <n>", "Offset for first result").action(async (opts) => {
1856
+ confluence.command("list-pages").description("List pages in a Confluence space (consider --output-file for large results)").requiredOption("--space <key>", "Space key").option("--limit <n>", "Max total pages to return (default: all)").option("--start <n>", "Offset for first result").action(async (opts) => {
1722
1857
  const start = Date.now();
1723
1858
  try {
1724
1859
  const client = getClient3(program2);
@@ -1850,7 +1985,7 @@ function registerConfluenceCommands(program2) {
1850
1985
  fail(err, "confluence", "remove-label", start);
1851
1986
  }
1852
1987
  });
1853
- confluence.command("list-spaces").description("List Confluence spaces").option("--type <type>", "Space type: global or personal").option("--limit <n>", "Max results (default: all)").action(async (opts) => {
1988
+ confluence.command("list-spaces").description("List Confluence spaces").option("--type <type>", "Space type: global or personal").option("--limit <n>", "Max total spaces to return (default: all)").action(async (opts) => {
1854
1989
  const start = Date.now();
1855
1990
  try {
1856
1991
  const client = getClient3(program2);
@@ -2426,23 +2561,23 @@ import { Option } from "commander";
2426
2561
 
2427
2562
  // src/services/deps/parsers/index.ts
2428
2563
  import fs from "fs";
2429
- import path4 from "path";
2564
+ import path5 from "path";
2430
2565
  import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
2431
2566
 
2432
2567
  // src/services/deps/parsers/npm.ts
2433
- import path from "path";
2568
+ import path2 from "path";
2434
2569
  function findNpmManifests(files) {
2435
2570
  const lockFiles = new Set(
2436
2571
  files.filter((f) => {
2437
- const base = path.basename(f);
2572
+ const base = path2.basename(f);
2438
2573
  return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
2439
2574
  })
2440
2575
  );
2441
2576
  const manifests = [];
2442
2577
  for (const file of files) {
2443
- if (path.basename(file) !== "package.json") continue;
2578
+ if (path2.basename(file) !== "package.json") continue;
2444
2579
  if (file.split("/").includes("node_modules")) continue;
2445
- const dir = path.dirname(file);
2580
+ const dir = path2.dirname(file);
2446
2581
  let lockFile;
2447
2582
  for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
2448
2583
  const candidate = dir === "." ? lf : `${dir}/${lf}`;
@@ -2457,7 +2592,7 @@ function findNpmManifests(files) {
2457
2592
  }
2458
2593
  function parseNpmPackages(content, manifest, opts, lockContent) {
2459
2594
  if (lockContent && manifest.lockFile) {
2460
- const base = path.basename(manifest.lockFile);
2595
+ const base = path2.basename(manifest.lockFile);
2461
2596
  if (base === "package-lock.json") {
2462
2597
  const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
2463
2598
  if (pkgs.length > 0) return pkgs;
@@ -2605,26 +2740,26 @@ function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
2605
2740
  }
2606
2741
 
2607
2742
  // src/services/deps/parsers/nuget.ts
2608
- import path2 from "path";
2743
+ import path3 from "path";
2609
2744
  var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
2610
2745
  function findNugetManifests(files) {
2611
2746
  const lockFiles = /* @__PURE__ */ new Map();
2612
2747
  for (const f of files) {
2613
- if (path2.basename(f) === "packages.lock.json") {
2614
- lockFiles.set(path2.dirname(f), f);
2748
+ if (path3.basename(f) === "packages.lock.json") {
2749
+ lockFiles.set(path3.dirname(f), f);
2615
2750
  }
2616
2751
  }
2617
2752
  const manifests = [];
2618
2753
  for (const file of files) {
2619
- const base = path2.basename(file);
2620
- const ext = path2.extname(file);
2754
+ const base = path3.basename(file);
2755
+ const ext = path3.extname(file);
2621
2756
  if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
2622
- const dir = path2.dirname(file);
2757
+ const dir = path3.dirname(file);
2623
2758
  const lockFile = lockFiles.get(dir);
2624
2759
  manifests.push({ file, ecosystem: "nuget", lockFile });
2625
2760
  }
2626
2761
  if (base === "Directory.Packages.props") {
2627
- const dir = path2.dirname(file);
2762
+ const dir = path3.dirname(file);
2628
2763
  manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
2629
2764
  }
2630
2765
  }
@@ -2643,7 +2778,7 @@ function findNugetManifests(files) {
2643
2778
  return deduped;
2644
2779
  }
2645
2780
  function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
2646
- const base = path2.basename(manifest.file);
2781
+ const base = path3.basename(manifest.file);
2647
2782
  if (lockContent) {
2648
2783
  return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
2649
2784
  }
@@ -2764,14 +2899,14 @@ function parsePackagesLock(content, filePath, opts) {
2764
2899
  }
2765
2900
 
2766
2901
  // src/services/deps/parsers/maven.ts
2767
- import path3 from "path";
2902
+ import path4 from "path";
2768
2903
  function findMavenManifests(files) {
2769
- const lockFiles = new Set(files.filter((f) => path3.basename(f) === "gradle.lockfile"));
2904
+ const lockFiles = new Set(files.filter((f) => path4.basename(f) === "gradle.lockfile"));
2770
2905
  const manifests = [];
2771
2906
  for (const file of files) {
2772
- const base = path3.basename(file);
2907
+ const base = path4.basename(file);
2773
2908
  if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
2774
- const dir = path3.dirname(file);
2909
+ const dir = path4.dirname(file);
2775
2910
  const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
2776
2911
  const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
2777
2912
  manifests.push({ file, ecosystem: "maven", lockFile });
@@ -2780,7 +2915,7 @@ function findMavenManifests(files) {
2780
2915
  return manifests;
2781
2916
  }
2782
2917
  function parseMavenPackages(content, manifest, opts, lockContent) {
2783
- const base = path3.basename(manifest.file);
2918
+ const base = path4.basename(manifest.file);
2784
2919
  if (lockContent && manifest.lockFile) {
2785
2920
  return parseGradleLock(lockContent, manifest.lockFile, opts);
2786
2921
  }
@@ -2941,7 +3076,7 @@ function getRepoFiles(repoRoot) {
2941
3076
  }
2942
3077
  function readFile(repoRoot, relPath) {
2943
3078
  try {
2944
- return fs.readFileSync(path4.join(repoRoot, relPath), "utf8");
3079
+ return fs.readFileSync(path5.join(repoRoot, relPath), "utf8");
2945
3080
  } catch {
2946
3081
  return null;
2947
3082
  }
@@ -2959,13 +3094,13 @@ function readFileAtRef(repoRoot, ref, relPath) {
2959
3094
  }
2960
3095
  }
2961
3096
  function findPropsContent(manifestFile, readFn) {
2962
- let dir = path4.dirname(manifestFile);
3097
+ let dir = path5.dirname(manifestFile);
2963
3098
  for (let depth = 0; depth < 10; depth++) {
2964
3099
  const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
2965
3100
  const content = readFn(candidate);
2966
3101
  if (content) return content;
2967
3102
  if (dir === ".") break;
2968
- dir = path4.dirname(dir);
3103
+ dir = path5.dirname(dir);
2969
3104
  }
2970
3105
  return null;
2971
3106
  }
@@ -4201,6 +4336,8 @@ import password from "@inquirer/password";
4201
4336
  import confirm from "@inquirer/confirm";
4202
4337
 
4203
4338
  // src/services/ado/client/work.ts
4339
+ import { readFileSync as readFileSync3 } from "fs";
4340
+ import { basename as basename2 } from "path";
4204
4341
  var API6 = "7.1";
4205
4342
  var API_PREVIEW = "7.1-preview.4";
4206
4343
  var AdoWorkClient = class {
@@ -4243,6 +4380,24 @@ var AdoWorkClient = class {
4243
4380
  { method: "POST", body: { text } }
4244
4381
  );
4245
4382
  }
4383
+ async addTags(collection, id, tags) {
4384
+ const item = await this.getWorkItem(collection, id);
4385
+ const existing = item.fields["System.Tags"] ?? "";
4386
+ const current = existing.split(";").map((t) => t.trim()).filter(Boolean);
4387
+ const lowerSet = new Set(current.map((t) => t.toLowerCase()));
4388
+ const toAdd = tags.filter((t) => !lowerSet.has(t.toLowerCase()));
4389
+ const merged = [...current, ...toAdd];
4390
+ const patch = [{ op: "add", path: "/fields/System.Tags", value: merged.join("; ") }];
4391
+ return this.updateWorkItem(collection, id, patch);
4392
+ }
4393
+ async removeTags(collection, id, tags) {
4394
+ const item = await this.getWorkItem(collection, id);
4395
+ const existing = item.fields["System.Tags"] ?? "";
4396
+ const lower = new Set(tags.map((t) => t.toLowerCase()));
4397
+ const remaining = existing.split(";").map((t) => t.trim()).filter((t) => t && !lower.has(t.toLowerCase()));
4398
+ const patch = [{ op: "add", path: "/fields/System.Tags", value: remaining.join("; ") }];
4399
+ return this.updateWorkItem(collection, id, patch);
4400
+ }
4246
4401
  async listWorkItemTypes(collection, project) {
4247
4402
  const result = await this.http.ado(
4248
4403
  `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes?api-version=${API6}`
@@ -4268,6 +4423,44 @@ var AdoWorkClient = class {
4268
4423
  );
4269
4424
  return result.value ?? [];
4270
4425
  }
4426
+ async listAttachments(collection, workItemId) {
4427
+ const item = await this.getWorkItem(collection, workItemId);
4428
+ return (item.relations ?? []).filter((r) => r.rel === "AttachedFile").map((r) => {
4429
+ const attrs = r.attributes ?? {};
4430
+ return {
4431
+ id: new URL(r.url).pathname.split("/").pop() ?? "",
4432
+ name: String(attrs["name"] ?? ""),
4433
+ url: r.url,
4434
+ comment: attrs["comment"] !== void 0 ? String(attrs["comment"]) : void 0,
4435
+ resourceSize: typeof attrs["resourceSize"] === "number" ? attrs["resourceSize"] : void 0,
4436
+ resourceCreatedDate: attrs["resourceCreatedDate"] !== void 0 ? String(attrs["resourceCreatedDate"]) : void 0,
4437
+ resourceModifiedDate: attrs["resourceModifiedDate"] !== void 0 ? String(attrs["resourceModifiedDate"]) : void 0
4438
+ };
4439
+ });
4440
+ }
4441
+ async downloadAttachment(absoluteUrl) {
4442
+ return this.http.adoBuffer(absoluteUrl);
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
+ }
4271
4464
  };
4272
4465
 
4273
4466
  // src/services/ado/discovery.ts
@@ -4441,8 +4634,8 @@ function registerConfigCommands(program2) {
4441
4634
  if (cfg.ado.baseUrl) {
4442
4635
  try {
4443
4636
  const collection = cfg.defaults.ado?.collection;
4444
- const path6 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
4445
- await http.ado(path6);
4637
+ const path8 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
4638
+ await http.ado(path8);
4446
4639
  results.ado = { ok: true, message: "connected" };
4447
4640
  } catch (err) {
4448
4641
  results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
@@ -4521,6 +4714,16 @@ function registerConfigCommands(program2) {
4521
4714
  } else {
4522
4715
  results.sonatypeiq = { ok: null, message: "not configured" };
4523
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
+ }
4524
4727
  success(results, "config", "test", start);
4525
4728
  } catch (err) {
4526
4729
  fail(err, "config", "test", start);
@@ -4614,8 +4817,8 @@ function registerConfigCommands(program2) {
4614
4817
  } else {
4615
4818
  try {
4616
4819
  const collection = cfg.defaults.ado?.collection;
4617
- const path6 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
4618
- await http.ado(path6, { timeoutMs: 1e4 });
4820
+ const path8 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
4821
+ await http.ado(path8, { timeoutMs: 1e4 });
4619
4822
  results.ado = { status: "valid", message: "ok" };
4620
4823
  } catch (err) {
4621
4824
  results.ado = categorize2(err);
@@ -4704,11 +4907,23 @@ function registerConfigCommands(program2) {
4704
4907
  results.sonatypeiq = categorize2(err);
4705
4908
  }
4706
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
+ }
4707
4922
  const statuses = Object.values(results).map((r) => r.status);
4708
4923
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
4709
4924
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
4710
4925
  if (cmdOpts.output === "table") {
4711
- 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"];
4712
4927
  const labelWidth = 14;
4713
4928
  const statusWidth = 9;
4714
4929
  for (const svc of services) {
@@ -4722,7 +4937,7 @@ function registerConfigCommands(program2) {
4722
4937
  process.stdout.write("\n");
4723
4938
  } else {
4724
4939
  if (opts.pretty) {
4725
- 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"];
4726
4941
  const labelWidth = 14;
4727
4942
  const statusWidth = 9;
4728
4943
  for (const svc of services) {
@@ -5210,6 +5425,41 @@ async function initGlobalConfig(start) {
5210
5425
  }
5211
5426
  }
5212
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
+ }
5213
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");
5214
5464
  const jiraProject = await input({
5215
5465
  message: "Default Jira project key (optional):",
@@ -5327,6 +5577,12 @@ async function initGlobalConfig(start) {
5327
5577
  passcode: sonatypeIqPasscode || void 0
5328
5578
  }
5329
5579
  } : {},
5580
+ ...useOpenShift && openShiftBaseUrl ? {
5581
+ openshift: {
5582
+ baseUrl: normalizeBaseUrl(openShiftBaseUrl),
5583
+ token: openShiftToken || void 0
5584
+ }
5585
+ } : {},
5330
5586
  defaults: {
5331
5587
  jira: {
5332
5588
  project: jiraProject || void 0
@@ -5777,6 +6033,8 @@ function registerAdoProjectCommands(ado) {
5777
6033
  }
5778
6034
 
5779
6035
  // src/services/ado/commands/work.ts
6036
+ import fs3 from "fs";
6037
+ import path6 from "path";
5780
6038
  function registerAdoWorkCommands(ado) {
5781
6039
  const work = ado.command("work").description("Azure DevOps work item operations");
5782
6040
  work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
@@ -5917,6 +6175,65 @@ function registerAdoWorkCommands(ado) {
5917
6175
  fail(err, "ado", "work-add-comment", start);
5918
6176
  }
5919
6177
  });
6178
+ work.command("add-tag").description("Add one or more tags to a work item (non-destructive)").requiredOption("--id <n>", "Work item ID").requiredOption("--tags <tags>", "Semicolon- or comma-separated tags to add").action(async (opts) => {
6179
+ const start = Date.now();
6180
+ try {
6181
+ const { collection, workClient } = getAdoContext(ado);
6182
+ const tags = opts.tags.split(/[;,]/).map((t) => t.trim()).filter(Boolean);
6183
+ const data = await workClient.addTags(collection, parseInt(opts.id, 10), tags);
6184
+ success(data, "ado", "work-add-tag", start);
6185
+ } catch (err) {
6186
+ fail(err, "ado", "work-add-tag", start);
6187
+ }
6188
+ });
6189
+ work.command("remove-tag").description("Remove one or more tags from a work item (non-destructive)").requiredOption("--id <n>", "Work item ID").requiredOption("--tags <tags>", "Semicolon- or comma-separated tags to remove").action(async (opts) => {
6190
+ const start = Date.now();
6191
+ try {
6192
+ const { collection, workClient } = getAdoContext(ado);
6193
+ const tags = opts.tags.split(/[;,]/).map((t) => t.trim()).filter(Boolean);
6194
+ const data = await workClient.removeTags(collection, parseInt(opts.id, 10), tags);
6195
+ success(data, "ado", "work-remove-tag", start);
6196
+ } catch (err) {
6197
+ fail(err, "ado", "work-remove-tag", start);
6198
+ }
6199
+ });
6200
+ work.command("list-attachments").description("List attachments on a work item").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
6201
+ const start = Date.now();
6202
+ try {
6203
+ const { collection, workClient } = getAdoContext(ado);
6204
+ const data = await workClient.listAttachments(collection, parseInt(opts.id, 10));
6205
+ success(data, "ado", "work-list-attachments", start);
6206
+ } catch (err) {
6207
+ fail(err, "ado", "work-list-attachments", start);
6208
+ }
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
+ });
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) => {
6221
+ const start = Date.now();
6222
+ try {
6223
+ const { collection, workClient } = getAdoContext(ado);
6224
+ const attachments = await workClient.listAttachments(collection, parseInt(opts.id, 10));
6225
+ const attachment = attachments.find((a) => a.id === opts.attachmentId);
6226
+ if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
6227
+ const outDir = opts.dir ?? path6.join(process.cwd(), ".pncli");
6228
+ fs3.mkdirSync(outDir, { recursive: true });
6229
+ const outPath = path6.join(outDir, path6.basename(attachment.name));
6230
+ const buffer = await workClient.downloadAttachment(attachment.url);
6231
+ fs3.writeFileSync(outPath, buffer);
6232
+ success({ saved: outPath, name: attachment.name, size: buffer.length }, "ado", "work-download-attachment", start);
6233
+ } catch (err) {
6234
+ fail(err, "ado", "work-download-attachment", start);
6235
+ }
6236
+ });
5920
6237
  work.command("types").description("List work item types in the project").option("--discover", "Fetch from server (always true for this command)").option("--save", "Save discovered types to ~/.pncli/config.json").action(async (opts) => {
5921
6238
  const start = Date.now();
5922
6239
  try {
@@ -5925,10 +6242,10 @@ function registerAdoWorkCommands(ado) {
5925
6242
  const types = await discoverTypes(workClient, collection, project);
5926
6243
  if (opts.save) {
5927
6244
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
5928
- const fs4 = await import("fs");
5929
- const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
6245
+ const fs5 = await import("fs");
6246
+ const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
5930
6247
  existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
5931
- fs4.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
6248
+ fs5.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
5932
6249
  warn(`Saved ${types.length} types to ${globalConfigPath}`);
5933
6250
  }
5934
6251
  success(types, "ado", "work-types", start);
@@ -5957,11 +6274,11 @@ function registerAdoWorkCommands(ado) {
5957
6274
  ) : allFields;
5958
6275
  if (opts.save) {
5959
6276
  const globalConfigPath = getGlobalConfigPath(globalOpts.config);
5960
- const fs4 = await import("fs");
5961
- const existing = JSON.parse(fs4.readFileSync(globalConfigPath, "utf8") || "{}");
6277
+ const fs5 = await import("fs");
6278
+ const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
5962
6279
  const aliases = buildDefaultAliases(allFields);
5963
6280
  existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
5964
- fs4.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
6281
+ fs5.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
5965
6282
  warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
5966
6283
  }
5967
6284
  success(fields, "ado", "work-fields", start);
@@ -6238,11 +6555,11 @@ function registerAdoRepoCommands(ado) {
6238
6555
  const start = Date.now();
6239
6556
  try {
6240
6557
  const globalOpts = ado.optsWithGlobals();
6241
- const { loadConfig: lc } = await import("./config-OFLPO5DN.js");
6558
+ const { loadConfig: lc } = await import("./config-TL5X47RY.js");
6242
6559
  const config = lc({ configPath: globalOpts.config });
6243
6560
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
6244
6561
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
6245
- const { createHttpClient: createHttpClient2 } = await import("./http-5GDQYH62.js");
6562
+ const { createHttpClient: createHttpClient2 } = await import("./http-UN6N4WIB.js");
6246
6563
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
6247
6564
  const coreClient = new AdoCoreClient2(http);
6248
6565
  const me = await coreClient.getConnectionData(collection);
@@ -6643,18 +6960,18 @@ var ArtifactoryClient = class {
6643
6960
  }
6644
6961
  });
6645
6962
  }
6646
- async getStorageInfo(repo, path6) {
6647
- const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
6963
+ async getStorageInfo(repo, path8) {
6964
+ const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
6648
6965
  return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`);
6649
6966
  }
6650
- async getProperties(repo, path6) {
6651
- const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
6967
+ async getProperties(repo, path8) {
6968
+ const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
6652
6969
  return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
6653
6970
  params: { properties: "" }
6654
6971
  });
6655
6972
  }
6656
- async setProperties(repo, path6, properties, opts = {}) {
6657
- const cleanPath = path6.startsWith("/") ? path6.slice(1) : path6;
6973
+ async setProperties(repo, path8, properties, opts = {}) {
6974
+ const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
6658
6975
  const propString = Object.entries(properties).map(([k, v]) => `${k}=${v}`).join(";");
6659
6976
  await this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
6660
6977
  method: "PUT",
@@ -6736,27 +7053,27 @@ function registerArtifactoryCommands(program2) {
6736
7053
  fail(err, "artifactory", "repos", start);
6737
7054
  }
6738
7055
  });
6739
- art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path6) => {
7056
+ art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path8) => {
6740
7057
  const start = Date.now();
6741
7058
  try {
6742
7059
  const client = getClient7(program2);
6743
- const data = await client.getStorageInfo(repo, path6);
7060
+ const data = await client.getStorageInfo(repo, path8);
6744
7061
  success(data, "artifactory", "artifact-info", start);
6745
7062
  } catch (err) {
6746
7063
  fail(err, "artifactory", "artifact-info", start);
6747
7064
  }
6748
7065
  });
6749
- art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path6) => {
7066
+ art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path8) => {
6750
7067
  const start = Date.now();
6751
7068
  try {
6752
7069
  const client = getClient7(program2);
6753
- const data = await client.getProperties(repo, path6);
7070
+ const data = await client.getProperties(repo, path8);
6754
7071
  success(data, "artifactory", "properties-get", start);
6755
7072
  } catch (err) {
6756
7073
  fail(err, "artifactory", "properties-get", start);
6757
7074
  }
6758
7075
  });
6759
- art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo, path6, keyvals, opts) => {
7076
+ art.command("properties-set <repo> <path> [keyvals...]").description("Set custom properties on an artifact (format: key=value key=value...)").option("--recursive", "Apply recursively to folder contents", false).action(async (repo, path8, keyvals, opts) => {
6760
7077
  const start = Date.now();
6761
7078
  try {
6762
7079
  if (!keyvals.length) throw new PncliError("Provide at least one key=value pair");
@@ -6767,8 +7084,8 @@ function registerArtifactoryCommands(program2) {
6767
7084
  properties[kv.slice(0, eq)] = kv.slice(eq + 1);
6768
7085
  }
6769
7086
  const client = getClient7(program2);
6770
- await client.setProperties(repo, path6, properties, { recursive: opts.recursive });
6771
- success({ repo, path: path6, properties }, "artifactory", "properties-set", start);
7087
+ await client.setProperties(repo, path8, properties, { recursive: opts.recursive });
7088
+ success({ repo, path: path8, properties }, "artifactory", "properties-set", start);
6772
7089
  } catch (err) {
6773
7090
  fail(err, "artifactory", "properties-set", start);
6774
7091
  }
@@ -7372,8 +7689,8 @@ function registerSonatypeIqCommands(program2) {
7372
7689
  const startTime = Date.now();
7373
7690
  try {
7374
7691
  const http = getHttpClient(program2);
7375
- const path6 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
7376
- const data = await http.sonatypeiq(path6);
7692
+ const path8 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
7693
+ const data = await http.sonatypeiq(path8);
7377
7694
  success(data, "sonatypeiq", "policies list", startTime);
7378
7695
  } catch (err) {
7379
7696
  fail(err, "sonatypeiq", "policies list", startTime);
@@ -7382,8 +7699,8 @@ function registerSonatypeIqCommands(program2) {
7382
7699
  }
7383
7700
 
7384
7701
  // src/services/skills/commands.ts
7385
- import fs3 from "fs";
7386
- import path5 from "path";
7702
+ import fs4 from "fs";
7703
+ import path7 from "path";
7387
7704
  import os from "os";
7388
7705
  import { fileURLToPath } from "url";
7389
7706
  import { execFileSync as execFileSync3 } from "child_process";
@@ -7409,16 +7726,16 @@ function repoNameFromUrl(url) {
7409
7726
  }
7410
7727
  }
7411
7728
  function defaultMarketplacePath(url) {
7412
- return path5.join(os.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
7729
+ return path7.join(os.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
7413
7730
  }
7414
7731
  var AGENT_PATHS = {
7415
- "github-copilot": { project: ".agents/skills", user: path5.join(os.homedir(), ".copilot/skills") },
7416
- "claude-code": { project: ".claude/skills", user: path5.join(os.homedir(), ".claude/skills") }
7732
+ "github-copilot": { project: ".agents/skills", user: path7.join(os.homedir(), ".copilot/skills") },
7733
+ "claude-code": { project: ".claude/skills", user: path7.join(os.homedir(), ".claude/skills") }
7417
7734
  };
7418
7735
  function getBundledSkillsDir() {
7419
7736
  try {
7420
7737
  const thisFile = fileURLToPath(import.meta.url);
7421
- return path5.resolve(path5.dirname(thisFile), "..", "skills");
7738
+ return path7.resolve(path7.dirname(thisFile), "..", "skills");
7422
7739
  } catch {
7423
7740
  return "";
7424
7741
  }
@@ -7430,22 +7747,22 @@ function registerSkillsCommands(program2) {
7430
7747
  try {
7431
7748
  let targetDir;
7432
7749
  if (opts.target) {
7433
- targetDir = path5.resolve(opts.target);
7750
+ targetDir = path7.resolve(opts.target);
7434
7751
  } else {
7435
7752
  const agentConfig = AGENT_PATHS[opts.agent];
7436
7753
  if (!agentConfig) {
7437
7754
  throw new Error(`Unknown agent: "${opts.agent}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
7438
7755
  }
7439
7756
  const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
7440
- targetDir = path5.resolve(scopePath);
7757
+ targetDir = path7.resolve(scopePath);
7441
7758
  }
7442
- const resolvedTarget = path5.resolve(targetDir);
7759
+ const resolvedTarget = path7.resolve(targetDir);
7443
7760
  const bundledDir = getBundledSkillsDir();
7444
7761
  let skillDirs = [];
7445
7762
  if (bundledDir !== "") {
7446
7763
  try {
7447
- skillDirs = fs3.readdirSync(bundledDir).filter(
7448
- (name) => fs3.existsSync(path5.join(bundledDir, name, "SKILL.md"))
7764
+ skillDirs = fs4.readdirSync(bundledDir).filter(
7765
+ (name) => fs4.existsSync(path7.join(bundledDir, name, "SKILL.md"))
7449
7766
  );
7450
7767
  } catch {
7451
7768
  }
@@ -7456,25 +7773,25 @@ function registerSkillsCommands(program2) {
7456
7773
  const installed = [];
7457
7774
  const failed = [];
7458
7775
  for (const skillName of skillDirs) {
7459
- const existingDir = path5.resolve(targetDir, skillName);
7460
- if (!existingDir.startsWith(resolvedTarget + path5.sep)) continue;
7461
- if (fs3.existsSync(existingDir)) {
7462
- fs3.rmSync(existingDir, { recursive: true, force: true });
7776
+ const existingDir = path7.resolve(targetDir, skillName);
7777
+ if (!existingDir.startsWith(resolvedTarget + path7.sep)) continue;
7778
+ if (fs4.existsSync(existingDir)) {
7779
+ fs4.rmSync(existingDir, { recursive: true, force: true });
7463
7780
  }
7464
7781
  }
7465
7782
  warn(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
7466
7783
  for (const skillName of skillDirs) {
7467
- const skillDir = path5.resolve(targetDir, skillName);
7468
- if (!skillDir.startsWith(resolvedTarget + path5.sep)) {
7784
+ const skillDir = path7.resolve(targetDir, skillName);
7785
+ if (!skillDir.startsWith(resolvedTarget + path7.sep)) {
7469
7786
  failed.push(skillName);
7470
7787
  continue;
7471
7788
  }
7472
7789
  try {
7473
- const mdFiles = fs3.readdirSync(path5.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
7474
- fs3.mkdirSync(skillDir, { recursive: true });
7790
+ const mdFiles = fs4.readdirSync(path7.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
7791
+ fs4.mkdirSync(skillDir, { recursive: true });
7475
7792
  for (const mdFile of mdFiles) {
7476
- const content = fs3.readFileSync(path5.join(bundledDir, skillName, mdFile), "utf8");
7477
- fs3.writeFileSync(path5.join(skillDir, mdFile), content, "utf8");
7793
+ const content = fs4.readFileSync(path7.join(bundledDir, skillName, mdFile), "utf8");
7794
+ fs4.writeFileSync(path7.join(skillDir, mdFile), content, "utf8");
7478
7795
  }
7479
7796
  installed.push(skillName);
7480
7797
  } catch {
@@ -7503,25 +7820,25 @@ function registerSkillsCommands(program2) {
7503
7820
  try {
7504
7821
  let targetDir;
7505
7822
  if (opts.target) {
7506
- targetDir = path5.resolve(opts.target);
7823
+ targetDir = path7.resolve(opts.target);
7507
7824
  } else {
7508
7825
  const agentConfig = AGENT_PATHS[opts.agent];
7509
7826
  if (!agentConfig) {
7510
7827
  throw new Error(`Unknown agent: "${opts.agent}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
7511
7828
  }
7512
7829
  const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
7513
- targetDir = path5.resolve(scopePath);
7830
+ targetDir = path7.resolve(scopePath);
7514
7831
  }
7515
- if (!fs3.existsSync(targetDir)) {
7832
+ if (!fs4.existsSync(targetDir)) {
7516
7833
  success({ skills: [], message: `No skills directory found at ${targetDir}. Run: pncli skills install` }, "skills", "list", start);
7517
7834
  return;
7518
7835
  }
7519
- const skillDirs = fs3.readdirSync(targetDir).filter((name) => {
7520
- const skillPath = path5.join(targetDir, name, "SKILL.md");
7521
- return fs3.existsSync(skillPath);
7836
+ const skillDirs = fs4.readdirSync(targetDir).filter((name) => {
7837
+ const skillPath = path7.join(targetDir, name, "SKILL.md");
7838
+ return fs4.existsSync(skillPath);
7522
7839
  });
7523
7840
  const skillsList = skillDirs.map((name) => {
7524
- const content = fs3.readFileSync(path5.join(targetDir, name, "SKILL.md"), "utf8");
7841
+ const content = fs4.readFileSync(path7.join(targetDir, name, "SKILL.md"), "utf8");
7525
7842
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7526
7843
  const data = {};
7527
7844
  const metadata = {};
@@ -7563,9 +7880,9 @@ function registerSkillsCommands(program2) {
7563
7880
  marketplace.command("setup").description("Clone a skills marketplace repo and register it in global config").argument("<url>", "Git clone URL of the marketplace repository").argument("[localPath]", "Local directory to clone into (default: ~/.agents/marketplaces/<repo-name>)").option("--branch <branch>", "Branch to clone (default: remote HEAD)").option("--token <token>", "HTTP access token for authenticated clone and pull (GitHub PAT or Bitbucket token)").action(async (url, localPath, opts) => {
7564
7881
  const start = Date.now();
7565
7882
  try {
7566
- const resolvedPath = path5.resolve(localPath ?? defaultMarketplacePath(url));
7567
- const hasGit = fs3.existsSync(path5.join(resolvedPath, ".git"));
7568
- if (fs3.existsSync(resolvedPath) && !hasGit && fs3.readdirSync(resolvedPath).length > 0) {
7883
+ const resolvedPath = path7.resolve(localPath ?? defaultMarketplacePath(url));
7884
+ const hasGit = fs4.existsSync(path7.join(resolvedPath, ".git"));
7885
+ if (fs4.existsSync(resolvedPath) && !hasGit && fs4.readdirSync(resolvedPath).length > 0) {
7569
7886
  throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
7570
7887
  }
7571
7888
  if (hasGit) {
@@ -7599,7 +7916,7 @@ function registerSkillsCommands(program2) {
7599
7916
  const configPath = getGlobalConfigPath();
7600
7917
  const globalConfig = loadJsonFile(configPath) ?? {};
7601
7918
  const marketplacePath = globalConfig.marketplace?.localPath;
7602
- if (!marketplacePath || !fs3.existsSync(marketplacePath)) {
7919
+ if (!marketplacePath || !fs4.existsSync(marketplacePath)) {
7603
7920
  throw new Error("Marketplace not configured. Run: pncli skills marketplace setup <url> <localPath>");
7604
7921
  }
7605
7922
  warn("Pulling latest marketplace content...");
@@ -7636,13 +7953,13 @@ function registerSkillsCommands(program2) {
7636
7953
  if (pluginChoices.length === 0) {
7637
7954
  throw new Error("No plugins found in marketplace. Check the marketplace repository structure.");
7638
7955
  }
7639
- const targetDir = opts.claude ? path5.join(os.homedir(), ".claude", "skills") : path5.join(os.homedir(), ".agents", "skills");
7956
+ const targetDir = opts.claude ? path7.join(os.homedir(), ".claude", "skills") : path7.join(os.homedir(), ".agents", "skills");
7640
7957
  if (plugin === "all") {
7641
7958
  const results = {};
7642
7959
  let totalInstalled = 0;
7643
7960
  for (const pluginChoice of pluginChoices) {
7644
7961
  const skillsSrc2 = resolveSkillsSrc(marketplacePath, pluginChoice.name);
7645
- if (!fs3.existsSync(skillsSrc2)) {
7962
+ if (!fs4.existsSync(skillsSrc2)) {
7646
7963
  results[pluginChoice.name] = { installed: [], failed: [] };
7647
7964
  warn(`No skills directory found for plugin "${pluginChoice.name}" \u2014 skipping.`);
7648
7965
  continue;
@@ -7650,6 +7967,9 @@ function registerSkillsCommands(program2) {
7650
7967
  const { installed: installed2, failed: failed2 } = copyPluginSkills(skillsSrc2, targetDir);
7651
7968
  results[pluginChoice.name] = { installed: installed2, failed: failed2 };
7652
7969
  totalInstalled += installed2.length;
7970
+ for (const skill of installed2) {
7971
+ warn(` ${skill}: ${path7.join(skillsSrc2, skill)} \u2192 ${path7.join(targetDir, skill)}`);
7972
+ }
7653
7973
  if (failed2.length > 0) {
7654
7974
  warn(`Skipped ${failed2.length} skill(s) with invalid names in "${pluginChoice.name}": ${failed2.join(", ")}`);
7655
7975
  }
@@ -7678,10 +7998,13 @@ function registerSkillsCommands(program2) {
7678
7998
  });
7679
7999
  }
7680
8000
  const skillsSrc = resolveSkillsSrc(marketplacePath, selectedPlugin);
7681
- if (!fs3.existsSync(skillsSrc)) {
8001
+ if (!fs4.existsSync(skillsSrc)) {
7682
8002
  throw new Error(`No skills directory found at ${skillsSrc}`);
7683
8003
  }
7684
8004
  const { installed, failed } = copyPluginSkills(skillsSrc, targetDir);
8005
+ for (const skill of installed) {
8006
+ warn(` ${skill}: ${path7.join(skillsSrc, skill)} \u2192 ${path7.join(targetDir, skill)}`);
8007
+ }
7685
8008
  if (failed.length > 0) {
7686
8009
  warn(`Skipped ${failed.length} skill(s) with invalid names: ${failed.join(", ")}`);
7687
8010
  }
@@ -7699,51 +8022,307 @@ function registerSkillsCommands(program2) {
7699
8022
  });
7700
8023
  }
7701
8024
  function resolvePluginChoices(marketplacePath) {
7702
- const marketplaceJsonPath = path5.join(marketplacePath, ".claude-plugin", "marketplace.json");
7703
- if (fs3.existsSync(marketplaceJsonPath)) {
8025
+ const marketplaceJsonPath = path7.join(marketplacePath, ".claude-plugin", "marketplace.json");
8026
+ if (fs4.existsSync(marketplaceJsonPath)) {
7704
8027
  try {
7705
- const meta = JSON.parse(fs3.readFileSync(marketplaceJsonPath, "utf8"));
8028
+ const meta = JSON.parse(fs4.readFileSync(marketplaceJsonPath, "utf8"));
7706
8029
  if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
7707
8030
  return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
7708
8031
  }
7709
8032
  } catch {
7710
8033
  }
7711
8034
  }
7712
- const pluginsDir = path5.join(marketplacePath, "plugins");
7713
- if (fs3.existsSync(pluginsDir)) {
7714
- return fs3.readdirSync(pluginsDir).filter((name) => fs3.statSync(path5.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
8035
+ const pluginsDir = path7.join(marketplacePath, "plugins");
8036
+ if (fs4.existsSync(pluginsDir)) {
8037
+ return fs4.readdirSync(pluginsDir).filter((name) => fs4.statSync(path7.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
7715
8038
  }
7716
8039
  return [];
7717
8040
  }
7718
8041
  function resolveSkillsSrc(marketplacePath, selectedPlugin) {
7719
- const pluginsBase = path5.resolve(marketplacePath, "plugins");
7720
- const skillsSrc = path5.resolve(pluginsBase, selectedPlugin, "skills");
7721
- if (!skillsSrc.startsWith(pluginsBase + path5.sep)) {
8042
+ const pluginsBase = path7.resolve(marketplacePath, "plugins");
8043
+ const skillsSrc = path7.resolve(pluginsBase, selectedPlugin, "skills");
8044
+ if (!skillsSrc.startsWith(pluginsBase + path7.sep)) {
7722
8045
  throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
7723
8046
  }
7724
8047
  return skillsSrc;
7725
8048
  }
7726
8049
  function copyPluginSkills(skillsSrc, targetDir) {
7727
- fs3.mkdirSync(targetDir, { recursive: true });
7728
- const resolvedTarget = path5.resolve(targetDir);
7729
- const skillNames = fs3.readdirSync(skillsSrc).filter(
7730
- (name) => fs3.statSync(path5.join(skillsSrc, name)).isDirectory()
8050
+ fs4.mkdirSync(targetDir, { recursive: true });
8051
+ const resolvedTarget = path7.resolve(targetDir);
8052
+ const skillNames = fs4.readdirSync(skillsSrc).filter(
8053
+ (name) => fs4.statSync(path7.join(skillsSrc, name)).isDirectory()
7731
8054
  );
7732
8055
  const installed = [];
7733
8056
  const failed = [];
7734
8057
  for (const skillName of skillNames) {
7735
- const dest = path5.resolve(targetDir, skillName);
7736
- if (!dest.startsWith(resolvedTarget + path5.sep)) {
8058
+ const dest = path7.resolve(targetDir, skillName);
8059
+ if (!dest.startsWith(resolvedTarget + path7.sep)) {
7737
8060
  failed.push(skillName);
7738
8061
  continue;
7739
8062
  }
7740
- fs3.rmSync(dest, { recursive: true, force: true });
7741
- fs3.cpSync(path5.join(skillsSrc, skillName), dest, { recursive: true });
8063
+ fs4.rmSync(dest, { recursive: true, force: true });
8064
+ fs4.cpSync(path7.join(skillsSrc, skillName), dest, { recursive: true });
7742
8065
  installed.push(skillName);
7743
8066
  }
7744
8067
  return { installed, failed };
7745
8068
  }
7746
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
+
7747
8326
  // src/cli.ts
7748
8327
  if (!process.env.PNCLI_VERIFY_TLS) {
7749
8328
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -7783,6 +8362,8 @@ registerServiceNowCommands(program);
7783
8362
  registerContrastCommands(program);
7784
8363
  registerSonatypeIqCommands(program);
7785
8364
  registerSkillsCommands(program);
8365
+ registerJwtCommands(program);
8366
+ registerOpenShiftCommands(program);
7786
8367
  program.addHelpText("after", `
7787
8368
  Services:
7788
8369
  git Local git operations (status, diff, log, branch)
@@ -7800,8 +8381,10 @@ Services:
7800
8381
  servicenow ServiceNow (change requests)
7801
8382
  contrast Contrast IAST (applications, vulnerability findings)
7802
8383
  sonatypeiq Sonatype IQ Server (applications, organizations, policies)
8384
+ openshift OpenShift / Kubernetes (pods, events, logs, metrics)
7803
8385
  config Manage pncli configuration
7804
8386
  skills Download and manage Claude Code skills
8387
+ jwt JWT token utilities (decode header and payload)
7805
8388
  `);
7806
8389
  program.parseAsync(process.argv).catch((err) => {
7807
8390
  if (process.exitCode !== void 0) return;