@kolatts/pncli 1.11.1 → 1.13.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-VPUWMP2M.js → chunk-BNT4SEZD.js} +84 -10
- package/dist/chunk-BNT4SEZD.js.map +1 -0
- package/dist/cli.js +676 -193
- package/dist/cli.js.map +1 -1
- package/dist/{http-LNDRNPP7.js → http-BNCZJQTD.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-VPUWMP2M.js.map +0 -1
- /package/dist/{http-LNDRNPP7.js.map → http-BNCZJQTD.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -18,8 +18,9 @@ import {
|
|
|
18
18
|
setGlobalOptions,
|
|
19
19
|
setGlobalUser,
|
|
20
20
|
success,
|
|
21
|
-
warn
|
|
22
|
-
|
|
21
|
+
warn,
|
|
22
|
+
writeRawOutput
|
|
23
|
+
} from "./chunk-BNT4SEZD.js";
|
|
23
24
|
import {
|
|
24
25
|
AdoCoreClient
|
|
25
26
|
} from "./chunk-VDL32PSG.js";
|
|
@@ -276,29 +277,44 @@ 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
|
}
|
|
307
|
+
function normalizeGitDate(dateStr) {
|
|
308
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
309
|
+
return `${dateStr}T00:00:00`;
|
|
310
|
+
}
|
|
311
|
+
return dateStr;
|
|
312
|
+
}
|
|
297
313
|
function buildReportArgs(opts) {
|
|
298
314
|
const fmt = `${COMMIT_PREFIX}%H %an %aI %s`;
|
|
299
315
|
const args = ["log", `--format=${fmt}`, "--numstat"];
|
|
300
|
-
if (opts.since) args.push(`--since=${opts.since}`);
|
|
301
|
-
if (opts.until) args.push(`--until=${opts.until}`);
|
|
316
|
+
if (opts.since) args.push(`--since=${normalizeGitDate(opts.since)}`);
|
|
317
|
+
if (opts.until) args.push(`--until=${normalizeGitDate(opts.until)}`);
|
|
302
318
|
if (opts.base && opts.branch) {
|
|
303
319
|
args.push(`${opts.base}..${opts.branch}`);
|
|
304
320
|
} else if (opts.base) {
|
|
@@ -405,38 +421,38 @@ function parseAdoRemote(remoteUrl, adoBaseUrl) {
|
|
|
405
421
|
} catch {
|
|
406
422
|
baseHostname = adoBaseUrl.replace(/\/$/, "").replace(/^https?:\/\//, "").split(/[:/]/)[0].toLowerCase();
|
|
407
423
|
}
|
|
408
|
-
let
|
|
424
|
+
let path8 = remoteUrl;
|
|
409
425
|
let remoteHostname = "";
|
|
410
|
-
const sshProtoMatch =
|
|
426
|
+
const sshProtoMatch = path8.match(/^ssh:\/\/[^@]*@?([^/:]+)(?::\d+)?(.*)/);
|
|
411
427
|
if (sshProtoMatch) {
|
|
412
428
|
remoteHostname = sshProtoMatch[1].toLowerCase();
|
|
413
|
-
|
|
429
|
+
path8 = sshProtoMatch[2];
|
|
414
430
|
} else {
|
|
415
|
-
const gitAtMatch =
|
|
431
|
+
const gitAtMatch = path8.match(/^git@([^:]+)(?::\d+)?:(.*)/);
|
|
416
432
|
if (gitAtMatch) {
|
|
417
433
|
remoteHostname = gitAtMatch[1].toLowerCase();
|
|
418
|
-
|
|
434
|
+
path8 = "/" + gitAtMatch[2];
|
|
419
435
|
} else {
|
|
420
|
-
const httpsMatch =
|
|
436
|
+
const httpsMatch = path8.match(/^https?:\/\/([^/:]+)(?::\d+)?(.*)/);
|
|
421
437
|
if (httpsMatch) {
|
|
422
438
|
remoteHostname = httpsMatch[1].toLowerCase();
|
|
423
|
-
|
|
439
|
+
path8 = httpsMatch[2];
|
|
424
440
|
}
|
|
425
441
|
}
|
|
426
442
|
}
|
|
427
443
|
if (!remoteHostname || remoteHostname !== baseHostname) return null;
|
|
428
|
-
|
|
429
|
-
const gitIdx =
|
|
430
|
-
const sshIdx =
|
|
444
|
+
path8 = path8.replace(/\.git$/, "");
|
|
445
|
+
const gitIdx = path8.indexOf("/_git/");
|
|
446
|
+
const sshIdx = path8.indexOf("/_ssh/");
|
|
431
447
|
const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
|
|
432
448
|
if (markerIdx === -1) return null;
|
|
433
|
-
const repo =
|
|
449
|
+
const repo = decodeURIComponent(path8.slice(markerIdx + 6));
|
|
434
450
|
if (!repo) return null;
|
|
435
|
-
const before =
|
|
451
|
+
const before = path8.slice(1, markerIdx);
|
|
436
452
|
const parts = before.split("/").filter(Boolean);
|
|
437
453
|
if (parts.length < 2) return null;
|
|
438
|
-
const project = parts[parts.length - 1];
|
|
439
|
-
const collection = parts[parts.length - 2];
|
|
454
|
+
const project = decodeURIComponent(parts[parts.length - 1]);
|
|
455
|
+
const collection = decodeURIComponent(parts[parts.length - 2]);
|
|
440
456
|
return { collection, project, repo };
|
|
441
457
|
}
|
|
442
458
|
function getRemoteUrls(repoRoot) {
|
|
@@ -736,7 +752,7 @@ function registerGitCommands(program2) {
|
|
|
736
752
|
until: opts.until
|
|
737
753
|
});
|
|
738
754
|
if (opts.csv) {
|
|
739
|
-
|
|
755
|
+
writeRawOutput(formatBranchReportCsv(report));
|
|
740
756
|
} else {
|
|
741
757
|
success(report, "git", "report", start);
|
|
742
758
|
}
|
|
@@ -776,7 +792,13 @@ function registerGitCommands(program2) {
|
|
|
776
792
|
});
|
|
777
793
|
}
|
|
778
794
|
|
|
795
|
+
// src/services/jira/commands.ts
|
|
796
|
+
import { readFileSync as readFileSync2, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
797
|
+
import path from "path";
|
|
798
|
+
|
|
779
799
|
// src/services/jira/client.ts
|
|
800
|
+
import { readFileSync } from "fs";
|
|
801
|
+
import { basename, extname } from "path";
|
|
780
802
|
var API2 = "/rest/api/2";
|
|
781
803
|
var JiraClient = class {
|
|
782
804
|
constructor(http) {
|
|
@@ -796,6 +818,7 @@ var JiraClient = class {
|
|
|
796
818
|
...opts.priority ? { priority: { name: opts.priority } } : {},
|
|
797
819
|
...opts.assignee ? { assignee: { name: opts.assignee } } : {},
|
|
798
820
|
...opts.labels?.length ? { labels: opts.labels } : {},
|
|
821
|
+
...opts.parent ? { parent: { key: opts.parent } } : {},
|
|
799
822
|
...opts.customFieldValues ?? {}
|
|
800
823
|
}
|
|
801
824
|
};
|
|
@@ -864,7 +887,86 @@ var JiraClient = class {
|
|
|
864
887
|
return { issues: allIssues, total: allIssues.length, startAt: 0, maxResults: allIssues.length };
|
|
865
888
|
}
|
|
866
889
|
async fetchFields() {
|
|
867
|
-
|
|
890
|
+
const fields = await this.http.jira(`${API2}/field`);
|
|
891
|
+
return fields.map((f) => ({ ...f, pncliType: schemaToPncliType(f.schema) }));
|
|
892
|
+
}
|
|
893
|
+
async fetchFieldsWithAllowedValues(project, issueType) {
|
|
894
|
+
const fields = await this.fetchFields();
|
|
895
|
+
let metaFields = {};
|
|
896
|
+
try {
|
|
897
|
+
const meta = await this.http.jira(`${API2}/issue/createmeta`, {
|
|
898
|
+
params: { projectKeys: project, expand: "projects.issuetypes.fields" }
|
|
899
|
+
});
|
|
900
|
+
metaFields = collectMetaFields(meta.projects, issueType);
|
|
901
|
+
} catch {
|
|
902
|
+
try {
|
|
903
|
+
metaFields = await this.fetchCreateMetaViaIssuetypes(project, issueType);
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return fields.map((f) => {
|
|
908
|
+
const av = metaFields[f.id]?.allowedValues;
|
|
909
|
+
return av?.length ? { ...f, allowedValues: av } : f;
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Fallback for instances where the classic createmeta endpoint returns 404.
|
|
914
|
+
* Uses the paginated endpoint available in Jira Datacenter 8.4+.
|
|
915
|
+
*/
|
|
916
|
+
async fetchCreateMetaViaIssuetypes(project, issueType) {
|
|
917
|
+
const itList = await this.http.jira(
|
|
918
|
+
`${API2}/issue/createmeta/${encodeURIComponent(project)}/issuetypes`
|
|
919
|
+
);
|
|
920
|
+
const matchingTypes = issueType ? itList.values.filter((it) => it.name.toLowerCase() === issueType.toLowerCase()) : itList.values;
|
|
921
|
+
const result = {};
|
|
922
|
+
for (const it of matchingTypes) {
|
|
923
|
+
const fieldList = await this.http.jira(
|
|
924
|
+
`${API2}/issue/createmeta/${encodeURIComponent(project)}/issuetypes/${it.id}`
|
|
925
|
+
);
|
|
926
|
+
for (const field of fieldList.values) {
|
|
927
|
+
if (!field.allowedValues?.length) continue;
|
|
928
|
+
const existing = result[field.fieldId]?.allowedValues ?? [];
|
|
929
|
+
const seen = new Set(existing.map((v) => v.id));
|
|
930
|
+
for (const av of field.allowedValues) {
|
|
931
|
+
if (!seen.has(av.id)) {
|
|
932
|
+
existing.push(av);
|
|
933
|
+
seen.add(av.id);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
result[field.fieldId] = { allowedValues: existing };
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
async addLabels(key, labels) {
|
|
942
|
+
await this.http.jira(`${API2}/issue/${key}`, {
|
|
943
|
+
method: "PUT",
|
|
944
|
+
body: { update: { labels: labels.map((l) => ({ add: l })) } }
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async removeLabels(key, labels) {
|
|
948
|
+
await this.http.jira(`${API2}/issue/${key}`, {
|
|
949
|
+
method: "PUT",
|
|
950
|
+
body: { update: { labels: labels.map((l) => ({ remove: l })) } }
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
async uploadAttachment(key, filePath) {
|
|
954
|
+
const fileContent = readFileSync(filePath);
|
|
955
|
+
const fileName = basename(filePath);
|
|
956
|
+
const mimeType = guessMimeType(filePath);
|
|
957
|
+
const formData = new FormData();
|
|
958
|
+
formData.append("file", new Blob([fileContent], { type: mimeType }), fileName);
|
|
959
|
+
return this.http.jiraUpload(`${API2}/issue/${key}/attachments`, formData);
|
|
960
|
+
}
|
|
961
|
+
async listAttachments(key) {
|
|
962
|
+
const result = await this.http.jira(
|
|
963
|
+
`${API2}/issue/${key}`,
|
|
964
|
+
{ params: { fields: "attachment" } }
|
|
965
|
+
);
|
|
966
|
+
return result.fields.attachment ?? [];
|
|
967
|
+
}
|
|
968
|
+
async downloadAttachment(contentUrl) {
|
|
969
|
+
return this.http.jiraBuffer(contentUrl);
|
|
868
970
|
}
|
|
869
971
|
async assignIssue(key, accountId) {
|
|
870
972
|
await this.http.jira(`${API2}/issue/${key}/assignee`, {
|
|
@@ -894,6 +996,70 @@ var JiraClient = class {
|
|
|
894
996
|
});
|
|
895
997
|
}
|
|
896
998
|
};
|
|
999
|
+
function guessMimeType(filePath) {
|
|
1000
|
+
const ext = extname(filePath).toLowerCase();
|
|
1001
|
+
const map = {
|
|
1002
|
+
".jpg": "image/jpeg",
|
|
1003
|
+
".jpeg": "image/jpeg",
|
|
1004
|
+
".png": "image/png",
|
|
1005
|
+
".gif": "image/gif",
|
|
1006
|
+
".svg": "image/svg+xml",
|
|
1007
|
+
".pdf": "application/pdf",
|
|
1008
|
+
".txt": "text/plain",
|
|
1009
|
+
".log": "text/plain",
|
|
1010
|
+
".csv": "text/csv",
|
|
1011
|
+
".json": "application/json",
|
|
1012
|
+
".xml": "application/xml",
|
|
1013
|
+
".zip": "application/zip",
|
|
1014
|
+
".tar": "application/x-tar",
|
|
1015
|
+
".gz": "application/gzip",
|
|
1016
|
+
".md": "text/markdown",
|
|
1017
|
+
".html": "text/html",
|
|
1018
|
+
".htm": "text/html",
|
|
1019
|
+
".doc": "application/msword",
|
|
1020
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1021
|
+
".xls": "application/vnd.ms-excel",
|
|
1022
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
1023
|
+
};
|
|
1024
|
+
return map[ext] ?? "application/octet-stream";
|
|
1025
|
+
}
|
|
1026
|
+
function schemaToPncliType(schema) {
|
|
1027
|
+
if (!schema) return void 0;
|
|
1028
|
+
const custom = schema.custom ?? "";
|
|
1029
|
+
if (custom.includes("cascadingselect")) return "cascading-select";
|
|
1030
|
+
if (custom.includes("multicheckboxes") || custom.includes("multiselect")) return "multi-select";
|
|
1031
|
+
if (custom.includes("select") || custom.includes("radiobuttons")) return "select";
|
|
1032
|
+
if (custom.includes("userpicker") || custom.includes("multiuserpicker")) return "user";
|
|
1033
|
+
if (custom.includes("labels")) return "labels";
|
|
1034
|
+
if (custom.includes("url")) return "url";
|
|
1035
|
+
if (custom.includes("textarea") || custom.includes("textfield")) return "textarea";
|
|
1036
|
+
if (schema.type === "number") return "number";
|
|
1037
|
+
if (schema.type === "date") return "date";
|
|
1038
|
+
if (schema.type === "datetime") return "datetime";
|
|
1039
|
+
if (schema.type === "string") return "string";
|
|
1040
|
+
return void 0;
|
|
1041
|
+
}
|
|
1042
|
+
function collectMetaFields(projects, issueType) {
|
|
1043
|
+
const result = {};
|
|
1044
|
+
for (const proj of projects) {
|
|
1045
|
+
for (const it of proj.issuetypes) {
|
|
1046
|
+
if (issueType && it.name.toLowerCase() !== issueType.toLowerCase()) continue;
|
|
1047
|
+
for (const [fieldId, fieldMeta] of Object.entries(it.fields)) {
|
|
1048
|
+
if (!fieldMeta.allowedValues?.length) continue;
|
|
1049
|
+
const existing = result[fieldId]?.allowedValues ?? [];
|
|
1050
|
+
const seen = new Set(existing.map((v) => v.id));
|
|
1051
|
+
for (const av of fieldMeta.allowedValues) {
|
|
1052
|
+
if (!seen.has(av.id)) {
|
|
1053
|
+
existing.push(av);
|
|
1054
|
+
seen.add(av.id);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
result[fieldId] = { allowedValues: existing };
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
897
1063
|
|
|
898
1064
|
// src/services/jira/custom-fields.ts
|
|
899
1065
|
function buildFieldMap(fields) {
|
|
@@ -937,6 +1103,15 @@ function formatFieldValue(value, type) {
|
|
|
937
1103
|
return { value };
|
|
938
1104
|
case "multi-select":
|
|
939
1105
|
return value.split(",").map((v) => ({ value: v.trim() }));
|
|
1106
|
+
case "option-id":
|
|
1107
|
+
return { id: value };
|
|
1108
|
+
case "cascading-select": {
|
|
1109
|
+
const colon = value.indexOf(":");
|
|
1110
|
+
if (colon !== -1) {
|
|
1111
|
+
return { id: value.slice(0, colon).trim(), child: { id: value.slice(colon + 1).trim() } };
|
|
1112
|
+
}
|
|
1113
|
+
return { id: value.trim() };
|
|
1114
|
+
}
|
|
940
1115
|
case "labels":
|
|
941
1116
|
return value.split(",").map((v) => v.trim());
|
|
942
1117
|
case "user":
|
|
@@ -986,41 +1161,41 @@ function registerJiraCommands(program2) {
|
|
|
986
1161
|
fail(err, "jira", "get-issue", start);
|
|
987
1162
|
}
|
|
988
1163
|
});
|
|
989
|
-
jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>",
|
|
1164
|
+
jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", "Parent issue key \u2014 sets fields.parent in the create payload").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").action(async (opts) => {
|
|
990
1165
|
const start = Date.now();
|
|
1166
|
+
let fieldMap = buildFieldMap([]);
|
|
991
1167
|
try {
|
|
992
|
-
const
|
|
1168
|
+
const resolved = getClientAndFields(program2);
|
|
1169
|
+
const client = resolved.client;
|
|
1170
|
+
fieldMap = resolved.fieldMap;
|
|
993
1171
|
const defaults = getDefaults(program2);
|
|
994
1172
|
const project = opts.project ?? defaults.project;
|
|
995
1173
|
const issueType = opts.type ?? defaults.issueType ?? "Task";
|
|
996
1174
|
const priority = opts.priority ?? defaults.priority;
|
|
997
1175
|
if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
|
|
998
1176
|
const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1177
|
+
const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
|
|
1178
|
+
const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
|
|
1179
|
+
const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, parent: opts.parent, customFieldValues });
|
|
1001
1180
|
success(data, "jira", "create-issue", start);
|
|
1002
|
-
if (opts.parent) {
|
|
1003
|
-
try {
|
|
1004
|
-
await client.linkIssue({ key: data.key, linkType: "is child of", target: opts.parent });
|
|
1005
|
-
} catch (linkErr) {
|
|
1006
|
-
warn(`Created ${data.key} but failed to link to parent ${opts.parent}: ${linkErr instanceof Error ? linkErr.message : linkErr}`);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
return;
|
|
1010
1181
|
} catch (err) {
|
|
1011
|
-
fail(err, "jira", "create-issue", start);
|
|
1182
|
+
fail(translateFieldErrors(err, fieldMap), "jira", "create-issue", start);
|
|
1012
1183
|
}
|
|
1013
1184
|
});
|
|
1014
|
-
jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
|
|
1185
|
+
jira.command("update-issue").description("Update a Jira issue").requiredOption("--key <issue-key>", "Issue key").option("--summary <text>", "New summary").option("--description <text>", "New description").option("--priority <name>", "New priority").option("--assignee <accountId>", "New assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value; use Name=@file.json to read value from file (repeatable)", (val, acc) => [...acc, val], []).option("--fields-file <path>", "Path to a JSON file mapping field names/IDs to their Jira API values").action(async (opts) => {
|
|
1015
1186
|
const start = Date.now();
|
|
1187
|
+
let fieldMap = buildFieldMap([]);
|
|
1016
1188
|
try {
|
|
1017
|
-
const
|
|
1189
|
+
const resolved = getClientAndFields(program2);
|
|
1190
|
+
const client = resolved.client;
|
|
1191
|
+
fieldMap = resolved.fieldMap;
|
|
1018
1192
|
const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
|
|
1019
|
-
const
|
|
1193
|
+
const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
|
|
1194
|
+
const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
|
|
1020
1195
|
await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
|
|
1021
1196
|
success({ updated: opts.key }, "jira", "update-issue", start);
|
|
1022
1197
|
} catch (err) {
|
|
1023
|
-
fail(err, "jira", "update-issue", start);
|
|
1198
|
+
fail(translateFieldErrors(err, fieldMap), "jira", "update-issue", start);
|
|
1024
1199
|
}
|
|
1025
1200
|
});
|
|
1026
1201
|
jira.command("transition-issue").description("Transition a Jira issue to a new status").requiredOption("--key <issue-key>", "Issue key").requiredOption("--transition <name-or-id>", "Transition name or ID").action(async (opts) => {
|
|
@@ -1086,6 +1261,28 @@ function registerJiraCommands(program2) {
|
|
|
1086
1261
|
fail(err, "jira", "search", start);
|
|
1087
1262
|
}
|
|
1088
1263
|
});
|
|
1264
|
+
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) => {
|
|
1265
|
+
const start = Date.now();
|
|
1266
|
+
try {
|
|
1267
|
+
const client = getClient(program2);
|
|
1268
|
+
const labels = opts.labels.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1269
|
+
await client.addLabels(opts.key, labels);
|
|
1270
|
+
success({ updated: opts.key, labelsAdded: labels }, "jira", "add-label", start);
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
fail(err, "jira", "add-label", start);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
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) => {
|
|
1276
|
+
const start = Date.now();
|
|
1277
|
+
try {
|
|
1278
|
+
const client = getClient(program2);
|
|
1279
|
+
const labels = opts.labels.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1280
|
+
await client.removeLabels(opts.key, labels);
|
|
1281
|
+
success({ updated: opts.key, labelsRemoved: labels }, "jira", "remove-label", start);
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
fail(err, "jira", "remove-label", start);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1089
1286
|
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) => {
|
|
1090
1287
|
const start = Date.now();
|
|
1091
1288
|
try {
|
|
@@ -1106,12 +1303,50 @@ function registerJiraCommands(program2) {
|
|
|
1106
1303
|
fail(err, "jira", "link-issue", start);
|
|
1107
1304
|
}
|
|
1108
1305
|
});
|
|
1109
|
-
jira.command("
|
|
1306
|
+
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) => {
|
|
1307
|
+
const start = Date.now();
|
|
1308
|
+
try {
|
|
1309
|
+
if (!existsSync(opts.file)) throw new PncliError(`File not found: ${opts.file}`, 1);
|
|
1310
|
+
const client = getClient(program2);
|
|
1311
|
+
const data = await client.uploadAttachment(opts.key, opts.file);
|
|
1312
|
+
success(data, "jira", "add-attachment", start);
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
fail(err, "jira", "add-attachment", start);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
jira.command("list-attachments").description("List attachments on a Jira issue").requiredOption("--key <issue-key>", "Issue key (e.g. PROJ-123)").action(async (opts) => {
|
|
1318
|
+
const start = Date.now();
|
|
1319
|
+
try {
|
|
1320
|
+
const client = getClient(program2);
|
|
1321
|
+
const data = await client.listAttachments(opts.key);
|
|
1322
|
+
success(data, "jira", "list-attachments", start);
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
fail(err, "jira", "list-attachments", start);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
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) => {
|
|
1328
|
+
const start = Date.now();
|
|
1329
|
+
try {
|
|
1330
|
+
const client = getClient(program2);
|
|
1331
|
+
const attachments = await client.listAttachments(opts.key);
|
|
1332
|
+
const attachment = attachments.find((a) => a.id === opts.attachmentId);
|
|
1333
|
+
if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
|
|
1334
|
+
const outDir = opts.dir ?? path.join(process.cwd(), ".pncli");
|
|
1335
|
+
mkdirSync(outDir, { recursive: true });
|
|
1336
|
+
const outPath = path.join(outDir, path.basename(attachment.filename));
|
|
1337
|
+
const buffer = await client.downloadAttachment(attachment.content);
|
|
1338
|
+
writeFileSync(outPath, buffer);
|
|
1339
|
+
success({ saved: outPath, filename: attachment.filename, size: buffer.length }, "jira", "download-attachment", start);
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
fail(err, "jira", "download-attachment", start);
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
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) => {
|
|
1110
1345
|
const start = Date.now();
|
|
1111
1346
|
try {
|
|
1112
1347
|
if (opts.discover) {
|
|
1113
1348
|
const client = getClient(program2);
|
|
1114
|
-
const fields = await client.fetchFields();
|
|
1349
|
+
const fields = opts.project ? await client.fetchFieldsWithAllowedValues(opts.project, opts.issueType) : await client.fetchFields();
|
|
1115
1350
|
const result = opts.customOnly ? fields.filter((f) => f.custom) : fields;
|
|
1116
1351
|
success(result, "jira", "fields", start);
|
|
1117
1352
|
} else {
|
|
@@ -1130,22 +1365,76 @@ function parseFieldArgs(fieldArgs, fieldMap) {
|
|
|
1130
1365
|
if (eq === -1) throw new PncliError(`Invalid --field format (expected Name=value): ${arg}`, 1);
|
|
1131
1366
|
const name = arg.slice(0, eq).trim();
|
|
1132
1367
|
const value = arg.slice(eq + 1);
|
|
1133
|
-
const def = fieldMap.byName.get(name.toLowerCase());
|
|
1134
|
-
if (!def) throw new PncliError(
|
|
1135
|
-
|
|
1368
|
+
const def = fieldMap.byName.get(name.toLowerCase()) ?? fieldMap.byId.get(name);
|
|
1369
|
+
if (!def) throw new PncliError(
|
|
1370
|
+
`Unknown custom field: "${name}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
|
|
1371
|
+
1
|
|
1372
|
+
);
|
|
1373
|
+
if (value.startsWith("@")) {
|
|
1374
|
+
const filePath = value.slice(1);
|
|
1375
|
+
let raw;
|
|
1376
|
+
try {
|
|
1377
|
+
raw = readFileSync2(filePath, "utf8").trim();
|
|
1378
|
+
} catch (e) {
|
|
1379
|
+
throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
result[def.id] = JSON.parse(raw);
|
|
1383
|
+
} catch {
|
|
1384
|
+
result[def.id] = raw;
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
result[def.id] = formatFieldValue(value, def.type);
|
|
1388
|
+
}
|
|
1136
1389
|
}
|
|
1137
1390
|
return result;
|
|
1138
1391
|
}
|
|
1392
|
+
function parseFieldsFile(filePath, fieldMap) {
|
|
1393
|
+
let raw;
|
|
1394
|
+
try {
|
|
1395
|
+
raw = readFileSync2(filePath, "utf8");
|
|
1396
|
+
} catch (e) {
|
|
1397
|
+
throw new PncliError(`Cannot read fields file "${filePath}": ${e.message}`, 1);
|
|
1398
|
+
}
|
|
1399
|
+
let data;
|
|
1400
|
+
try {
|
|
1401
|
+
data = JSON.parse(raw);
|
|
1402
|
+
} catch {
|
|
1403
|
+
throw new PncliError(`Fields file "${filePath}" must be a valid JSON object`, 1);
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
1406
|
+
throw new PncliError(`Fields file "${filePath}" must be a JSON object`, 1);
|
|
1407
|
+
}
|
|
1408
|
+
const result = {};
|
|
1409
|
+
for (const [key, val] of Object.entries(data)) {
|
|
1410
|
+
const def = fieldMap.byName.get(key.toLowerCase()) ?? fieldMap.byId.get(key);
|
|
1411
|
+
if (!def) throw new PncliError(
|
|
1412
|
+
`Unknown field in "${filePath}": "${key}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
|
|
1413
|
+
1
|
|
1414
|
+
);
|
|
1415
|
+
result[def.id] = val;
|
|
1416
|
+
}
|
|
1417
|
+
return result;
|
|
1418
|
+
}
|
|
1419
|
+
function translateFieldErrors(err, fieldMap) {
|
|
1420
|
+
if (!(err instanceof PncliError) || fieldMap.byId.size === 0) return err;
|
|
1421
|
+
const translated = err.message.replace(/\bcustomfield_\d+\b/g, (id) => {
|
|
1422
|
+
const def = fieldMap.byId.get(id);
|
|
1423
|
+
return def ? `${def.name} (${id})` : id;
|
|
1424
|
+
});
|
|
1425
|
+
if (translated === err.message) return err;
|
|
1426
|
+
return new PncliError(translated, err.status, err.url);
|
|
1427
|
+
}
|
|
1139
1428
|
|
|
1140
1429
|
// src/services/bitbucket/commands.ts
|
|
1141
|
-
function getClient2(program2) {
|
|
1430
|
+
function getClient2(program2, overrides) {
|
|
1142
1431
|
const opts = program2.optsWithGlobals();
|
|
1143
1432
|
const config = loadConfig({ configPath: opts.config });
|
|
1144
1433
|
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
1145
1434
|
const client = new BitbucketClient(http);
|
|
1146
1435
|
const ctx = getGitContext(config);
|
|
1147
|
-
const project = opts.project ?? ctx?.project ?? config.defaults.bitbucket?.project ?? "";
|
|
1148
|
-
const repo = opts.repo ?? ctx?.repo ?? config.defaults.bitbucket?.repo ?? "";
|
|
1436
|
+
const project = overrides?.project ?? opts.project ?? ctx?.project ?? config.defaults.bitbucket?.project ?? "";
|
|
1437
|
+
const repo = overrides?.repo ?? opts.repo ?? ctx?.repo ?? config.defaults.bitbucket?.repo ?? "";
|
|
1149
1438
|
if (!project || !repo) {
|
|
1150
1439
|
throw new PncliError(
|
|
1151
1440
|
"Could not determine Bitbucket project/repo. Pass --project and --repo, or run pncli config init.",
|
|
@@ -1176,10 +1465,10 @@ function registerBitbucketCommands(program2) {
|
|
|
1176
1465
|
fail(err, "bitbucket", "get-pr", start);
|
|
1177
1466
|
}
|
|
1178
1467
|
});
|
|
1179
|
-
bb.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--source <branch>", "Source branch").option("--target <branch>", "Target branch (defaults to config)").option("--description <desc>", "PR description").option("--reviewers <users>", "Comma-separated reviewer usernames").action(async (opts) => {
|
|
1468
|
+
bb.command("create-pr").description("Create a pull request").requiredOption("--title <title>", "PR title").requiredOption("--source <branch>", "Source branch").option("--target <branch>", "Target branch (defaults to config)").option("--description <desc>", "PR description").option("--reviewers <users>", "Comma-separated reviewer usernames").option("--project <key>", "Bitbucket project key (overrides parent --project; use ~username for personal repos)").option("--repo <slug>", "Bitbucket repository slug (overrides parent --repo)").action(async (opts) => {
|
|
1180
1469
|
const start = Date.now();
|
|
1181
1470
|
try {
|
|
1182
|
-
const { client, project, repo } = getClient2(bb);
|
|
1471
|
+
const { client, project, repo } = getClient2(bb, { project: opts.project, repo: opts.repo });
|
|
1183
1472
|
const config = loadConfig({ configPath: bb.optsWithGlobals().config });
|
|
1184
1473
|
const target = opts.target ?? config.defaults.bitbucket?.targetBranch ?? "main";
|
|
1185
1474
|
const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : [];
|
|
@@ -1403,9 +1692,9 @@ var ConfluenceClient = class {
|
|
|
1403
1692
|
const initialStart = opts.start ?? 0;
|
|
1404
1693
|
return this.http.confluencePaginate(async (start, limit) => {
|
|
1405
1694
|
return this.http.confluence(`${API3}/content`, {
|
|
1406
|
-
params: { spaceKey, type: "page", expand: "version", start: initialStart + start, limit
|
|
1695
|
+
params: { spaceKey, type: "page", expand: "version", start: initialStart + start, limit }
|
|
1407
1696
|
});
|
|
1408
|
-
});
|
|
1697
|
+
}, opts.limit);
|
|
1409
1698
|
}
|
|
1410
1699
|
async getPageChildren(id) {
|
|
1411
1700
|
return this.http.confluencePaginate(async (start, limit) => {
|
|
@@ -1508,11 +1797,11 @@ var ConfluenceClient = class {
|
|
|
1508
1797
|
return this.http.confluence(`${API3}/space`, {
|
|
1509
1798
|
params: {
|
|
1510
1799
|
start: initialStart + start,
|
|
1511
|
-
limit
|
|
1800
|
+
limit,
|
|
1512
1801
|
...opts.type ? { type: opts.type } : {}
|
|
1513
1802
|
}
|
|
1514
1803
|
});
|
|
1515
|
-
});
|
|
1804
|
+
}, opts.limit);
|
|
1516
1805
|
}
|
|
1517
1806
|
async getSpace(key) {
|
|
1518
1807
|
return this.http.confluence(`${API3}/space/${key}`, {
|
|
@@ -1559,7 +1848,7 @@ function registerConfluenceCommands(program2) {
|
|
|
1559
1848
|
fail(err, "confluence", "get-page-by-title", start);
|
|
1560
1849
|
}
|
|
1561
1850
|
});
|
|
1562
|
-
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
|
|
1851
|
+
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) => {
|
|
1563
1852
|
const start = Date.now();
|
|
1564
1853
|
try {
|
|
1565
1854
|
const client = getClient3(program2);
|
|
@@ -1691,7 +1980,7 @@ function registerConfluenceCommands(program2) {
|
|
|
1691
1980
|
fail(err, "confluence", "remove-label", start);
|
|
1692
1981
|
}
|
|
1693
1982
|
});
|
|
1694
|
-
confluence.command("list-spaces").description("List Confluence spaces").option("--type <type>", "Space type: global or personal").option("--limit <n>", "Max
|
|
1983
|
+
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) => {
|
|
1695
1984
|
const start = Date.now();
|
|
1696
1985
|
try {
|
|
1697
1986
|
const client = getClient3(program2);
|
|
@@ -2267,23 +2556,23 @@ import { Option } from "commander";
|
|
|
2267
2556
|
|
|
2268
2557
|
// src/services/deps/parsers/index.ts
|
|
2269
2558
|
import fs from "fs";
|
|
2270
|
-
import
|
|
2559
|
+
import path5 from "path";
|
|
2271
2560
|
import { execSync as execSync3, execFileSync as execFileSync2 } from "child_process";
|
|
2272
2561
|
|
|
2273
2562
|
// src/services/deps/parsers/npm.ts
|
|
2274
|
-
import
|
|
2563
|
+
import path2 from "path";
|
|
2275
2564
|
function findNpmManifests(files) {
|
|
2276
2565
|
const lockFiles = new Set(
|
|
2277
2566
|
files.filter((f) => {
|
|
2278
|
-
const base =
|
|
2567
|
+
const base = path2.basename(f);
|
|
2279
2568
|
return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
|
|
2280
2569
|
})
|
|
2281
2570
|
);
|
|
2282
2571
|
const manifests = [];
|
|
2283
2572
|
for (const file of files) {
|
|
2284
|
-
if (
|
|
2573
|
+
if (path2.basename(file) !== "package.json") continue;
|
|
2285
2574
|
if (file.split("/").includes("node_modules")) continue;
|
|
2286
|
-
const dir =
|
|
2575
|
+
const dir = path2.dirname(file);
|
|
2287
2576
|
let lockFile;
|
|
2288
2577
|
for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
|
|
2289
2578
|
const candidate = dir === "." ? lf : `${dir}/${lf}`;
|
|
@@ -2298,7 +2587,7 @@ function findNpmManifests(files) {
|
|
|
2298
2587
|
}
|
|
2299
2588
|
function parseNpmPackages(content, manifest, opts, lockContent) {
|
|
2300
2589
|
if (lockContent && manifest.lockFile) {
|
|
2301
|
-
const base =
|
|
2590
|
+
const base = path2.basename(manifest.lockFile);
|
|
2302
2591
|
if (base === "package-lock.json") {
|
|
2303
2592
|
const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
|
|
2304
2593
|
if (pkgs.length > 0) return pkgs;
|
|
@@ -2446,26 +2735,26 @@ function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
|
|
|
2446
2735
|
}
|
|
2447
2736
|
|
|
2448
2737
|
// src/services/deps/parsers/nuget.ts
|
|
2449
|
-
import
|
|
2738
|
+
import path3 from "path";
|
|
2450
2739
|
var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
|
|
2451
2740
|
function findNugetManifests(files) {
|
|
2452
2741
|
const lockFiles = /* @__PURE__ */ new Map();
|
|
2453
2742
|
for (const f of files) {
|
|
2454
|
-
if (
|
|
2455
|
-
lockFiles.set(
|
|
2743
|
+
if (path3.basename(f) === "packages.lock.json") {
|
|
2744
|
+
lockFiles.set(path3.dirname(f), f);
|
|
2456
2745
|
}
|
|
2457
2746
|
}
|
|
2458
2747
|
const manifests = [];
|
|
2459
2748
|
for (const file of files) {
|
|
2460
|
-
const base =
|
|
2461
|
-
const ext =
|
|
2749
|
+
const base = path3.basename(file);
|
|
2750
|
+
const ext = path3.extname(file);
|
|
2462
2751
|
if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
|
|
2463
|
-
const dir =
|
|
2752
|
+
const dir = path3.dirname(file);
|
|
2464
2753
|
const lockFile = lockFiles.get(dir);
|
|
2465
2754
|
manifests.push({ file, ecosystem: "nuget", lockFile });
|
|
2466
2755
|
}
|
|
2467
2756
|
if (base === "Directory.Packages.props") {
|
|
2468
|
-
const dir =
|
|
2757
|
+
const dir = path3.dirname(file);
|
|
2469
2758
|
manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
|
|
2470
2759
|
}
|
|
2471
2760
|
}
|
|
@@ -2484,7 +2773,7 @@ function findNugetManifests(files) {
|
|
|
2484
2773
|
return deduped;
|
|
2485
2774
|
}
|
|
2486
2775
|
function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
|
|
2487
|
-
const base =
|
|
2776
|
+
const base = path3.basename(manifest.file);
|
|
2488
2777
|
if (lockContent) {
|
|
2489
2778
|
return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
|
|
2490
2779
|
}
|
|
@@ -2605,14 +2894,14 @@ function parsePackagesLock(content, filePath, opts) {
|
|
|
2605
2894
|
}
|
|
2606
2895
|
|
|
2607
2896
|
// src/services/deps/parsers/maven.ts
|
|
2608
|
-
import
|
|
2897
|
+
import path4 from "path";
|
|
2609
2898
|
function findMavenManifests(files) {
|
|
2610
|
-
const lockFiles = new Set(files.filter((f) =>
|
|
2899
|
+
const lockFiles = new Set(files.filter((f) => path4.basename(f) === "gradle.lockfile"));
|
|
2611
2900
|
const manifests = [];
|
|
2612
2901
|
for (const file of files) {
|
|
2613
|
-
const base =
|
|
2902
|
+
const base = path4.basename(file);
|
|
2614
2903
|
if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
|
|
2615
|
-
const dir =
|
|
2904
|
+
const dir = path4.dirname(file);
|
|
2616
2905
|
const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
|
|
2617
2906
|
const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
|
|
2618
2907
|
manifests.push({ file, ecosystem: "maven", lockFile });
|
|
@@ -2621,7 +2910,7 @@ function findMavenManifests(files) {
|
|
|
2621
2910
|
return manifests;
|
|
2622
2911
|
}
|
|
2623
2912
|
function parseMavenPackages(content, manifest, opts, lockContent) {
|
|
2624
|
-
const base =
|
|
2913
|
+
const base = path4.basename(manifest.file);
|
|
2625
2914
|
if (lockContent && manifest.lockFile) {
|
|
2626
2915
|
return parseGradleLock(lockContent, manifest.lockFile, opts);
|
|
2627
2916
|
}
|
|
@@ -2782,7 +3071,7 @@ function getRepoFiles(repoRoot) {
|
|
|
2782
3071
|
}
|
|
2783
3072
|
function readFile(repoRoot, relPath) {
|
|
2784
3073
|
try {
|
|
2785
|
-
return fs.readFileSync(
|
|
3074
|
+
return fs.readFileSync(path5.join(repoRoot, relPath), "utf8");
|
|
2786
3075
|
} catch {
|
|
2787
3076
|
return null;
|
|
2788
3077
|
}
|
|
@@ -2800,13 +3089,13 @@ function readFileAtRef(repoRoot, ref, relPath) {
|
|
|
2800
3089
|
}
|
|
2801
3090
|
}
|
|
2802
3091
|
function findPropsContent(manifestFile, readFn) {
|
|
2803
|
-
let dir =
|
|
3092
|
+
let dir = path5.dirname(manifestFile);
|
|
2804
3093
|
for (let depth = 0; depth < 10; depth++) {
|
|
2805
3094
|
const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
|
|
2806
3095
|
const content = readFn(candidate);
|
|
2807
3096
|
if (content) return content;
|
|
2808
3097
|
if (dir === ".") break;
|
|
2809
|
-
dir =
|
|
3098
|
+
dir = path5.dirname(dir);
|
|
2810
3099
|
}
|
|
2811
3100
|
return null;
|
|
2812
3101
|
}
|
|
@@ -4084,6 +4373,24 @@ var AdoWorkClient = class {
|
|
|
4084
4373
|
{ method: "POST", body: { text } }
|
|
4085
4374
|
);
|
|
4086
4375
|
}
|
|
4376
|
+
async addTags(collection, id, tags) {
|
|
4377
|
+
const item = await this.getWorkItem(collection, id);
|
|
4378
|
+
const existing = item.fields["System.Tags"] ?? "";
|
|
4379
|
+
const current = existing.split(";").map((t) => t.trim()).filter(Boolean);
|
|
4380
|
+
const lowerSet = new Set(current.map((t) => t.toLowerCase()));
|
|
4381
|
+
const toAdd = tags.filter((t) => !lowerSet.has(t.toLowerCase()));
|
|
4382
|
+
const merged = [...current, ...toAdd];
|
|
4383
|
+
const patch = [{ op: "add", path: "/fields/System.Tags", value: merged.join("; ") }];
|
|
4384
|
+
return this.updateWorkItem(collection, id, patch);
|
|
4385
|
+
}
|
|
4386
|
+
async removeTags(collection, id, tags) {
|
|
4387
|
+
const item = await this.getWorkItem(collection, id);
|
|
4388
|
+
const existing = item.fields["System.Tags"] ?? "";
|
|
4389
|
+
const lower = new Set(tags.map((t) => t.toLowerCase()));
|
|
4390
|
+
const remaining = existing.split(";").map((t) => t.trim()).filter((t) => t && !lower.has(t.toLowerCase()));
|
|
4391
|
+
const patch = [{ op: "add", path: "/fields/System.Tags", value: remaining.join("; ") }];
|
|
4392
|
+
return this.updateWorkItem(collection, id, patch);
|
|
4393
|
+
}
|
|
4087
4394
|
async listWorkItemTypes(collection, project) {
|
|
4088
4395
|
const result = await this.http.ado(
|
|
4089
4396
|
`/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/wit/workitemtypes?api-version=${API6}`
|
|
@@ -4109,6 +4416,24 @@ var AdoWorkClient = class {
|
|
|
4109
4416
|
);
|
|
4110
4417
|
return result.value ?? [];
|
|
4111
4418
|
}
|
|
4419
|
+
async listAttachments(collection, workItemId) {
|
|
4420
|
+
const item = await this.getWorkItem(collection, workItemId);
|
|
4421
|
+
return (item.relations ?? []).filter((r) => r.rel === "AttachedFile").map((r) => {
|
|
4422
|
+
const attrs = r.attributes ?? {};
|
|
4423
|
+
return {
|
|
4424
|
+
id: new URL(r.url).pathname.split("/").pop() ?? "",
|
|
4425
|
+
name: String(attrs["name"] ?? ""),
|
|
4426
|
+
url: r.url,
|
|
4427
|
+
comment: attrs["comment"] !== void 0 ? String(attrs["comment"]) : void 0,
|
|
4428
|
+
resourceSize: typeof attrs["resourceSize"] === "number" ? attrs["resourceSize"] : void 0,
|
|
4429
|
+
resourceCreatedDate: attrs["resourceCreatedDate"] !== void 0 ? String(attrs["resourceCreatedDate"]) : void 0,
|
|
4430
|
+
resourceModifiedDate: attrs["resourceModifiedDate"] !== void 0 ? String(attrs["resourceModifiedDate"]) : void 0
|
|
4431
|
+
};
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
async downloadAttachment(absoluteUrl) {
|
|
4435
|
+
return this.http.adoBuffer(absoluteUrl);
|
|
4436
|
+
}
|
|
4112
4437
|
};
|
|
4113
4438
|
|
|
4114
4439
|
// src/services/ado/discovery.ts
|
|
@@ -4282,8 +4607,8 @@ function registerConfigCommands(program2) {
|
|
|
4282
4607
|
if (cfg.ado.baseUrl) {
|
|
4283
4608
|
try {
|
|
4284
4609
|
const collection = cfg.defaults.ado?.collection;
|
|
4285
|
-
const
|
|
4286
|
-
await http.ado(
|
|
4610
|
+
const path8 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
|
|
4611
|
+
await http.ado(path8);
|
|
4287
4612
|
results.ado = { ok: true, message: "connected" };
|
|
4288
4613
|
} catch (err) {
|
|
4289
4614
|
results.ado = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
@@ -4455,8 +4780,8 @@ function registerConfigCommands(program2) {
|
|
|
4455
4780
|
} else {
|
|
4456
4781
|
try {
|
|
4457
4782
|
const collection = cfg.defaults.ado?.collection;
|
|
4458
|
-
const
|
|
4459
|
-
await http.ado(
|
|
4783
|
+
const path8 = collection ? `/${encodeURIComponent(collection)}/_apis/connectionData?api-version=7.1-preview.1` : "/_apis/projectCollections?api-version=7.1";
|
|
4784
|
+
await http.ado(path8, { timeoutMs: 1e4 });
|
|
4460
4785
|
results.ado = { status: "valid", message: "ok" };
|
|
4461
4786
|
} catch (err) {
|
|
4462
4787
|
results.ado = categorize2(err);
|
|
@@ -5452,15 +5777,22 @@ var AdoBuildClient = class {
|
|
|
5452
5777
|
);
|
|
5453
5778
|
}
|
|
5454
5779
|
async listBuilds(collection, project, opts = {}) {
|
|
5780
|
+
const baseParams = {
|
|
5781
|
+
"api-version": API8,
|
|
5782
|
+
...opts.definitionIds?.length ? { definitions: opts.definitionIds.join(",") } : {},
|
|
5783
|
+
...opts.branchName ? { branchName: opts.branchName } : {},
|
|
5784
|
+
...opts.statusFilter ? { statusFilter: opts.statusFilter } : {},
|
|
5785
|
+
...opts.top ? { "$top": opts.top } : {}
|
|
5786
|
+
};
|
|
5787
|
+
if (opts.top) {
|
|
5788
|
+
const { data } = await this.http.adoRaw(
|
|
5789
|
+
`/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds`,
|
|
5790
|
+
{ params: baseParams }
|
|
5791
|
+
);
|
|
5792
|
+
return (data.value ?? []).slice(0, opts.top);
|
|
5793
|
+
}
|
|
5455
5794
|
return this.http.adoPaginate(async (token) => {
|
|
5456
|
-
const params = {
|
|
5457
|
-
"api-version": API8,
|
|
5458
|
-
...opts.definitionIds?.length ? { definitionIds: opts.definitionIds.join(",") } : {},
|
|
5459
|
-
...opts.branchName ? { branchName: opts.branchName } : {},
|
|
5460
|
-
...opts.statusFilter ? { statusFilter: opts.statusFilter } : {},
|
|
5461
|
-
...opts.top ? { "$top": opts.top } : {},
|
|
5462
|
-
...token ? { continuationToken: token } : {}
|
|
5463
|
-
};
|
|
5795
|
+
const params = { ...baseParams, ...token ? { continuationToken: token } : {} };
|
|
5464
5796
|
return this.http.adoRaw(
|
|
5465
5797
|
`/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds`,
|
|
5466
5798
|
{ params }
|
|
@@ -5611,6 +5943,8 @@ function registerAdoProjectCommands(ado) {
|
|
|
5611
5943
|
}
|
|
5612
5944
|
|
|
5613
5945
|
// src/services/ado/commands/work.ts
|
|
5946
|
+
import fs3 from "fs";
|
|
5947
|
+
import path6 from "path";
|
|
5614
5948
|
function registerAdoWorkCommands(ado) {
|
|
5615
5949
|
const work = ado.command("work").description("Azure DevOps work item operations");
|
|
5616
5950
|
work.command("get").description("Get a work item by ID").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
|
|
@@ -5751,6 +6085,55 @@ function registerAdoWorkCommands(ado) {
|
|
|
5751
6085
|
fail(err, "ado", "work-add-comment", start);
|
|
5752
6086
|
}
|
|
5753
6087
|
});
|
|
6088
|
+
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) => {
|
|
6089
|
+
const start = Date.now();
|
|
6090
|
+
try {
|
|
6091
|
+
const { collection, workClient } = getAdoContext(ado);
|
|
6092
|
+
const tags = opts.tags.split(/[;,]/).map((t) => t.trim()).filter(Boolean);
|
|
6093
|
+
const data = await workClient.addTags(collection, parseInt(opts.id, 10), tags);
|
|
6094
|
+
success(data, "ado", "work-add-tag", start);
|
|
6095
|
+
} catch (err) {
|
|
6096
|
+
fail(err, "ado", "work-add-tag", start);
|
|
6097
|
+
}
|
|
6098
|
+
});
|
|
6099
|
+
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) => {
|
|
6100
|
+
const start = Date.now();
|
|
6101
|
+
try {
|
|
6102
|
+
const { collection, workClient } = getAdoContext(ado);
|
|
6103
|
+
const tags = opts.tags.split(/[;,]/).map((t) => t.trim()).filter(Boolean);
|
|
6104
|
+
const data = await workClient.removeTags(collection, parseInt(opts.id, 10), tags);
|
|
6105
|
+
success(data, "ado", "work-remove-tag", start);
|
|
6106
|
+
} catch (err) {
|
|
6107
|
+
fail(err, "ado", "work-remove-tag", start);
|
|
6108
|
+
}
|
|
6109
|
+
});
|
|
6110
|
+
work.command("list-attachments").description("List attachments on a work item").requiredOption("--id <n>", "Work item ID").action(async (opts) => {
|
|
6111
|
+
const start = Date.now();
|
|
6112
|
+
try {
|
|
6113
|
+
const { collection, workClient } = getAdoContext(ado);
|
|
6114
|
+
const data = await workClient.listAttachments(collection, parseInt(opts.id, 10));
|
|
6115
|
+
success(data, "ado", "work-list-attachments", start);
|
|
6116
|
+
} catch (err) {
|
|
6117
|
+
fail(err, "ado", "work-list-attachments", start);
|
|
6118
|
+
}
|
|
6119
|
+
});
|
|
6120
|
+
work.command("download-attachment").description("Download a work item attachment to .pncli/ (or --dir)").requiredOption("--id <n>", "Work item ID").requiredOption("--attachment-id <guid>", "Attachment ID from work list-attachments output").option("--dir <path>", "Output directory (default: .pncli relative to cwd)").action(async (opts) => {
|
|
6121
|
+
const start = Date.now();
|
|
6122
|
+
try {
|
|
6123
|
+
const { collection, workClient } = getAdoContext(ado);
|
|
6124
|
+
const attachments = await workClient.listAttachments(collection, parseInt(opts.id, 10));
|
|
6125
|
+
const attachment = attachments.find((a) => a.id === opts.attachmentId);
|
|
6126
|
+
if (!attachment) throw new PncliError(`Attachment not found: ${opts.attachmentId}`, 1);
|
|
6127
|
+
const outDir = opts.dir ?? path6.join(process.cwd(), ".pncli");
|
|
6128
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
6129
|
+
const outPath = path6.join(outDir, path6.basename(attachment.name));
|
|
6130
|
+
const buffer = await workClient.downloadAttachment(attachment.url);
|
|
6131
|
+
fs3.writeFileSync(outPath, buffer);
|
|
6132
|
+
success({ saved: outPath, name: attachment.name, size: buffer.length }, "ado", "work-download-attachment", start);
|
|
6133
|
+
} catch (err) {
|
|
6134
|
+
fail(err, "ado", "work-download-attachment", start);
|
|
6135
|
+
}
|
|
6136
|
+
});
|
|
5754
6137
|
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) => {
|
|
5755
6138
|
const start = Date.now();
|
|
5756
6139
|
try {
|
|
@@ -5759,10 +6142,10 @@ function registerAdoWorkCommands(ado) {
|
|
|
5759
6142
|
const types = await discoverTypes(workClient, collection, project);
|
|
5760
6143
|
if (opts.save) {
|
|
5761
6144
|
const globalConfigPath = getGlobalConfigPath(globalOpts.config);
|
|
5762
|
-
const
|
|
5763
|
-
const existing = JSON.parse(
|
|
6145
|
+
const fs5 = await import("fs");
|
|
6146
|
+
const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
5764
6147
|
existing.ado = { ...existing.ado ?? {}, discoveredTypes: types };
|
|
5765
|
-
|
|
6148
|
+
fs5.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
5766
6149
|
warn(`Saved ${types.length} types to ${globalConfigPath}`);
|
|
5767
6150
|
}
|
|
5768
6151
|
success(types, "ado", "work-types", start);
|
|
@@ -5791,11 +6174,11 @@ function registerAdoWorkCommands(ado) {
|
|
|
5791
6174
|
) : allFields;
|
|
5792
6175
|
if (opts.save) {
|
|
5793
6176
|
const globalConfigPath = getGlobalConfigPath(globalOpts.config);
|
|
5794
|
-
const
|
|
5795
|
-
const existing = JSON.parse(
|
|
6177
|
+
const fs5 = await import("fs");
|
|
6178
|
+
const existing = JSON.parse(fs5.readFileSync(globalConfigPath, "utf8") || "{}");
|
|
5796
6179
|
const aliases = buildDefaultAliases(allFields);
|
|
5797
6180
|
existing.ado = { ...existing.ado ?? {}, discoveredFields: allFields, fieldAliases: aliases };
|
|
5798
|
-
|
|
6181
|
+
fs5.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
5799
6182
|
warn(`Saved ${allFields.length} fields and ${Object.keys(aliases).length} aliases to ${globalConfigPath}`);
|
|
5800
6183
|
}
|
|
5801
6184
|
success(fields, "ado", "work-fields", start);
|
|
@@ -6076,7 +6459,7 @@ function registerAdoRepoCommands(ado) {
|
|
|
6076
6459
|
const config = lc({ configPath: globalOpts.config });
|
|
6077
6460
|
const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
|
|
6078
6461
|
const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
|
|
6079
|
-
const { createHttpClient: createHttpClient2 } = await import("./http-
|
|
6462
|
+
const { createHttpClient: createHttpClient2 } = await import("./http-BNCZJQTD.js");
|
|
6080
6463
|
const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
|
|
6081
6464
|
const coreClient = new AdoCoreClient2(http);
|
|
6082
6465
|
const me = await coreClient.getConnectionData(collection);
|
|
@@ -6169,12 +6552,32 @@ function registerAdoPipelineCommands(ado) {
|
|
|
6169
6552
|
fail(err, "ado", "pipeline-run", start);
|
|
6170
6553
|
}
|
|
6171
6554
|
});
|
|
6172
|
-
pipeline.command("list-runs").description("List pipeline runs").option("--definition <id>", "Filter by definition ID").option("--branch <ref>", "Filter by branch name").option("--status <filter>", "Filter by status (inProgress|completed|cancelling|...)").option("--top <n>", "Maximum results", "50").action(async (opts) => {
|
|
6555
|
+
pipeline.command("list-runs").description("List pipeline runs").option("--definition <id>", "Filter by definition ID").option("--name <name>", "Filter by pipeline name (resolved to definition ID; use pipeline list to see names)").option("--branch <ref>", "Filter by branch name").option("--status <filter>", "Filter by status (inProgress|completed|cancelling|...)").option("--top <n>", "Maximum results", "50").action(async (opts) => {
|
|
6173
6556
|
const start = Date.now();
|
|
6174
6557
|
try {
|
|
6175
6558
|
const { collection, project, buildClient } = getAdoContext(ado);
|
|
6559
|
+
let definitionId;
|
|
6560
|
+
if (opts.definition && opts.name) {
|
|
6561
|
+
throw new PncliError("Cannot specify both --definition and --name. Use one or the other.", 1);
|
|
6562
|
+
}
|
|
6563
|
+
if (opts.definition) {
|
|
6564
|
+
definitionId = parseInt(opts.definition, 10);
|
|
6565
|
+
if (isNaN(definitionId)) {
|
|
6566
|
+
throw new PncliError(`--definition must be a numeric ID. Got "${opts.definition}". Use --name to filter by pipeline name instead.`, 1);
|
|
6567
|
+
}
|
|
6568
|
+
} else if (opts.name) {
|
|
6569
|
+
const defs = await buildClient.listDefinitions(collection, project);
|
|
6570
|
+
const match = defs.find((d) => d.name.toLowerCase() === opts.name.toLowerCase());
|
|
6571
|
+
if (!match) {
|
|
6572
|
+
throw new PncliError(
|
|
6573
|
+
`Pipeline "${opts.name}" not found. Run 'pncli ado pipeline list' to see available pipelines.`,
|
|
6574
|
+
1
|
|
6575
|
+
);
|
|
6576
|
+
}
|
|
6577
|
+
definitionId = match.id;
|
|
6578
|
+
}
|
|
6176
6579
|
const data = await buildClient.listBuilds(collection, project, {
|
|
6177
|
-
definitionIds:
|
|
6580
|
+
definitionIds: definitionId !== void 0 ? [definitionId] : void 0,
|
|
6178
6581
|
branchName: opts.branch,
|
|
6179
6582
|
statusFilter: opts.status,
|
|
6180
6583
|
top: parseInt(opts.top, 10)
|
|
@@ -6204,11 +6607,13 @@ function registerAdoPipelineCommands(ado) {
|
|
|
6204
6607
|
fail(err, "ado", "pipeline-cancel-run", start);
|
|
6205
6608
|
}
|
|
6206
6609
|
});
|
|
6207
|
-
pipeline.command("logs").description("List or retrieve build logs (consider --output-file for large logs)").
|
|
6610
|
+
pipeline.command("logs").description("List or retrieve build logs (consider --output-file for large logs)").option("--build-id <n>", "Build (run) ID \u2014 from pipeline list-runs or get-run").option("--id <n>", "Build (run) ID \u2014 alias for --build-id (kept for backwards compatibility)").option("--log-id <n>", "Specific log ID (omit to list all logs)").action(async (opts) => {
|
|
6208
6611
|
const start = Date.now();
|
|
6209
6612
|
try {
|
|
6613
|
+
const rawId = opts.buildId ?? opts.id;
|
|
6614
|
+
if (!rawId) throw new PncliError("--build-id is required (build run ID from pipeline list-runs or get-run)", 1);
|
|
6210
6615
|
const { collection, project, buildClient } = getAdoContext(ado);
|
|
6211
|
-
const buildId = parseInt(
|
|
6616
|
+
const buildId = parseInt(rawId, 10);
|
|
6212
6617
|
if (opts.logId) {
|
|
6213
6618
|
const data = await buildClient.getLog(collection, project, buildId, parseInt(opts.logId, 10));
|
|
6214
6619
|
success({ log: data }, "ado", "pipeline-logs", start);
|
|
@@ -6260,9 +6665,10 @@ var JenkinsClient = class {
|
|
|
6260
6665
|
this.http = http;
|
|
6261
6666
|
}
|
|
6262
6667
|
http;
|
|
6263
|
-
async listJobs() {
|
|
6668
|
+
async listJobs(folder) {
|
|
6669
|
+
const endpoint = folder ? `/job/${jobPath(folder)}/api/json` : "/api/json";
|
|
6264
6670
|
const data = await this.http.jenkins(
|
|
6265
|
-
|
|
6671
|
+
endpoint,
|
|
6266
6672
|
{ params: { tree: "jobs[name,url,color,description,buildable,lastBuild[number,url,result]]" } }
|
|
6267
6673
|
);
|
|
6268
6674
|
return data.jobs;
|
|
@@ -6342,17 +6748,17 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
|
|
|
6342
6748
|
function registerJenkinsCommands(program2) {
|
|
6343
6749
|
const jenkins = program2.command("jenkins").description("Jenkins Data Center operations");
|
|
6344
6750
|
const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
|
|
6345
|
-
pipeline.command("list").description("List all jobs on the Jenkins instance").action(async () => {
|
|
6751
|
+
pipeline.command("list").description("List all jobs on the Jenkins instance (or within a folder)").option("--folder <name>", "Folder name to enumerate (supports nested folders: parentFolder/childFolder)").action(async (opts) => {
|
|
6346
6752
|
const start = Date.now();
|
|
6347
6753
|
try {
|
|
6348
6754
|
const client = getClient6(program2);
|
|
6349
|
-
const data = await client.listJobs();
|
|
6755
|
+
const data = await client.listJobs(opts.folder);
|
|
6350
6756
|
success(data, "jenkins", "pipeline-list", start);
|
|
6351
6757
|
} catch (err) {
|
|
6352
6758
|
fail(err, "jenkins", "pipeline-list", start);
|
|
6353
6759
|
}
|
|
6354
6760
|
});
|
|
6355
|
-
pipeline.command("get").description("Get details for a specific job").requiredOption("--name <job>", "Job name").action(async (opts) => {
|
|
6761
|
+
pipeline.command("get").description("Get details for a specific job").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").action(async (opts) => {
|
|
6356
6762
|
const start = Date.now();
|
|
6357
6763
|
try {
|
|
6358
6764
|
const client = getClient6(program2);
|
|
@@ -6362,7 +6768,7 @@ function registerJenkinsCommands(program2) {
|
|
|
6362
6768
|
fail(err, "jenkins", "pipeline-get", start);
|
|
6363
6769
|
}
|
|
6364
6770
|
});
|
|
6365
|
-
pipeline.command("run").description("Trigger a build").requiredOption("--name <job>", "Job name").option("--parameter <k=v>", "Build parameter (repeatable)", (v, acc) => {
|
|
6771
|
+
pipeline.command("run").description("Trigger a build").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").option("--parameter <k=v>", "Build parameter (repeatable)", (v, acc) => {
|
|
6366
6772
|
acc.push(v);
|
|
6367
6773
|
return acc;
|
|
6368
6774
|
}, []).option("--wait", "Wait for the build to complete before returning").option("--timeout <s>", "Max wait time in seconds per phase (queue-wait and build-wait each get this budget; default 600)", "600").option("--poll <s>", "Poll interval in seconds (default 10)", "10").action(async (opts) => {
|
|
@@ -6398,7 +6804,7 @@ function registerJenkinsCommands(program2) {
|
|
|
6398
6804
|
fail(err, "jenkins", "pipeline-run", start);
|
|
6399
6805
|
}
|
|
6400
6806
|
});
|
|
6401
|
-
pipeline.command("list-runs").description("List recent builds for a job").requiredOption("--name <job>", "Job name").option("--top <n>", "Maximum number of builds to return (default 25)", "25").action(async (opts) => {
|
|
6807
|
+
pipeline.command("list-runs").description("List recent builds for a job").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").option("--top <n>", "Maximum number of builds to return (default 25)", "25").action(async (opts) => {
|
|
6402
6808
|
const start = Date.now();
|
|
6403
6809
|
try {
|
|
6404
6810
|
const client = getClient6(program2);
|
|
@@ -6410,7 +6816,7 @@ function registerJenkinsCommands(program2) {
|
|
|
6410
6816
|
fail(err, "jenkins", "pipeline-list-runs", start);
|
|
6411
6817
|
}
|
|
6412
6818
|
});
|
|
6413
|
-
pipeline.command("get-run").description("Get details for a specific build").requiredOption("--name <job>", "Job name").requiredOption("--number <n>", "Build number").action(async (opts) => {
|
|
6819
|
+
pipeline.command("get-run").description("Get details for a specific build").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").requiredOption("--number <n>", "Build number").action(async (opts) => {
|
|
6414
6820
|
const start = Date.now();
|
|
6415
6821
|
try {
|
|
6416
6822
|
const client = getClient6(program2);
|
|
@@ -6422,7 +6828,7 @@ function registerJenkinsCommands(program2) {
|
|
|
6422
6828
|
fail(err, "jenkins", "pipeline-get-run", start);
|
|
6423
6829
|
}
|
|
6424
6830
|
});
|
|
6425
|
-
pipeline.command("logs").description("Fetch console log for a build (consider --output-file for large logs)").requiredOption("--name <job>", "Job name").requiredOption("--number <n>", "Build number").action(async (opts) => {
|
|
6831
|
+
pipeline.command("logs").description("Fetch console log for a build (consider --output-file for large logs)").requiredOption("--name <job>", "Job name (use folder/job for folder-scoped jobs)").requiredOption("--number <n>", "Build number").action(async (opts) => {
|
|
6426
6832
|
const start = Date.now();
|
|
6427
6833
|
try {
|
|
6428
6834
|
const client = getClient6(program2);
|
|
@@ -6454,18 +6860,18 @@ var ArtifactoryClient = class {
|
|
|
6454
6860
|
}
|
|
6455
6861
|
});
|
|
6456
6862
|
}
|
|
6457
|
-
async getStorageInfo(repo,
|
|
6458
|
-
const cleanPath =
|
|
6863
|
+
async getStorageInfo(repo, path8) {
|
|
6864
|
+
const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
|
|
6459
6865
|
return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`);
|
|
6460
6866
|
}
|
|
6461
|
-
async getProperties(repo,
|
|
6462
|
-
const cleanPath =
|
|
6867
|
+
async getProperties(repo, path8) {
|
|
6868
|
+
const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
|
|
6463
6869
|
return this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
|
|
6464
6870
|
params: { properties: "" }
|
|
6465
6871
|
});
|
|
6466
6872
|
}
|
|
6467
|
-
async setProperties(repo,
|
|
6468
|
-
const cleanPath =
|
|
6873
|
+
async setProperties(repo, path8, properties, opts = {}) {
|
|
6874
|
+
const cleanPath = path8.startsWith("/") ? path8.slice(1) : path8;
|
|
6469
6875
|
const propString = Object.entries(properties).map(([k, v]) => `${k}=${v}`).join(";");
|
|
6470
6876
|
await this.http.artifactory(`${API9}/storage/${repo}/${cleanPath}`, {
|
|
6471
6877
|
method: "PUT",
|
|
@@ -6493,8 +6899,18 @@ var ArtifactoryClient = class {
|
|
|
6493
6899
|
headers: { "Content-Type": "text/plain" }
|
|
6494
6900
|
});
|
|
6495
6901
|
}
|
|
6496
|
-
async listBuilds() {
|
|
6497
|
-
|
|
6902
|
+
async listBuilds(opts = {}) {
|
|
6903
|
+
const raw = await this.http.artifactory(`${API9}/build`, { timeoutMs: opts.timeoutMs });
|
|
6904
|
+
const builds = (raw.builds ?? []).map((b) => ({
|
|
6905
|
+
name: b.uri.replace(/^\/|\/$/g, ""),
|
|
6906
|
+
uri: b.uri,
|
|
6907
|
+
lastStarted: b.lastStarted
|
|
6908
|
+
}));
|
|
6909
|
+
const result = { builds, uri: raw.uri };
|
|
6910
|
+
if (builds.length === 0) {
|
|
6911
|
+
result.hint = 'No builds are registered in Artifactory Build Integration. Build names may differ from ADO pipeline or uDeploy component names. Use "artifactory repos" to list repositories, then "artifactory search --repo <repo>" to locate artifacts directly.';
|
|
6912
|
+
}
|
|
6913
|
+
return result;
|
|
6498
6914
|
}
|
|
6499
6915
|
async listBuildRuns(buildName) {
|
|
6500
6916
|
return this.http.artifactory(`${API9}/build/${encodeURIComponent(buildName)}`);
|
|
@@ -6537,27 +6953,27 @@ function registerArtifactoryCommands(program2) {
|
|
|
6537
6953
|
fail(err, "artifactory", "repos", start);
|
|
6538
6954
|
}
|
|
6539
6955
|
});
|
|
6540
|
-
art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo,
|
|
6956
|
+
art.command("artifact-info <repo> <path>").description("Get metadata for an artifact or folder (checksums, size, timestamps)").action(async (repo, path8) => {
|
|
6541
6957
|
const start = Date.now();
|
|
6542
6958
|
try {
|
|
6543
6959
|
const client = getClient7(program2);
|
|
6544
|
-
const data = await client.getStorageInfo(repo,
|
|
6960
|
+
const data = await client.getStorageInfo(repo, path8);
|
|
6545
6961
|
success(data, "artifactory", "artifact-info", start);
|
|
6546
6962
|
} catch (err) {
|
|
6547
6963
|
fail(err, "artifactory", "artifact-info", start);
|
|
6548
6964
|
}
|
|
6549
6965
|
});
|
|
6550
|
-
art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo,
|
|
6966
|
+
art.command("properties-get <repo> <path>").description("Read custom properties on an artifact").action(async (repo, path8) => {
|
|
6551
6967
|
const start = Date.now();
|
|
6552
6968
|
try {
|
|
6553
6969
|
const client = getClient7(program2);
|
|
6554
|
-
const data = await client.getProperties(repo,
|
|
6970
|
+
const data = await client.getProperties(repo, path8);
|
|
6555
6971
|
success(data, "artifactory", "properties-get", start);
|
|
6556
6972
|
} catch (err) {
|
|
6557
6973
|
fail(err, "artifactory", "properties-get", start);
|
|
6558
6974
|
}
|
|
6559
6975
|
});
|
|
6560
|
-
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,
|
|
6976
|
+
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) => {
|
|
6561
6977
|
const start = Date.now();
|
|
6562
6978
|
try {
|
|
6563
6979
|
if (!keyvals.length) throw new PncliError("Provide at least one key=value pair");
|
|
@@ -6568,8 +6984,8 @@ function registerArtifactoryCommands(program2) {
|
|
|
6568
6984
|
properties[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
6569
6985
|
}
|
|
6570
6986
|
const client = getClient7(program2);
|
|
6571
|
-
await client.setProperties(repo,
|
|
6572
|
-
success({ repo, path:
|
|
6987
|
+
await client.setProperties(repo, path8, properties, { recursive: opts.recursive });
|
|
6988
|
+
success({ repo, path: path8, properties }, "artifactory", "properties-set", start);
|
|
6573
6989
|
} catch (err) {
|
|
6574
6990
|
fail(err, "artifactory", "properties-set", start);
|
|
6575
6991
|
}
|
|
@@ -6595,11 +7011,14 @@ function registerArtifactoryCommands(program2) {
|
|
|
6595
7011
|
fail(err, "artifactory", "search", start);
|
|
6596
7012
|
}
|
|
6597
7013
|
});
|
|
6598
|
-
art.command("builds").description(
|
|
7014
|
+
art.command("builds").description('List builds registered in Artifactory Build Integration (names may differ from ADO pipeline or uDeploy component names; use "search --repo" when empty)').option("--timeout <s>", "Request timeout in seconds (default 60; increase for large Artifactory instances)", "60").action(async (opts) => {
|
|
6599
7015
|
const start = Date.now();
|
|
6600
7016
|
try {
|
|
6601
7017
|
const client = getClient7(program2);
|
|
6602
|
-
const
|
|
7018
|
+
const timeoutSecs = parseInt(opts.timeout, 10);
|
|
7019
|
+
if (isNaN(timeoutSecs) || timeoutSecs <= 0) throw new PncliError("--timeout must be a positive integer (seconds)", 1);
|
|
7020
|
+
const timeoutMs = timeoutSecs * 1e3;
|
|
7021
|
+
const data = await client.listBuilds({ timeoutMs });
|
|
6603
7022
|
success(data, "artifactory", "builds", start);
|
|
6604
7023
|
} catch (err) {
|
|
6605
7024
|
fail(err, "artifactory", "builds", start);
|
|
@@ -7170,8 +7589,8 @@ function registerSonatypeIqCommands(program2) {
|
|
|
7170
7589
|
const startTime = Date.now();
|
|
7171
7590
|
try {
|
|
7172
7591
|
const http = getHttpClient(program2);
|
|
7173
|
-
const
|
|
7174
|
-
const data = await http.sonatypeiq(
|
|
7592
|
+
const path8 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
|
|
7593
|
+
const data = await http.sonatypeiq(path8);
|
|
7175
7594
|
success(data, "sonatypeiq", "policies list", startTime);
|
|
7176
7595
|
} catch (err) {
|
|
7177
7596
|
fail(err, "sonatypeiq", "policies list", startTime);
|
|
@@ -7180,8 +7599,8 @@ function registerSonatypeIqCommands(program2) {
|
|
|
7180
7599
|
}
|
|
7181
7600
|
|
|
7182
7601
|
// src/services/skills/commands.ts
|
|
7183
|
-
import
|
|
7184
|
-
import
|
|
7602
|
+
import fs4 from "fs";
|
|
7603
|
+
import path7 from "path";
|
|
7185
7604
|
import os from "os";
|
|
7186
7605
|
import { fileURLToPath } from "url";
|
|
7187
7606
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
@@ -7193,18 +7612,30 @@ function injectTokenIntoUrl(url, token) {
|
|
|
7193
7612
|
} catch {
|
|
7194
7613
|
throw new Error(`--token requires an HTTPS clone URL; got: ${url}`);
|
|
7195
7614
|
}
|
|
7196
|
-
parsed.username = "x-token-auth";
|
|
7615
|
+
parsed.username = parsed.hostname === "github.com" ? "x-access-token" : "x-token-auth";
|
|
7197
7616
|
parsed.password = token;
|
|
7198
7617
|
return parsed.toString();
|
|
7199
7618
|
}
|
|
7619
|
+
function repoNameFromUrl(url) {
|
|
7620
|
+
try {
|
|
7621
|
+
const parsed = new URL(url);
|
|
7622
|
+
const segment = parsed.pathname.split("/").filter(Boolean).pop() ?? "marketplace";
|
|
7623
|
+
return segment.replace(/\.git$/i, "") || "marketplace";
|
|
7624
|
+
} catch {
|
|
7625
|
+
return "marketplace";
|
|
7626
|
+
}
|
|
7627
|
+
}
|
|
7628
|
+
function defaultMarketplacePath(url) {
|
|
7629
|
+
return path7.join(os.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
|
|
7630
|
+
}
|
|
7200
7631
|
var AGENT_PATHS = {
|
|
7201
|
-
"github-copilot": { project: ".agents/skills", user:
|
|
7202
|
-
"claude-code": { project: ".claude/skills", user:
|
|
7632
|
+
"github-copilot": { project: ".agents/skills", user: path7.join(os.homedir(), ".copilot/skills") },
|
|
7633
|
+
"claude-code": { project: ".claude/skills", user: path7.join(os.homedir(), ".claude/skills") }
|
|
7203
7634
|
};
|
|
7204
7635
|
function getBundledSkillsDir() {
|
|
7205
7636
|
try {
|
|
7206
7637
|
const thisFile = fileURLToPath(import.meta.url);
|
|
7207
|
-
return
|
|
7638
|
+
return path7.resolve(path7.dirname(thisFile), "..", "skills");
|
|
7208
7639
|
} catch {
|
|
7209
7640
|
return "";
|
|
7210
7641
|
}
|
|
@@ -7216,22 +7647,22 @@ function registerSkillsCommands(program2) {
|
|
|
7216
7647
|
try {
|
|
7217
7648
|
let targetDir;
|
|
7218
7649
|
if (opts.target) {
|
|
7219
|
-
targetDir =
|
|
7650
|
+
targetDir = path7.resolve(opts.target);
|
|
7220
7651
|
} else {
|
|
7221
7652
|
const agentConfig = AGENT_PATHS[opts.agent];
|
|
7222
7653
|
if (!agentConfig) {
|
|
7223
7654
|
throw new Error(`Unknown agent: "${opts.agent}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
|
|
7224
7655
|
}
|
|
7225
7656
|
const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
|
|
7226
|
-
targetDir =
|
|
7657
|
+
targetDir = path7.resolve(scopePath);
|
|
7227
7658
|
}
|
|
7228
|
-
const resolvedTarget =
|
|
7659
|
+
const resolvedTarget = path7.resolve(targetDir);
|
|
7229
7660
|
const bundledDir = getBundledSkillsDir();
|
|
7230
7661
|
let skillDirs = [];
|
|
7231
7662
|
if (bundledDir !== "") {
|
|
7232
7663
|
try {
|
|
7233
|
-
skillDirs =
|
|
7234
|
-
(name) =>
|
|
7664
|
+
skillDirs = fs4.readdirSync(bundledDir).filter(
|
|
7665
|
+
(name) => fs4.existsSync(path7.join(bundledDir, name, "SKILL.md"))
|
|
7235
7666
|
);
|
|
7236
7667
|
} catch {
|
|
7237
7668
|
}
|
|
@@ -7242,25 +7673,25 @@ function registerSkillsCommands(program2) {
|
|
|
7242
7673
|
const installed = [];
|
|
7243
7674
|
const failed = [];
|
|
7244
7675
|
for (const skillName of skillDirs) {
|
|
7245
|
-
const existingDir =
|
|
7246
|
-
if (!existingDir.startsWith(resolvedTarget +
|
|
7247
|
-
if (
|
|
7248
|
-
|
|
7676
|
+
const existingDir = path7.resolve(targetDir, skillName);
|
|
7677
|
+
if (!existingDir.startsWith(resolvedTarget + path7.sep)) continue;
|
|
7678
|
+
if (fs4.existsSync(existingDir)) {
|
|
7679
|
+
fs4.rmSync(existingDir, { recursive: true, force: true });
|
|
7249
7680
|
}
|
|
7250
7681
|
}
|
|
7251
7682
|
warn(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
|
|
7252
7683
|
for (const skillName of skillDirs) {
|
|
7253
|
-
const skillDir =
|
|
7254
|
-
if (!skillDir.startsWith(resolvedTarget +
|
|
7684
|
+
const skillDir = path7.resolve(targetDir, skillName);
|
|
7685
|
+
if (!skillDir.startsWith(resolvedTarget + path7.sep)) {
|
|
7255
7686
|
failed.push(skillName);
|
|
7256
7687
|
continue;
|
|
7257
7688
|
}
|
|
7258
7689
|
try {
|
|
7259
|
-
const mdFiles =
|
|
7260
|
-
|
|
7690
|
+
const mdFiles = fs4.readdirSync(path7.join(bundledDir, skillName)).filter((f) => f.endsWith(".md"));
|
|
7691
|
+
fs4.mkdirSync(skillDir, { recursive: true });
|
|
7261
7692
|
for (const mdFile of mdFiles) {
|
|
7262
|
-
const content =
|
|
7263
|
-
|
|
7693
|
+
const content = fs4.readFileSync(path7.join(bundledDir, skillName, mdFile), "utf8");
|
|
7694
|
+
fs4.writeFileSync(path7.join(skillDir, mdFile), content, "utf8");
|
|
7264
7695
|
}
|
|
7265
7696
|
installed.push(skillName);
|
|
7266
7697
|
} catch {
|
|
@@ -7289,25 +7720,25 @@ function registerSkillsCommands(program2) {
|
|
|
7289
7720
|
try {
|
|
7290
7721
|
let targetDir;
|
|
7291
7722
|
if (opts.target) {
|
|
7292
|
-
targetDir =
|
|
7723
|
+
targetDir = path7.resolve(opts.target);
|
|
7293
7724
|
} else {
|
|
7294
7725
|
const agentConfig = AGENT_PATHS[opts.agent];
|
|
7295
7726
|
if (!agentConfig) {
|
|
7296
7727
|
throw new Error(`Unknown agent: "${opts.agent}". Use: ${Object.keys(AGENT_PATHS).join(" | ")}`);
|
|
7297
7728
|
}
|
|
7298
7729
|
const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
|
|
7299
|
-
targetDir =
|
|
7730
|
+
targetDir = path7.resolve(scopePath);
|
|
7300
7731
|
}
|
|
7301
|
-
if (!
|
|
7732
|
+
if (!fs4.existsSync(targetDir)) {
|
|
7302
7733
|
success({ skills: [], message: `No skills directory found at ${targetDir}. Run: pncli skills install` }, "skills", "list", start);
|
|
7303
7734
|
return;
|
|
7304
7735
|
}
|
|
7305
|
-
const skillDirs =
|
|
7306
|
-
const skillPath =
|
|
7307
|
-
return
|
|
7736
|
+
const skillDirs = fs4.readdirSync(targetDir).filter((name) => {
|
|
7737
|
+
const skillPath = path7.join(targetDir, name, "SKILL.md");
|
|
7738
|
+
return fs4.existsSync(skillPath);
|
|
7308
7739
|
});
|
|
7309
7740
|
const skillsList = skillDirs.map((name) => {
|
|
7310
|
-
const content =
|
|
7741
|
+
const content = fs4.readFileSync(path7.join(targetDir, name, "SKILL.md"), "utf8");
|
|
7311
7742
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7312
7743
|
const data = {};
|
|
7313
7744
|
const metadata = {};
|
|
@@ -7346,42 +7777,46 @@ function registerSkillsCommands(program2) {
|
|
|
7346
7777
|
}
|
|
7347
7778
|
});
|
|
7348
7779
|
const marketplace = skills.command("marketplace").description("Manage a git-hosted skills marketplace");
|
|
7349
|
-
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("
|
|
7780
|
+
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) => {
|
|
7350
7781
|
const start = Date.now();
|
|
7351
7782
|
try {
|
|
7352
|
-
const resolvedPath =
|
|
7353
|
-
const hasGit =
|
|
7354
|
-
if (
|
|
7783
|
+
const resolvedPath = path7.resolve(localPath ?? defaultMarketplacePath(url));
|
|
7784
|
+
const hasGit = fs4.existsSync(path7.join(resolvedPath, ".git"));
|
|
7785
|
+
if (fs4.existsSync(resolvedPath) && !hasGit && fs4.readdirSync(resolvedPath).length > 0) {
|
|
7355
7786
|
throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
|
|
7356
7787
|
}
|
|
7357
7788
|
if (hasGit) {
|
|
7358
7789
|
warn(`Directory already contains a git repo at ${resolvedPath} \u2014 skipping clone, updating config only.`);
|
|
7359
7790
|
} else {
|
|
7360
|
-
|
|
7791
|
+
const branchLabel = opts.branch ?? "remote default";
|
|
7792
|
+
warn(`Cloning ${url} (branch: ${branchLabel}) \u2192 ${resolvedPath}...`);
|
|
7361
7793
|
const cloneUrl = opts.token ? injectTokenIntoUrl(url, opts.token) : url;
|
|
7794
|
+
const cloneArgs = ["clone"];
|
|
7795
|
+
if (opts.branch) cloneArgs.push("--branch", opts.branch);
|
|
7796
|
+
cloneArgs.push(cloneUrl, resolvedPath);
|
|
7362
7797
|
try {
|
|
7363
|
-
execFileSync3("git",
|
|
7798
|
+
execFileSync3("git", cloneArgs, { stdio: ["inherit", "inherit", "pipe"] });
|
|
7364
7799
|
} catch (e) {
|
|
7365
7800
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7366
|
-
throw new Error(msg.replace(/x-token-auth:[^@]+@/g, "x-token-auth:***@"));
|
|
7801
|
+
throw new Error(msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@"));
|
|
7367
7802
|
}
|
|
7368
7803
|
}
|
|
7369
7804
|
const configPath = getGlobalConfigPath();
|
|
7370
7805
|
const existing = loadJsonFile(configPath) ?? {};
|
|
7371
7806
|
existing.marketplace = { repoUrl: url, localPath: resolvedPath, ...opts.token ? { token: opts.token } : {} };
|
|
7372
7807
|
writeGlobalConfig(existing, configPath);
|
|
7373
|
-
success({ repoUrl: url, localPath: resolvedPath, branch: opts.branch, tokenConfigured: !!opts.token }, "skills", "marketplace-setup", start);
|
|
7808
|
+
success({ repoUrl: url, localPath: resolvedPath, branch: opts.branch ?? null, tokenConfigured: !!opts.token }, "skills", "marketplace-setup", start);
|
|
7374
7809
|
} catch (err) {
|
|
7375
7810
|
fail(err, "skills", "marketplace-setup", start);
|
|
7376
7811
|
}
|
|
7377
7812
|
});
|
|
7378
|
-
marketplace.command("sync").description("Pull latest marketplace content and install a plugin's skills").argument("[plugin]",
|
|
7813
|
+
marketplace.command("sync").description("Pull latest marketplace content and install a plugin's skills").argument("[plugin]", 'Plugin name to install, or "all" to install every plugin (skips interactive selection)').option("--claude", "Install to ~/.claude/skills instead of ~/.agents/skills").option("--force", "Force reinstall even if the marketplace repo has no new changes").action(async (plugin, opts) => {
|
|
7379
7814
|
const start = Date.now();
|
|
7380
7815
|
try {
|
|
7381
7816
|
const configPath = getGlobalConfigPath();
|
|
7382
7817
|
const globalConfig = loadJsonFile(configPath) ?? {};
|
|
7383
7818
|
const marketplacePath = globalConfig.marketplace?.localPath;
|
|
7384
|
-
if (!marketplacePath || !
|
|
7819
|
+
if (!marketplacePath || !fs4.existsSync(marketplacePath)) {
|
|
7385
7820
|
throw new Error("Marketplace not configured. Run: pncli skills marketplace setup <url> <localPath>");
|
|
7386
7821
|
}
|
|
7387
7822
|
warn("Pulling latest marketplace content...");
|
|
@@ -7392,16 +7827,61 @@ function registerSkillsCommands(program2) {
|
|
|
7392
7827
|
gitArgs.push("-c", `remote.origin.url=${injectTokenIntoUrl(repoUrl, token)}`);
|
|
7393
7828
|
}
|
|
7394
7829
|
gitArgs.push("pull");
|
|
7830
|
+
let pullOutput;
|
|
7395
7831
|
try {
|
|
7396
|
-
execFileSync3("git", gitArgs, { stdio: ["inherit", "
|
|
7832
|
+
pullOutput = execFileSync3("git", gitArgs, { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } });
|
|
7397
7833
|
} catch (e) {
|
|
7398
7834
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7399
|
-
throw new Error(msg.replace(/x-token-auth:[^@]+@/g, "x-token-auth:***@"));
|
|
7835
|
+
throw new Error(msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@"));
|
|
7836
|
+
}
|
|
7837
|
+
const marketplaceUpdated = !pullOutput.includes("Already up to date");
|
|
7838
|
+
if (pullOutput.trim() && marketplaceUpdated) {
|
|
7839
|
+
warn(pullOutput.trim());
|
|
7840
|
+
}
|
|
7841
|
+
warn(marketplaceUpdated ? "Marketplace updated." : "Marketplace already up to date \u2014 no changes to sync.");
|
|
7842
|
+
if (!marketplaceUpdated && !plugin && !opts.force) {
|
|
7843
|
+
success({
|
|
7844
|
+
marketplace: marketplacePath,
|
|
7845
|
+
marketplaceUpdated: false,
|
|
7846
|
+
updated: false,
|
|
7847
|
+
skipped: true,
|
|
7848
|
+
message: "No marketplace changes detected \u2014 skipping plugin selection and install."
|
|
7849
|
+
}, "skills", "marketplace-sync", start);
|
|
7850
|
+
return;
|
|
7400
7851
|
}
|
|
7401
7852
|
const pluginChoices = resolvePluginChoices(marketplacePath);
|
|
7402
7853
|
if (pluginChoices.length === 0) {
|
|
7403
7854
|
throw new Error("No plugins found in marketplace. Check the marketplace repository structure.");
|
|
7404
7855
|
}
|
|
7856
|
+
const targetDir = opts.claude ? path7.join(os.homedir(), ".claude", "skills") : path7.join(os.homedir(), ".agents", "skills");
|
|
7857
|
+
if (plugin === "all") {
|
|
7858
|
+
const results = {};
|
|
7859
|
+
let totalInstalled = 0;
|
|
7860
|
+
for (const pluginChoice of pluginChoices) {
|
|
7861
|
+
const skillsSrc2 = resolveSkillsSrc(marketplacePath, pluginChoice.name);
|
|
7862
|
+
if (!fs4.existsSync(skillsSrc2)) {
|
|
7863
|
+
results[pluginChoice.name] = { installed: [], failed: [] };
|
|
7864
|
+
warn(`No skills directory found for plugin "${pluginChoice.name}" \u2014 skipping.`);
|
|
7865
|
+
continue;
|
|
7866
|
+
}
|
|
7867
|
+
const { installed: installed2, failed: failed2 } = copyPluginSkills(skillsSrc2, targetDir);
|
|
7868
|
+
results[pluginChoice.name] = { installed: installed2, failed: failed2 };
|
|
7869
|
+
totalInstalled += installed2.length;
|
|
7870
|
+
for (const skill of installed2) {
|
|
7871
|
+
warn(` ${skill}: ${path7.join(skillsSrc2, skill)} \u2192 ${path7.join(targetDir, skill)}`);
|
|
7872
|
+
}
|
|
7873
|
+
if (failed2.length > 0) {
|
|
7874
|
+
warn(`Skipped ${failed2.length} skill(s) with invalid names in "${pluginChoice.name}": ${failed2.join(", ")}`);
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
success({
|
|
7878
|
+
plugins: results,
|
|
7879
|
+
total: totalInstalled,
|
|
7880
|
+
target: targetDir,
|
|
7881
|
+
marketplaceUpdated
|
|
7882
|
+
}, "skills", "marketplace-sync", start);
|
|
7883
|
+
return;
|
|
7884
|
+
}
|
|
7405
7885
|
let selectedPlugin;
|
|
7406
7886
|
if (plugin) {
|
|
7407
7887
|
if (!pluginChoices.some((p) => p.name === plugin)) {
|
|
@@ -7418,11 +7898,13 @@ function registerSkillsCommands(program2) {
|
|
|
7418
7898
|
});
|
|
7419
7899
|
}
|
|
7420
7900
|
const skillsSrc = resolveSkillsSrc(marketplacePath, selectedPlugin);
|
|
7421
|
-
if (!
|
|
7901
|
+
if (!fs4.existsSync(skillsSrc)) {
|
|
7422
7902
|
throw new Error(`No skills directory found at ${skillsSrc}`);
|
|
7423
7903
|
}
|
|
7424
|
-
const targetDir = opts.claude ? path5.join(os.homedir(), ".claude", "skills") : path5.join(os.homedir(), ".agents", "skills");
|
|
7425
7904
|
const { installed, failed } = copyPluginSkills(skillsSrc, targetDir);
|
|
7905
|
+
for (const skill of installed) {
|
|
7906
|
+
warn(` ${skill}: ${path7.join(skillsSrc, skill)} \u2192 ${path7.join(targetDir, skill)}`);
|
|
7907
|
+
}
|
|
7426
7908
|
if (failed.length > 0) {
|
|
7427
7909
|
warn(`Skipped ${failed.length} skill(s) with invalid names: ${failed.join(", ")}`);
|
|
7428
7910
|
}
|
|
@@ -7431,7 +7913,8 @@ function registerSkillsCommands(program2) {
|
|
|
7431
7913
|
installed,
|
|
7432
7914
|
failed,
|
|
7433
7915
|
total: installed.length,
|
|
7434
|
-
target: targetDir
|
|
7916
|
+
target: targetDir,
|
|
7917
|
+
marketplaceUpdated
|
|
7435
7918
|
}, "skills", "marketplace-sync", start);
|
|
7436
7919
|
} catch (err) {
|
|
7437
7920
|
fail(err, "skills", "marketplace-sync", start);
|
|
@@ -7439,46 +7922,46 @@ function registerSkillsCommands(program2) {
|
|
|
7439
7922
|
});
|
|
7440
7923
|
}
|
|
7441
7924
|
function resolvePluginChoices(marketplacePath) {
|
|
7442
|
-
const marketplaceJsonPath =
|
|
7443
|
-
if (
|
|
7925
|
+
const marketplaceJsonPath = path7.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
7926
|
+
if (fs4.existsSync(marketplaceJsonPath)) {
|
|
7444
7927
|
try {
|
|
7445
|
-
const meta = JSON.parse(
|
|
7928
|
+
const meta = JSON.parse(fs4.readFileSync(marketplaceJsonPath, "utf8"));
|
|
7446
7929
|
if (Array.isArray(meta.plugins) && meta.plugins.length > 0) {
|
|
7447
7930
|
return meta.plugins.map((p) => ({ name: p.name, description: p.description ?? "" }));
|
|
7448
7931
|
}
|
|
7449
7932
|
} catch {
|
|
7450
7933
|
}
|
|
7451
7934
|
}
|
|
7452
|
-
const pluginsDir =
|
|
7453
|
-
if (
|
|
7454
|
-
return
|
|
7935
|
+
const pluginsDir = path7.join(marketplacePath, "plugins");
|
|
7936
|
+
if (fs4.existsSync(pluginsDir)) {
|
|
7937
|
+
return fs4.readdirSync(pluginsDir).filter((name) => fs4.statSync(path7.join(pluginsDir, name)).isDirectory()).map((name) => ({ name, description: "" }));
|
|
7455
7938
|
}
|
|
7456
7939
|
return [];
|
|
7457
7940
|
}
|
|
7458
7941
|
function resolveSkillsSrc(marketplacePath, selectedPlugin) {
|
|
7459
|
-
const pluginsBase =
|
|
7460
|
-
const skillsSrc =
|
|
7461
|
-
if (!skillsSrc.startsWith(pluginsBase +
|
|
7942
|
+
const pluginsBase = path7.resolve(marketplacePath, "plugins");
|
|
7943
|
+
const skillsSrc = path7.resolve(pluginsBase, selectedPlugin, "skills");
|
|
7944
|
+
if (!skillsSrc.startsWith(pluginsBase + path7.sep)) {
|
|
7462
7945
|
throw new Error(`Invalid plugin name: "${selectedPlugin}"`);
|
|
7463
7946
|
}
|
|
7464
7947
|
return skillsSrc;
|
|
7465
7948
|
}
|
|
7466
7949
|
function copyPluginSkills(skillsSrc, targetDir) {
|
|
7467
|
-
|
|
7468
|
-
const resolvedTarget =
|
|
7469
|
-
const skillNames =
|
|
7470
|
-
(name) =>
|
|
7950
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
7951
|
+
const resolvedTarget = path7.resolve(targetDir);
|
|
7952
|
+
const skillNames = fs4.readdirSync(skillsSrc).filter(
|
|
7953
|
+
(name) => fs4.statSync(path7.join(skillsSrc, name)).isDirectory()
|
|
7471
7954
|
);
|
|
7472
7955
|
const installed = [];
|
|
7473
7956
|
const failed = [];
|
|
7474
7957
|
for (const skillName of skillNames) {
|
|
7475
|
-
const dest =
|
|
7476
|
-
if (!dest.startsWith(resolvedTarget +
|
|
7958
|
+
const dest = path7.resolve(targetDir, skillName);
|
|
7959
|
+
if (!dest.startsWith(resolvedTarget + path7.sep)) {
|
|
7477
7960
|
failed.push(skillName);
|
|
7478
7961
|
continue;
|
|
7479
7962
|
}
|
|
7480
|
-
|
|
7481
|
-
|
|
7963
|
+
fs4.rmSync(dest, { recursive: true, force: true });
|
|
7964
|
+
fs4.cpSync(path7.join(skillsSrc, skillName), dest, { recursive: true });
|
|
7482
7965
|
installed.push(skillName);
|
|
7483
7966
|
}
|
|
7484
7967
|
return { installed, failed };
|