@kolatts/pncli 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  setGlobalUser,
20
20
  success,
21
21
  warn
22
- } from "./chunk-VPUWMP2M.js";
22
+ } from "./chunk-SLQOSGZJ.js";
23
23
  import {
24
24
  AdoCoreClient
25
25
  } from "./chunk-VDL32PSG.js";
@@ -294,11 +294,17 @@ function parseBranchReportOutput(raw, branchName, opts) {
294
294
  }
295
295
  };
296
296
  }
297
+ function normalizeGitDate(dateStr) {
298
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
299
+ return `${dateStr}T00:00:00`;
300
+ }
301
+ return dateStr;
302
+ }
297
303
  function buildReportArgs(opts) {
298
304
  const fmt = `${COMMIT_PREFIX}%H %an %aI %s`;
299
305
  const args = ["log", `--format=${fmt}`, "--numstat"];
300
- if (opts.since) args.push(`--since=${opts.since}`);
301
- if (opts.until) args.push(`--until=${opts.until}`);
306
+ if (opts.since) args.push(`--since=${normalizeGitDate(opts.since)}`);
307
+ if (opts.until) args.push(`--until=${normalizeGitDate(opts.until)}`);
302
308
  if (opts.base && opts.branch) {
303
309
  args.push(`${opts.base}..${opts.branch}`);
304
310
  } else if (opts.base) {
@@ -430,13 +436,13 @@ function parseAdoRemote(remoteUrl, adoBaseUrl) {
430
436
  const sshIdx = path6.indexOf("/_ssh/");
431
437
  const markerIdx = gitIdx !== -1 ? gitIdx : sshIdx;
432
438
  if (markerIdx === -1) return null;
433
- const repo = path6.slice(markerIdx + 6);
439
+ const repo = decodeURIComponent(path6.slice(markerIdx + 6));
434
440
  if (!repo) return null;
435
441
  const before = path6.slice(1, markerIdx);
436
442
  const parts = before.split("/").filter(Boolean);
437
443
  if (parts.length < 2) return null;
438
- const project = parts[parts.length - 1];
439
- const collection = parts[parts.length - 2];
444
+ const project = decodeURIComponent(parts[parts.length - 1]);
445
+ const collection = decodeURIComponent(parts[parts.length - 2]);
440
446
  return { collection, project, repo };
441
447
  }
442
448
  function getRemoteUrls(repoRoot) {
@@ -776,6 +782,9 @@ function registerGitCommands(program2) {
776
782
  });
777
783
  }
778
784
 
785
+ // src/services/jira/commands.ts
786
+ import { readFileSync } from "fs";
787
+
779
788
  // src/services/jira/client.ts
780
789
  var API2 = "/rest/api/2";
781
790
  var JiraClient = class {
@@ -796,6 +805,7 @@ var JiraClient = class {
796
805
  ...opts.priority ? { priority: { name: opts.priority } } : {},
797
806
  ...opts.assignee ? { assignee: { name: opts.assignee } } : {},
798
807
  ...opts.labels?.length ? { labels: opts.labels } : {},
808
+ ...opts.parent ? { parent: { key: opts.parent } } : {},
799
809
  ...opts.customFieldValues ?? {}
800
810
  }
801
811
  };
@@ -864,7 +874,56 @@ var JiraClient = class {
864
874
  return { issues: allIssues, total: allIssues.length, startAt: 0, maxResults: allIssues.length };
865
875
  }
866
876
  async fetchFields() {
867
- return this.http.jira(`${API2}/field`);
877
+ const fields = await this.http.jira(`${API2}/field`);
878
+ return fields.map((f) => ({ ...f, pncliType: schemaToPncliType(f.schema) }));
879
+ }
880
+ async fetchFieldsWithAllowedValues(project, issueType) {
881
+ const fields = await this.fetchFields();
882
+ let metaFields = {};
883
+ try {
884
+ const meta = await this.http.jira(`${API2}/issue/createmeta`, {
885
+ params: { projectKeys: project, expand: "projects.issuetypes.fields" }
886
+ });
887
+ metaFields = collectMetaFields(meta.projects, issueType);
888
+ } catch {
889
+ try {
890
+ metaFields = await this.fetchCreateMetaViaIssuetypes(project, issueType);
891
+ } catch {
892
+ }
893
+ }
894
+ return fields.map((f) => {
895
+ const av = metaFields[f.id]?.allowedValues;
896
+ return av?.length ? { ...f, allowedValues: av } : f;
897
+ });
898
+ }
899
+ /**
900
+ * Fallback for instances where the classic createmeta endpoint returns 404.
901
+ * Uses the paginated endpoint available in Jira Datacenter 8.4+.
902
+ */
903
+ async fetchCreateMetaViaIssuetypes(project, issueType) {
904
+ const itList = await this.http.jira(
905
+ `${API2}/issue/createmeta/${encodeURIComponent(project)}/issuetypes`
906
+ );
907
+ const matchingTypes = issueType ? itList.values.filter((it) => it.name.toLowerCase() === issueType.toLowerCase()) : itList.values;
908
+ const result = {};
909
+ for (const it of matchingTypes) {
910
+ const fieldList = await this.http.jira(
911
+ `${API2}/issue/createmeta/${encodeURIComponent(project)}/issuetypes/${it.id}`
912
+ );
913
+ for (const field of fieldList.values) {
914
+ if (!field.allowedValues?.length) continue;
915
+ const existing = result[field.fieldId]?.allowedValues ?? [];
916
+ const seen = new Set(existing.map((v) => v.id));
917
+ for (const av of field.allowedValues) {
918
+ if (!seen.has(av.id)) {
919
+ existing.push(av);
920
+ seen.add(av.id);
921
+ }
922
+ }
923
+ result[field.fieldId] = { allowedValues: existing };
924
+ }
925
+ }
926
+ return result;
868
927
  }
869
928
  async assignIssue(key, accountId) {
870
929
  await this.http.jira(`${API2}/issue/${key}/assignee`, {
@@ -894,6 +953,43 @@ var JiraClient = class {
894
953
  });
895
954
  }
896
955
  };
956
+ function schemaToPncliType(schema) {
957
+ if (!schema) return void 0;
958
+ const custom = schema.custom ?? "";
959
+ if (custom.includes("cascadingselect")) return "cascading-select";
960
+ if (custom.includes("multicheckboxes") || custom.includes("multiselect")) return "multi-select";
961
+ if (custom.includes("select") || custom.includes("radiobuttons")) return "select";
962
+ if (custom.includes("userpicker") || custom.includes("multiuserpicker")) return "user";
963
+ if (custom.includes("labels")) return "labels";
964
+ if (custom.includes("url")) return "url";
965
+ if (custom.includes("textarea") || custom.includes("textfield")) return "textarea";
966
+ if (schema.type === "number") return "number";
967
+ if (schema.type === "date") return "date";
968
+ if (schema.type === "datetime") return "datetime";
969
+ if (schema.type === "string") return "string";
970
+ return void 0;
971
+ }
972
+ function collectMetaFields(projects, issueType) {
973
+ const result = {};
974
+ for (const proj of projects) {
975
+ for (const it of proj.issuetypes) {
976
+ if (issueType && it.name.toLowerCase() !== issueType.toLowerCase()) continue;
977
+ for (const [fieldId, fieldMeta] of Object.entries(it.fields)) {
978
+ if (!fieldMeta.allowedValues?.length) continue;
979
+ const existing = result[fieldId]?.allowedValues ?? [];
980
+ const seen = new Set(existing.map((v) => v.id));
981
+ for (const av of fieldMeta.allowedValues) {
982
+ if (!seen.has(av.id)) {
983
+ existing.push(av);
984
+ seen.add(av.id);
985
+ }
986
+ }
987
+ result[fieldId] = { allowedValues: existing };
988
+ }
989
+ }
990
+ }
991
+ return result;
992
+ }
897
993
 
898
994
  // src/services/jira/custom-fields.ts
899
995
  function buildFieldMap(fields) {
@@ -937,6 +1033,15 @@ function formatFieldValue(value, type) {
937
1033
  return { value };
938
1034
  case "multi-select":
939
1035
  return value.split(",").map((v) => ({ value: v.trim() }));
1036
+ case "option-id":
1037
+ return { id: value };
1038
+ case "cascading-select": {
1039
+ const colon = value.indexOf(":");
1040
+ if (colon !== -1) {
1041
+ return { id: value.slice(0, colon).trim(), child: { id: value.slice(colon + 1).trim() } };
1042
+ }
1043
+ return { id: value.trim() };
1044
+ }
940
1045
  case "labels":
941
1046
  return value.split(",").map((v) => v.trim());
942
1047
  case "user":
@@ -986,41 +1091,41 @@ function registerJiraCommands(program2) {
986
1091
  fail(err, "jira", "get-issue", start);
987
1092
  }
988
1093
  });
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>", 'Parent issue key \u2014 creates an "is child of" link after creation').option("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
1094
+ 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
1095
  const start = Date.now();
1096
+ let fieldMap = buildFieldMap([]);
991
1097
  try {
992
- const { client, fieldMap } = getClientAndFields(program2);
1098
+ const resolved = getClientAndFields(program2);
1099
+ const client = resolved.client;
1100
+ fieldMap = resolved.fieldMap;
993
1101
  const defaults = getDefaults(program2);
994
1102
  const project = opts.project ?? defaults.project;
995
1103
  const issueType = opts.type ?? defaults.issueType ?? "Task";
996
1104
  const priority = opts.priority ?? defaults.priority;
997
1105
  if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
998
1106
  const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
999
- const customFieldValues = parseFieldArgs(opts.field, fieldMap);
1000
- const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, customFieldValues });
1107
+ const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1108
+ const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1109
+ const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, parent: opts.parent, customFieldValues });
1001
1110
  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
1111
  } catch (err) {
1011
- fail(err, "jira", "create-issue", start);
1112
+ fail(translateFieldErrors(err, fieldMap), "jira", "create-issue", start);
1012
1113
  }
1013
1114
  });
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) => {
1115
+ 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
1116
  const start = Date.now();
1117
+ let fieldMap = buildFieldMap([]);
1016
1118
  try {
1017
- const { client, fieldMap } = getClientAndFields(program2);
1119
+ const resolved = getClientAndFields(program2);
1120
+ const client = resolved.client;
1121
+ fieldMap = resolved.fieldMap;
1018
1122
  const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
1019
- const customFieldValues = parseFieldArgs(opts.field, fieldMap);
1123
+ const fileFields = opts.fieldsFile ? parseFieldsFile(opts.fieldsFile, fieldMap) : {};
1124
+ const customFieldValues = { ...fileFields, ...parseFieldArgs(opts.field, fieldMap) };
1020
1125
  await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
1021
1126
  success({ updated: opts.key }, "jira", "update-issue", start);
1022
1127
  } catch (err) {
1023
- fail(err, "jira", "update-issue", start);
1128
+ fail(translateFieldErrors(err, fieldMap), "jira", "update-issue", start);
1024
1129
  }
1025
1130
  });
1026
1131
  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) => {
@@ -1106,12 +1211,12 @@ function registerJiraCommands(program2) {
1106
1211
  fail(err, "jira", "link-issue", start);
1107
1212
  }
1108
1213
  });
1109
- 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)").action(async (opts) => {
1214
+ 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
1215
  const start = Date.now();
1111
1216
  try {
1112
1217
  if (opts.discover) {
1113
1218
  const client = getClient(program2);
1114
- const fields = await client.fetchFields();
1219
+ const fields = opts.project ? await client.fetchFieldsWithAllowedValues(opts.project, opts.issueType) : await client.fetchFields();
1115
1220
  const result = opts.customOnly ? fields.filter((f) => f.custom) : fields;
1116
1221
  success(result, "jira", "fields", start);
1117
1222
  } else {
@@ -1130,22 +1235,76 @@ function parseFieldArgs(fieldArgs, fieldMap) {
1130
1235
  if (eq === -1) throw new PncliError(`Invalid --field format (expected Name=value): ${arg}`, 1);
1131
1236
  const name = arg.slice(0, eq).trim();
1132
1237
  const value = arg.slice(eq + 1);
1133
- const def = fieldMap.byName.get(name.toLowerCase());
1134
- if (!def) throw new PncliError(`Unknown custom field: "${name}". Run: pncli jira fields`, 1);
1135
- result[def.id] = formatFieldValue(value, def.type);
1238
+ const def = fieldMap.byName.get(name.toLowerCase()) ?? fieldMap.byId.get(name);
1239
+ if (!def) throw new PncliError(
1240
+ `Unknown custom field: "${name}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1241
+ 1
1242
+ );
1243
+ if (value.startsWith("@")) {
1244
+ const filePath = value.slice(1);
1245
+ let raw;
1246
+ try {
1247
+ raw = readFileSync(filePath, "utf8").trim();
1248
+ } catch (e) {
1249
+ throw new PncliError(`Cannot read file "${filePath}": ${e.message}`, 1);
1250
+ }
1251
+ try {
1252
+ result[def.id] = JSON.parse(raw);
1253
+ } catch {
1254
+ result[def.id] = raw;
1255
+ }
1256
+ } else {
1257
+ result[def.id] = formatFieldValue(value, def.type);
1258
+ }
1259
+ }
1260
+ return result;
1261
+ }
1262
+ function parseFieldsFile(filePath, fieldMap) {
1263
+ let raw;
1264
+ try {
1265
+ raw = readFileSync(filePath, "utf8");
1266
+ } catch (e) {
1267
+ throw new PncliError(`Cannot read fields file "${filePath}": ${e.message}`, 1);
1268
+ }
1269
+ let data;
1270
+ try {
1271
+ data = JSON.parse(raw);
1272
+ } catch {
1273
+ throw new PncliError(`Fields file "${filePath}" must be a valid JSON object`, 1);
1274
+ }
1275
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1276
+ throw new PncliError(`Fields file "${filePath}" must be a JSON object`, 1);
1277
+ }
1278
+ const result = {};
1279
+ for (const [key, val] of Object.entries(data)) {
1280
+ const def = fieldMap.byName.get(key.toLowerCase()) ?? fieldMap.byId.get(key);
1281
+ if (!def) throw new PncliError(
1282
+ `Unknown field in "${filePath}": "${key}". Fields must be registered in config by friendly name or ID. Run: pncli jira fields`,
1283
+ 1
1284
+ );
1285
+ result[def.id] = val;
1136
1286
  }
1137
1287
  return result;
1138
1288
  }
1289
+ function translateFieldErrors(err, fieldMap) {
1290
+ if (!(err instanceof PncliError) || fieldMap.byId.size === 0) return err;
1291
+ const translated = err.message.replace(/\bcustomfield_\d+\b/g, (id) => {
1292
+ const def = fieldMap.byId.get(id);
1293
+ return def ? `${def.name} (${id})` : id;
1294
+ });
1295
+ if (translated === err.message) return err;
1296
+ return new PncliError(translated, err.status, err.url);
1297
+ }
1139
1298
 
1140
1299
  // src/services/bitbucket/commands.ts
1141
- function getClient2(program2) {
1300
+ function getClient2(program2, overrides) {
1142
1301
  const opts = program2.optsWithGlobals();
1143
1302
  const config = loadConfig({ configPath: opts.config });
1144
1303
  const http = createHttpClient(config, Boolean(opts.dryRun));
1145
1304
  const client = new BitbucketClient(http);
1146
1305
  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 ?? "";
1306
+ const project = overrides?.project ?? opts.project ?? ctx?.project ?? config.defaults.bitbucket?.project ?? "";
1307
+ const repo = overrides?.repo ?? opts.repo ?? ctx?.repo ?? config.defaults.bitbucket?.repo ?? "";
1149
1308
  if (!project || !repo) {
1150
1309
  throw new PncliError(
1151
1310
  "Could not determine Bitbucket project/repo. Pass --project and --repo, or run pncli config init.",
@@ -1176,10 +1335,10 @@ function registerBitbucketCommands(program2) {
1176
1335
  fail(err, "bitbucket", "get-pr", start);
1177
1336
  }
1178
1337
  });
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) => {
1338
+ 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
1339
  const start = Date.now();
1181
1340
  try {
1182
- const { client, project, repo } = getClient2(bb);
1341
+ const { client, project, repo } = getClient2(bb, { project: opts.project, repo: opts.repo });
1183
1342
  const config = loadConfig({ configPath: bb.optsWithGlobals().config });
1184
1343
  const target = opts.target ?? config.defaults.bitbucket?.targetBranch ?? "main";
1185
1344
  const reviewers = opts.reviewers ? opts.reviewers.split(",").map((s) => s.trim()) : [];
@@ -4037,7 +4196,9 @@ function registerDepsCommands(program2) {
4037
4196
  }
4038
4197
 
4039
4198
  // src/services/config/commands.ts
4040
- import { input, password, confirm } from "@inquirer/prompts";
4199
+ import input from "@inquirer/input";
4200
+ import password from "@inquirer/password";
4201
+ import confirm from "@inquirer/confirm";
4041
4202
 
4042
4203
  // src/services/ado/client/work.ts
4043
4204
  var API6 = "7.1";
@@ -5450,15 +5611,22 @@ var AdoBuildClient = class {
5450
5611
  );
5451
5612
  }
5452
5613
  async listBuilds(collection, project, opts = {}) {
5614
+ const baseParams = {
5615
+ "api-version": API8,
5616
+ ...opts.definitionIds?.length ? { definitions: opts.definitionIds.join(",") } : {},
5617
+ ...opts.branchName ? { branchName: opts.branchName } : {},
5618
+ ...opts.statusFilter ? { statusFilter: opts.statusFilter } : {},
5619
+ ...opts.top ? { "$top": opts.top } : {}
5620
+ };
5621
+ if (opts.top) {
5622
+ const { data } = await this.http.adoRaw(
5623
+ `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds`,
5624
+ { params: baseParams }
5625
+ );
5626
+ return (data.value ?? []).slice(0, opts.top);
5627
+ }
5453
5628
  return this.http.adoPaginate(async (token) => {
5454
- const params = {
5455
- "api-version": API8,
5456
- ...opts.definitionIds?.length ? { definitionIds: opts.definitionIds.join(",") } : {},
5457
- ...opts.branchName ? { branchName: opts.branchName } : {},
5458
- ...opts.statusFilter ? { statusFilter: opts.statusFilter } : {},
5459
- ...opts.top ? { "$top": opts.top } : {},
5460
- ...token ? { continuationToken: token } : {}
5461
- };
5629
+ const params = { ...baseParams, ...token ? { continuationToken: token } : {} };
5462
5630
  return this.http.adoRaw(
5463
5631
  `/${encodeURIComponent(collection)}/${encodeURIComponent(project)}/_apis/build/builds`,
5464
5632
  { params }
@@ -6074,7 +6242,7 @@ function registerAdoRepoCommands(ado) {
6074
6242
  const config = lc({ configPath: globalOpts.config });
6075
6243
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
6076
6244
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
6077
- const { createHttpClient: createHttpClient2 } = await import("./http-LNDRNPP7.js");
6245
+ const { createHttpClient: createHttpClient2 } = await import("./http-5GDQYH62.js");
6078
6246
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
6079
6247
  const coreClient = new AdoCoreClient2(http);
6080
6248
  const me = await coreClient.getConnectionData(collection);
@@ -6167,12 +6335,32 @@ function registerAdoPipelineCommands(ado) {
6167
6335
  fail(err, "ado", "pipeline-run", start);
6168
6336
  }
6169
6337
  });
6170
- 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) => {
6338
+ 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) => {
6171
6339
  const start = Date.now();
6172
6340
  try {
6173
6341
  const { collection, project, buildClient } = getAdoContext(ado);
6342
+ let definitionId;
6343
+ if (opts.definition && opts.name) {
6344
+ throw new PncliError("Cannot specify both --definition and --name. Use one or the other.", 1);
6345
+ }
6346
+ if (opts.definition) {
6347
+ definitionId = parseInt(opts.definition, 10);
6348
+ if (isNaN(definitionId)) {
6349
+ throw new PncliError(`--definition must be a numeric ID. Got "${opts.definition}". Use --name to filter by pipeline name instead.`, 1);
6350
+ }
6351
+ } else if (opts.name) {
6352
+ const defs = await buildClient.listDefinitions(collection, project);
6353
+ const match = defs.find((d) => d.name.toLowerCase() === opts.name.toLowerCase());
6354
+ if (!match) {
6355
+ throw new PncliError(
6356
+ `Pipeline "${opts.name}" not found. Run 'pncli ado pipeline list' to see available pipelines.`,
6357
+ 1
6358
+ );
6359
+ }
6360
+ definitionId = match.id;
6361
+ }
6174
6362
  const data = await buildClient.listBuilds(collection, project, {
6175
- definitionIds: opts.definition ? [parseInt(opts.definition, 10)] : void 0,
6363
+ definitionIds: definitionId !== void 0 ? [definitionId] : void 0,
6176
6364
  branchName: opts.branch,
6177
6365
  statusFilter: opts.status,
6178
6366
  top: parseInt(opts.top, 10)
@@ -6202,11 +6390,13 @@ function registerAdoPipelineCommands(ado) {
6202
6390
  fail(err, "ado", "pipeline-cancel-run", start);
6203
6391
  }
6204
6392
  });
6205
- pipeline.command("logs").description("List or retrieve build logs (consider --output-file for large logs)").requiredOption("--id <n>", "Build ID").option("--log-id <n>", "Specific log ID (omit to list all logs)").action(async (opts) => {
6393
+ 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) => {
6206
6394
  const start = Date.now();
6207
6395
  try {
6396
+ const rawId = opts.buildId ?? opts.id;
6397
+ if (!rawId) throw new PncliError("--build-id is required (build run ID from pipeline list-runs or get-run)", 1);
6208
6398
  const { collection, project, buildClient } = getAdoContext(ado);
6209
- const buildId = parseInt(opts.id, 10);
6399
+ const buildId = parseInt(rawId, 10);
6210
6400
  if (opts.logId) {
6211
6401
  const data = await buildClient.getLog(collection, project, buildId, parseInt(opts.logId, 10));
6212
6402
  success({ log: data }, "ado", "pipeline-logs", start);
@@ -6258,9 +6448,10 @@ var JenkinsClient = class {
6258
6448
  this.http = http;
6259
6449
  }
6260
6450
  http;
6261
- async listJobs() {
6451
+ async listJobs(folder) {
6452
+ const endpoint = folder ? `/job/${jobPath(folder)}/api/json` : "/api/json";
6262
6453
  const data = await this.http.jenkins(
6263
- "/api/json",
6454
+ endpoint,
6264
6455
  { params: { tree: "jobs[name,url,color,description,buildable,lastBuild[number,url,result]]" } }
6265
6456
  );
6266
6457
  return data.jobs;
@@ -6340,17 +6531,17 @@ async function pollBuildComplete(client, jobName, buildNumber, pollMs, timeoutMs
6340
6531
  function registerJenkinsCommands(program2) {
6341
6532
  const jenkins = program2.command("jenkins").description("Jenkins Data Center operations");
6342
6533
  const pipeline = jenkins.command("pipeline").description("Pipeline (job/build) operations");
6343
- pipeline.command("list").description("List all jobs on the Jenkins instance").action(async () => {
6534
+ 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) => {
6344
6535
  const start = Date.now();
6345
6536
  try {
6346
6537
  const client = getClient6(program2);
6347
- const data = await client.listJobs();
6538
+ const data = await client.listJobs(opts.folder);
6348
6539
  success(data, "jenkins", "pipeline-list", start);
6349
6540
  } catch (err) {
6350
6541
  fail(err, "jenkins", "pipeline-list", start);
6351
6542
  }
6352
6543
  });
6353
- pipeline.command("get").description("Get details for a specific job").requiredOption("--name <job>", "Job name").action(async (opts) => {
6544
+ 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) => {
6354
6545
  const start = Date.now();
6355
6546
  try {
6356
6547
  const client = getClient6(program2);
@@ -6360,7 +6551,7 @@ function registerJenkinsCommands(program2) {
6360
6551
  fail(err, "jenkins", "pipeline-get", start);
6361
6552
  }
6362
6553
  });
6363
- pipeline.command("run").description("Trigger a build").requiredOption("--name <job>", "Job name").option("--parameter <k=v>", "Build parameter (repeatable)", (v, acc) => {
6554
+ 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) => {
6364
6555
  acc.push(v);
6365
6556
  return acc;
6366
6557
  }, []).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) => {
@@ -6396,7 +6587,7 @@ function registerJenkinsCommands(program2) {
6396
6587
  fail(err, "jenkins", "pipeline-run", start);
6397
6588
  }
6398
6589
  });
6399
- 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) => {
6590
+ 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) => {
6400
6591
  const start = Date.now();
6401
6592
  try {
6402
6593
  const client = getClient6(program2);
@@ -6408,7 +6599,7 @@ function registerJenkinsCommands(program2) {
6408
6599
  fail(err, "jenkins", "pipeline-list-runs", start);
6409
6600
  }
6410
6601
  });
6411
- pipeline.command("get-run").description("Get details for a specific build").requiredOption("--name <job>", "Job name").requiredOption("--number <n>", "Build number").action(async (opts) => {
6602
+ 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) => {
6412
6603
  const start = Date.now();
6413
6604
  try {
6414
6605
  const client = getClient6(program2);
@@ -6420,7 +6611,7 @@ function registerJenkinsCommands(program2) {
6420
6611
  fail(err, "jenkins", "pipeline-get-run", start);
6421
6612
  }
6422
6613
  });
6423
- 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) => {
6614
+ 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) => {
6424
6615
  const start = Date.now();
6425
6616
  try {
6426
6617
  const client = getClient6(program2);
@@ -6491,8 +6682,18 @@ var ArtifactoryClient = class {
6491
6682
  headers: { "Content-Type": "text/plain" }
6492
6683
  });
6493
6684
  }
6494
- async listBuilds() {
6495
- return this.http.artifactory(`${API9}/build`);
6685
+ async listBuilds(opts = {}) {
6686
+ const raw = await this.http.artifactory(`${API9}/build`, { timeoutMs: opts.timeoutMs });
6687
+ const builds = (raw.builds ?? []).map((b) => ({
6688
+ name: b.uri.replace(/^\/|\/$/g, ""),
6689
+ uri: b.uri,
6690
+ lastStarted: b.lastStarted
6691
+ }));
6692
+ const result = { builds, uri: raw.uri };
6693
+ if (builds.length === 0) {
6694
+ 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.';
6695
+ }
6696
+ return result;
6496
6697
  }
6497
6698
  async listBuildRuns(buildName) {
6498
6699
  return this.http.artifactory(`${API9}/build/${encodeURIComponent(buildName)}`);
@@ -6593,11 +6794,14 @@ function registerArtifactoryCommands(program2) {
6593
6794
  fail(err, "artifactory", "search", start);
6594
6795
  }
6595
6796
  });
6596
- art.command("builds").description("List all build names in Artifactory").action(async () => {
6797
+ 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) => {
6597
6798
  const start = Date.now();
6598
6799
  try {
6599
6800
  const client = getClient7(program2);
6600
- const data = await client.listBuilds();
6801
+ const timeoutSecs = parseInt(opts.timeout, 10);
6802
+ if (isNaN(timeoutSecs) || timeoutSecs <= 0) throw new PncliError("--timeout must be a positive integer (seconds)", 1);
6803
+ const timeoutMs = timeoutSecs * 1e3;
6804
+ const data = await client.listBuilds({ timeoutMs });
6601
6805
  success(data, "artifactory", "builds", start);
6602
6806
  } catch (err) {
6603
6807
  fail(err, "artifactory", "builds", start);
@@ -7183,7 +7387,7 @@ import path5 from "path";
7183
7387
  import os from "os";
7184
7388
  import { fileURLToPath } from "url";
7185
7389
  import { execFileSync as execFileSync3 } from "child_process";
7186
- import { select } from "@inquirer/prompts";
7390
+ import select from "@inquirer/select";
7187
7391
  function injectTokenIntoUrl(url, token) {
7188
7392
  let parsed;
7189
7393
  try {
@@ -7191,10 +7395,22 @@ function injectTokenIntoUrl(url, token) {
7191
7395
  } catch {
7192
7396
  throw new Error(`--token requires an HTTPS clone URL; got: ${url}`);
7193
7397
  }
7194
- parsed.username = "x-token-auth";
7398
+ parsed.username = parsed.hostname === "github.com" ? "x-access-token" : "x-token-auth";
7195
7399
  parsed.password = token;
7196
7400
  return parsed.toString();
7197
7401
  }
7402
+ function repoNameFromUrl(url) {
7403
+ try {
7404
+ const parsed = new URL(url);
7405
+ const segment = parsed.pathname.split("/").filter(Boolean).pop() ?? "marketplace";
7406
+ return segment.replace(/\.git$/i, "") || "marketplace";
7407
+ } catch {
7408
+ return "marketplace";
7409
+ }
7410
+ }
7411
+ function defaultMarketplacePath(url) {
7412
+ return path5.join(os.homedir(), ".agents", "marketplaces", repoNameFromUrl(url));
7413
+ }
7198
7414
  var AGENT_PATHS = {
7199
7415
  "github-copilot": { project: ".agents/skills", user: path5.join(os.homedir(), ".copilot/skills") },
7200
7416
  "claude-code": { project: ".claude/skills", user: path5.join(os.homedir(), ".claude/skills") }
@@ -7344,10 +7560,10 @@ function registerSkillsCommands(program2) {
7344
7560
  }
7345
7561
  });
7346
7562
  const marketplace = skills.command("marketplace").description("Manage a git-hosted skills marketplace");
7347
- 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 the marketplace into").option("--branch <branch>", "Branch to clone", "master").option("--token <token>", "Bitbucket HTTP access token for authenticated clone and pull").action(async (url, localPath, opts) => {
7563
+ 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) => {
7348
7564
  const start = Date.now();
7349
7565
  try {
7350
- const resolvedPath = path5.resolve(localPath);
7566
+ const resolvedPath = path5.resolve(localPath ?? defaultMarketplacePath(url));
7351
7567
  const hasGit = fs3.existsSync(path5.join(resolvedPath, ".git"));
7352
7568
  if (fs3.existsSync(resolvedPath) && !hasGit && fs3.readdirSync(resolvedPath).length > 0) {
7353
7569
  throw new Error(`Directory already exists and is not a git repo: ${resolvedPath}`);
@@ -7355,25 +7571,29 @@ function registerSkillsCommands(program2) {
7355
7571
  if (hasGit) {
7356
7572
  warn(`Directory already contains a git repo at ${resolvedPath} \u2014 skipping clone, updating config only.`);
7357
7573
  } else {
7358
- warn(`Cloning ${url} (branch: ${opts.branch}) \u2192 ${resolvedPath}...`);
7574
+ const branchLabel = opts.branch ?? "remote default";
7575
+ warn(`Cloning ${url} (branch: ${branchLabel}) \u2192 ${resolvedPath}...`);
7359
7576
  const cloneUrl = opts.token ? injectTokenIntoUrl(url, opts.token) : url;
7577
+ const cloneArgs = ["clone"];
7578
+ if (opts.branch) cloneArgs.push("--branch", opts.branch);
7579
+ cloneArgs.push(cloneUrl, resolvedPath);
7360
7580
  try {
7361
- execFileSync3("git", ["clone", "--branch", opts.branch, cloneUrl, resolvedPath], { stdio: ["inherit", "inherit", "pipe"] });
7581
+ execFileSync3("git", cloneArgs, { stdio: ["inherit", "inherit", "pipe"] });
7362
7582
  } catch (e) {
7363
7583
  const msg = e instanceof Error ? e.message : String(e);
7364
- throw new Error(msg.replace(/x-token-auth:[^@]+@/g, "x-token-auth:***@"));
7584
+ throw new Error(msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@"));
7365
7585
  }
7366
7586
  }
7367
7587
  const configPath = getGlobalConfigPath();
7368
7588
  const existing = loadJsonFile(configPath) ?? {};
7369
7589
  existing.marketplace = { repoUrl: url, localPath: resolvedPath, ...opts.token ? { token: opts.token } : {} };
7370
7590
  writeGlobalConfig(existing, configPath);
7371
- success({ repoUrl: url, localPath: resolvedPath, branch: opts.branch, tokenConfigured: !!opts.token }, "skills", "marketplace-setup", start);
7591
+ success({ repoUrl: url, localPath: resolvedPath, branch: opts.branch ?? null, tokenConfigured: !!opts.token }, "skills", "marketplace-setup", start);
7372
7592
  } catch (err) {
7373
7593
  fail(err, "skills", "marketplace-setup", start);
7374
7594
  }
7375
7595
  });
7376
- marketplace.command("sync").description("Pull latest marketplace content and install a plugin's skills").argument("[plugin]", "Plugin name to install (skips interactive selection)").option("--claude", "Install to ~/.claude/skills instead of ~/.agents/skills").action(async (plugin, opts) => {
7596
+ 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) => {
7377
7597
  const start = Date.now();
7378
7598
  try {
7379
7599
  const configPath = getGlobalConfigPath();
@@ -7390,16 +7610,58 @@ function registerSkillsCommands(program2) {
7390
7610
  gitArgs.push("-c", `remote.origin.url=${injectTokenIntoUrl(repoUrl, token)}`);
7391
7611
  }
7392
7612
  gitArgs.push("pull");
7613
+ let pullOutput;
7393
7614
  try {
7394
- execFileSync3("git", gitArgs, { stdio: ["inherit", "inherit", "pipe"] });
7615
+ pullOutput = execFileSync3("git", gitArgs, { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } });
7395
7616
  } catch (e) {
7396
7617
  const msg = e instanceof Error ? e.message : String(e);
7397
- throw new Error(msg.replace(/x-token-auth:[^@]+@/g, "x-token-auth:***@"));
7618
+ throw new Error(msg.replace(/x-(?:token-auth|access-token):[^@]+@/g, "x-token-auth:***@"));
7619
+ }
7620
+ const marketplaceUpdated = !pullOutput.includes("Already up to date");
7621
+ if (pullOutput.trim() && marketplaceUpdated) {
7622
+ warn(pullOutput.trim());
7623
+ }
7624
+ warn(marketplaceUpdated ? "Marketplace updated." : "Marketplace already up to date \u2014 no changes to sync.");
7625
+ if (!marketplaceUpdated && !plugin && !opts.force) {
7626
+ success({
7627
+ marketplace: marketplacePath,
7628
+ marketplaceUpdated: false,
7629
+ updated: false,
7630
+ skipped: true,
7631
+ message: "No marketplace changes detected \u2014 skipping plugin selection and install."
7632
+ }, "skills", "marketplace-sync", start);
7633
+ return;
7398
7634
  }
7399
7635
  const pluginChoices = resolvePluginChoices(marketplacePath);
7400
7636
  if (pluginChoices.length === 0) {
7401
7637
  throw new Error("No plugins found in marketplace. Check the marketplace repository structure.");
7402
7638
  }
7639
+ const targetDir = opts.claude ? path5.join(os.homedir(), ".claude", "skills") : path5.join(os.homedir(), ".agents", "skills");
7640
+ if (plugin === "all") {
7641
+ const results = {};
7642
+ let totalInstalled = 0;
7643
+ for (const pluginChoice of pluginChoices) {
7644
+ const skillsSrc2 = resolveSkillsSrc(marketplacePath, pluginChoice.name);
7645
+ if (!fs3.existsSync(skillsSrc2)) {
7646
+ results[pluginChoice.name] = { installed: [], failed: [] };
7647
+ warn(`No skills directory found for plugin "${pluginChoice.name}" \u2014 skipping.`);
7648
+ continue;
7649
+ }
7650
+ const { installed: installed2, failed: failed2 } = copyPluginSkills(skillsSrc2, targetDir);
7651
+ results[pluginChoice.name] = { installed: installed2, failed: failed2 };
7652
+ totalInstalled += installed2.length;
7653
+ if (failed2.length > 0) {
7654
+ warn(`Skipped ${failed2.length} skill(s) with invalid names in "${pluginChoice.name}": ${failed2.join(", ")}`);
7655
+ }
7656
+ }
7657
+ success({
7658
+ plugins: results,
7659
+ total: totalInstalled,
7660
+ target: targetDir,
7661
+ marketplaceUpdated
7662
+ }, "skills", "marketplace-sync", start);
7663
+ return;
7664
+ }
7403
7665
  let selectedPlugin;
7404
7666
  if (plugin) {
7405
7667
  if (!pluginChoices.some((p) => p.name === plugin)) {
@@ -7419,7 +7681,6 @@ function registerSkillsCommands(program2) {
7419
7681
  if (!fs3.existsSync(skillsSrc)) {
7420
7682
  throw new Error(`No skills directory found at ${skillsSrc}`);
7421
7683
  }
7422
- const targetDir = opts.claude ? path5.join(os.homedir(), ".claude", "skills") : path5.join(os.homedir(), ".agents", "skills");
7423
7684
  const { installed, failed } = copyPluginSkills(skillsSrc, targetDir);
7424
7685
  if (failed.length > 0) {
7425
7686
  warn(`Skipped ${failed.length} skill(s) with invalid names: ${failed.join(", ")}`);
@@ -7429,7 +7690,8 @@ function registerSkillsCommands(program2) {
7429
7690
  installed,
7430
7691
  failed,
7431
7692
  total: installed.length,
7432
- target: targetDir
7693
+ target: targetDir,
7694
+ marketplaceUpdated
7433
7695
  }, "skills", "marketplace-sync", start);
7434
7696
  } catch (err) {
7435
7697
  fail(err, "skills", "marketplace-sync", start);