@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/{chunk-SLQOSGZJ.js → chunk-BNMFIGXF.js} +162 -7
- package/dist/chunk-BNMFIGXF.js.map +1 -0
- package/dist/{chunk-AWSXVWRN.js → chunk-CFUUOW7B.js} +11 -1
- package/dist/chunk-CFUUOW7B.js.map +1 -0
- package/dist/cli.js +717 -134
- package/dist/cli.js.map +1 -1
- package/dist/{config-OFLPO5DN.js → config-TL5X47RY.js} +2 -2
- package/dist/{http-5GDQYH62.js → http-UN6N4WIB.js} +2 -2
- package/package.json +1 -1
- package/skills/pncli/SKILL.md +3 -1
- package/skills/pncli/jwt.md +41 -0
- package/skills/pncli/openshift.md +106 -0
- package/dist/chunk-AWSXVWRN.js.map +0 -1
- package/dist/chunk-SLQOSGZJ.js.map +0 -1
- /package/dist/{config-OFLPO5DN.js.map → config-TL5X47RY.js.map} +0 -0
- /package/dist/{http-5GDQYH62.js.map → http-UN6N4WIB.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
setRepoConfigValue,
|
|
9
9
|
writeGlobalConfig,
|
|
10
10
|
writeRepoConfig
|
|
11
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
|
280
|
-
const
|
|
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(
|
|
287
|
-
byAuthor: buildByAuthor(
|
|
295
|
+
commits: filteredCommits,
|
|
296
|
+
weeks: buildWeeklyBreakdown(filteredCommits),
|
|
297
|
+
byAuthor: buildByAuthor(filteredCommits),
|
|
288
298
|
summary: {
|
|
289
|
-
commitCount:
|
|
299
|
+
commitCount: filteredCommits.length,
|
|
290
300
|
totalInsertions,
|
|
291
301
|
totalDeletions,
|
|
292
302
|
totalNetLines: totalInsertions - totalDeletions,
|
|
293
|
-
totalFilesChanged:
|
|
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
|
|
424
|
+
let path8 = remoteUrl;
|
|
415
425
|
let remoteHostname = "";
|
|
416
|
-
const sshProtoMatch =
|
|
426
|
+
const sshProtoMatch = path8.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
|
|
417
427
|
if (sshProtoMatch) {
|
|
418
428
|
remoteHostname = sshProtoMatch[1].toLowerCase();
|
|
419
|
-
|
|
429
|
+
path8 = sshProtoMatch[2];
|
|
420
430
|
} else {
|
|
421
|
-
const gitAtMatch =
|
|
431
|
+
const gitAtMatch = path8.match(/^git@([^:]+)(?::\d+)?:(.*)/);
|
|
422
432
|
if (gitAtMatch) {
|
|
423
433
|
remoteHostname = gitAtMatch[1].toLowerCase();
|
|
424
|
-
|
|
434
|
+
path8 = "/" + gitAtMatch[2];
|
|
425
435
|
} else {
|
|
426
|
-
const httpsMatch =
|
|
436
|
+
const httpsMatch = path8.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
|
|
427
437
|
if (httpsMatch) {
|
|
428
438
|
remoteHostname = httpsMatch[1].toLowerCase();
|
|
429
|
-
|
|
439
|
+
path8 = httpsMatch[2];
|
|
430
440
|
}
|
|
431
441
|
}
|
|
432
442
|
}
|
|
433
443
|
if (!remoteHostname || remoteHostname !== baseHostname) return null;
|
|
434
|
-
|
|
435
|
-
const gitIdx =
|
|
436
|
-
const sshIdx =
|
|
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(
|
|
449
|
+
const repo = decodeURIComponent(path8.slice(markerIdx + 6));
|
|
440
450
|
if (!repo) return null;
|
|
441
|
-
const before =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2568
|
+
import path2 from "path";
|
|
2434
2569
|
function findNpmManifests(files) {
|
|
2435
2570
|
const lockFiles = new Set(
|
|
2436
2571
|
files.filter((f) => {
|
|
2437
|
-
const base =
|
|
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 (
|
|
2578
|
+
if (path2.basename(file) !== "package.json") continue;
|
|
2444
2579
|
if (file.split("/").includes("node_modules")) continue;
|
|
2445
|
-
const dir =
|
|
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 =
|
|
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
|
|
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 (
|
|
2614
|
-
lockFiles.set(
|
|
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 =
|
|
2620
|
-
const ext =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2902
|
+
import path4 from "path";
|
|
2768
2903
|
function findMavenManifests(files) {
|
|
2769
|
-
const lockFiles = new Set(files.filter((f) =>
|
|
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 =
|
|
2907
|
+
const base = path4.basename(file);
|
|
2773
2908
|
if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
|
|
2774
|
-
const dir =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
4445
|
-
await http.ado(
|
|
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
|
|
4618
|
-
await http.ado(
|
|
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
|
|
5929
|
-
const existing = JSON.parse(
|
|
6245
|
+
const fs5 = await import("fs");
|
|
6246
|
+
const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
5930
6247
|
existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
|
|
5931
|
-
|
|
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
|
|
5961
|
-
const existing = JSON.parse(
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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,
|
|
6647
|
-
const cleanPath =
|
|
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,
|
|
6651
|
-
const cleanPath =
|
|
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,
|
|
6657
|
-
const cleanPath =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
6771
|
-
success({ repo, path:
|
|
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
|
|
7376
|
-
const data = await http.sonatypeiq(
|
|
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
|
|
7386
|
-
import
|
|
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
|
|
7729
|
+
return path7.join(os.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
|
|
7413
7730
|
}
|
|
7414
7731
|
var AGENT_PATHS = {
|
|
7415
|
-
"github-copilot": { project: ".agents/skills", user:
|
|
7416
|
-
"claude-code": { project: ".claude/skills", user:
|
|
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
|
|
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 =
|
|
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 =
|
|
7757
|
+
targetDir = path7.resolve(scopePath);
|
|
7441
7758
|
}
|
|
7442
|
-
const resolvedTarget =
|
|
7759
|
+
const resolvedTarget = path7.resolve(targetDir);
|
|
7443
7760
|
const bundledDir = getBundledSkillsDir();
|
|
7444
7761
|
let skillDirs = [];
|
|
7445
7762
|
if (bundledDir !== "") {
|
|
7446
7763
|
try {
|
|
7447
|
-
skillDirs =
|
|
7448
|
-
(name) =>
|
|
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 =
|
|
7460
|
-
if (!existingDir.startsWith(resolvedTarget +
|
|
7461
|
-
if (
|
|
7462
|
-
|
|
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 =
|
|
7468
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
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 =
|
|
7474
|
-
|
|
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 =
|
|
7477
|
-
|
|
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 =
|
|
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 =
|
|
7830
|
+
targetDir = path7.resolve(scopePath);
|
|
7514
7831
|
}
|
|
7515
|
-
if (!
|
|
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 =
|
|
7520
|
-
const skillPath =
|
|
7521
|
-
return
|
|
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 =
|
|
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 =
|
|
7567
|
-
const hasGit =
|
|
7568
|
-
if (
|
|
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 || !
|
|
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 ?
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
7703
|
-
if (
|
|
8025
|
+
const marketplaceJsonPath = path7.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
8026
|
+
if (fs4.existsSync(marketplaceJsonPath)) {
|
|
7704
8027
|
try {
|
|
7705
|
-
const meta = JSON.parse(
|
|
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 =
|
|
7713
|
-
if (
|
|
7714
|
-
return
|
|
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 =
|
|
7720
|
-
const skillsSrc =
|
|
7721
|
-
if (!skillsSrc.startsWith(pluginsBase +
|
|
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
|
-
|
|
7728
|
-
const resolvedTarget =
|
|
7729
|
-
const skillNames =
|
|
7730
|
-
(name) =>
|
|
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 =
|
|
7736
|
-
if (!dest.startsWith(resolvedTarget +
|
|
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
|
-
|
|
7741
|
-
|
|
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;
|