@kolatts/pncli 1.0.1 → 1.1.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
@@ -21,15 +21,20 @@ var PncliError = class extends Error {
21
21
 
22
22
  // src/lib/output.ts
23
23
  var globalOptions = { pretty: false, verbose: false };
24
+ var globalUser = { email: void 0, userId: void 0 };
24
25
  function setGlobalOptions(opts) {
25
26
  globalOptions = opts;
26
27
  }
28
+ function setGlobalUser(user) {
29
+ globalUser = user;
30
+ }
27
31
  function buildMeta(service, action, startTime) {
28
32
  return {
29
33
  service,
30
34
  action,
31
35
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
32
- duration_ms: Date.now() - startTime
36
+ duration_ms: Date.now() - startTime,
37
+ user: globalUser.email ?? globalUser.userId ? globalUser : void 0
33
38
  };
34
39
  }
35
40
  function success(data, service, action, startTime) {
@@ -73,11 +78,127 @@ function warn(message) {
73
78
  );
74
79
  }
75
80
 
81
+ // src/lib/config.ts
82
+ import fs from "fs";
83
+ import os from "os";
84
+ import path from "path";
85
+ import { execSync } from "child_process";
86
+ var ENV_KEYS = {
87
+ EMAIL: "PNCLI_EMAIL",
88
+ USERID: "PNCLI_USERID",
89
+ JIRA_BASE_URL: "PNCLI_JIRA_BASE_URL",
90
+ JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
91
+ BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
92
+ BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
93
+ CONFIG_PATH: "PNCLI_CONFIG_PATH"
94
+ };
95
+ function getGlobalConfigPath(overridePath) {
96
+ if (overridePath) return overridePath;
97
+ const envPath = process.env[ENV_KEYS.CONFIG_PATH];
98
+ if (envPath) return envPath;
99
+ return path.join(os.homedir(), ".pncli", "config.json");
100
+ }
101
+ function loadJsonFile(filePath) {
102
+ try {
103
+ const content = fs.readFileSync(filePath, "utf8");
104
+ return JSON.parse(content);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function getRepoRoot() {
110
+ try {
111
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ function mergeCustomFields(global, repo) {
117
+ const map = /* @__PURE__ */ new Map();
118
+ for (const f of global ?? []) map.set(f.id, f);
119
+ for (const f of repo ?? []) map.set(f.id, f);
120
+ return Array.from(map.values());
121
+ }
122
+ function mergeDefaults(global, repo) {
123
+ return {
124
+ jira: { ...global?.jira, ...repo?.jira },
125
+ bitbucket: { ...global?.bitbucket, ...repo?.bitbucket }
126
+ };
127
+ }
128
+ function loadConfig(opts = {}) {
129
+ const globalConfigPath = getGlobalConfigPath(opts.configPath);
130
+ const globalConfig = loadJsonFile(globalConfigPath) ?? {};
131
+ const repoRoot = getRepoRoot();
132
+ let repoConfig = {};
133
+ if (repoRoot) {
134
+ repoConfig = loadJsonFile(path.join(repoRoot, ".pncli.json")) ?? {};
135
+ }
136
+ const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);
137
+ return {
138
+ user: {
139
+ email: process.env[ENV_KEYS.EMAIL] ?? globalConfig.user?.email,
140
+ userId: process.env[ENV_KEYS.USERID] ?? globalConfig.user?.userId
141
+ },
142
+ jira: {
143
+ baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,
144
+ apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,
145
+ customFields: mergeCustomFields(globalConfig.jira?.customFields, repoConfig.jira?.customFields)
146
+ },
147
+ bitbucket: {
148
+ baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
149
+ pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
150
+ },
151
+ defaults: mergedDefaults
152
+ };
153
+ }
154
+ function writeGlobalConfig(config, configPath) {
155
+ const filePath = getGlobalConfigPath(configPath);
156
+ const dir = path.dirname(filePath);
157
+ if (!fs.existsSync(dir)) {
158
+ fs.mkdirSync(dir, { recursive: true });
159
+ }
160
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
161
+ }
162
+ function writeRepoConfig(config) {
163
+ const repoRoot = getRepoRoot();
164
+ const targetDir = repoRoot ?? process.cwd();
165
+ fs.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
166
+ }
167
+ function setConfigValue(key, value, configPath) {
168
+ const filePath = getGlobalConfigPath(configPath);
169
+ const existing = loadJsonFile(filePath) ?? {};
170
+ const parts = key.split(".");
171
+ let current = existing;
172
+ for (let i = 0; i < parts.length - 1; i++) {
173
+ const part = parts[i];
174
+ if (typeof current[part] !== "object" || current[part] === null) {
175
+ current[part] = {};
176
+ }
177
+ current = current[part];
178
+ }
179
+ current[parts[parts.length - 1]] = value;
180
+ writeGlobalConfig(existing, configPath);
181
+ }
182
+ function maskConfig(config) {
183
+ return {
184
+ ...config,
185
+ jira: {
186
+ ...config.jira,
187
+ apiToken: config.jira.apiToken ? "***" : void 0,
188
+ customFields: config.jira.customFields
189
+ },
190
+ bitbucket: {
191
+ ...config.bitbucket,
192
+ pat: config.bitbucket.pat ? "***" : void 0
193
+ }
194
+ };
195
+ }
196
+
76
197
  // src/services/git/client.ts
77
- import { execSync, execFileSync } from "child_process";
198
+ import { execSync as execSync2, execFileSync } from "child_process";
78
199
  var DIFF_LINE_LIMIT = 5e3;
79
200
  function exec(cmd, cwd) {
80
- return execSync(cmd, { encoding: "utf8", cwd }).trim();
201
+ return execSync2(cmd, { encoding: "utf8", cwd }).trim();
81
202
  }
82
203
  function getStatus(root) {
83
204
  const output = exec("git status --porcelain", root);
@@ -209,17 +330,17 @@ function getBranches(root) {
209
330
  }
210
331
 
211
332
  // src/lib/git-context.ts
212
- import { execSync as execSync2 } from "child_process";
213
- function getRepoRoot() {
333
+ import { execSync as execSync3 } from "child_process";
334
+ function getRepoRoot2() {
214
335
  try {
215
- return execSync2("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
336
+ return execSync3("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
216
337
  } catch {
217
338
  return null;
218
339
  }
219
340
  }
220
341
  function getCurrentBranch(repoRoot) {
221
342
  try {
222
- return execSync2("git rev-parse --abbrev-ref HEAD", {
343
+ return execSync3("git rev-parse --abbrev-ref HEAD", {
223
344
  encoding: "utf8",
224
345
  cwd: repoRoot
225
346
  }).trim();
@@ -248,14 +369,14 @@ function parseRemote(remoteUrl, bitbucketBaseUrl) {
248
369
  }
249
370
  function getRemoteUrls(repoRoot) {
250
371
  try {
251
- const output = execSync2("git remote -v", { encoding: "utf8", cwd: repoRoot });
372
+ const output = execSync3("git remote -v", { encoding: "utf8", cwd: repoRoot });
252
373
  return output.split("\n").filter((line) => line.includes("(fetch)")).map((line) => line.split(" ")[1]?.split(" ")[0] ?? "").filter(Boolean);
253
374
  } catch {
254
375
  return [];
255
376
  }
256
377
  }
257
378
  function getGitContext(config) {
258
- const root = getRepoRoot();
379
+ const root = getRepoRoot2();
259
380
  if (!root) return null;
260
381
  const branch = getCurrentBranch(root) ?? "unknown";
261
382
  const remoteUrls = getRemoteUrls(root);
@@ -272,110 +393,6 @@ function getGitContext(config) {
272
393
  return { root, branch, project, repo };
273
394
  }
274
395
 
275
- // src/lib/config.ts
276
- import fs from "fs";
277
- import os from "os";
278
- import path from "path";
279
- import { execSync as execSync3 } from "child_process";
280
- var ENV_KEYS = {
281
- JIRA_BASE_URL: "PNCLI_JIRA_BASE_URL",
282
- JIRA_EMAIL: "PNCLI_JIRA_EMAIL",
283
- JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
284
- BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
285
- BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
286
- CONFIG_PATH: "PNCLI_CONFIG_PATH"
287
- };
288
- function getGlobalConfigPath(overridePath) {
289
- if (overridePath) return overridePath;
290
- const envPath = process.env[ENV_KEYS.CONFIG_PATH];
291
- if (envPath) return envPath;
292
- return path.join(os.homedir(), ".pncli", "config.json");
293
- }
294
- function loadJsonFile(filePath) {
295
- try {
296
- const content = fs.readFileSync(filePath, "utf8");
297
- return JSON.parse(content);
298
- } catch {
299
- return null;
300
- }
301
- }
302
- function getRepoRoot2() {
303
- try {
304
- return execSync3("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
305
- } catch {
306
- return null;
307
- }
308
- }
309
- function mergeDefaults(global, repo) {
310
- return {
311
- jira: { ...global?.jira, ...repo?.jira },
312
- bitbucket: { ...global?.bitbucket, ...repo?.bitbucket }
313
- };
314
- }
315
- function loadConfig(opts = {}) {
316
- const globalConfigPath = getGlobalConfigPath(opts.configPath);
317
- const globalConfig = loadJsonFile(globalConfigPath) ?? {};
318
- const repoRoot = getRepoRoot2();
319
- let repoConfig = {};
320
- if (repoRoot) {
321
- repoConfig = loadJsonFile(path.join(repoRoot, ".pncli.json")) ?? {};
322
- }
323
- const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);
324
- return {
325
- jira: {
326
- baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,
327
- email: process.env[ENV_KEYS.JIRA_EMAIL] ?? globalConfig.jira?.email,
328
- apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken
329
- },
330
- bitbucket: {
331
- baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
332
- pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
333
- },
334
- defaults: mergedDefaults
335
- };
336
- }
337
- function writeGlobalConfig(config, configPath) {
338
- const filePath = getGlobalConfigPath(configPath);
339
- const dir = path.dirname(filePath);
340
- if (!fs.existsSync(dir)) {
341
- fs.mkdirSync(dir, { recursive: true });
342
- }
343
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
344
- }
345
- function writeRepoConfig(config) {
346
- const repoRoot = getRepoRoot2();
347
- const targetDir = repoRoot ?? process.cwd();
348
- fs.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
349
- }
350
- function setConfigValue(key, value, configPath) {
351
- const filePath = getGlobalConfigPath(configPath);
352
- const existing = loadJsonFile(filePath) ?? {};
353
- const parts = key.split(".");
354
- let current = existing;
355
- for (let i = 0; i < parts.length - 1; i++) {
356
- const part = parts[i];
357
- if (typeof current[part] !== "object" || current[part] === null) {
358
- current[part] = {};
359
- }
360
- current = current[part];
361
- }
362
- current[parts[parts.length - 1]] = value;
363
- writeGlobalConfig(existing, configPath);
364
- }
365
- function maskConfig(config) {
366
- return {
367
- ...config,
368
- jira: {
369
- ...config.jira,
370
- apiToken: config.jira.apiToken ? "***" : void 0
371
- },
372
- bitbucket: {
373
- ...config.bitbucket,
374
- pat: config.bitbucket.pat ? "***" : void 0
375
- }
376
- };
377
- }
378
-
379
396
  // src/lib/http.ts
380
397
  function buildUrl(base, path2, params) {
381
398
  const url = new URL(path2, base.endsWith("/") ? base : base + "/");
@@ -444,11 +461,10 @@ var HttpClient = class {
444
461
  this.dryRun = dryRun;
445
462
  }
446
463
  jiraHeaders() {
447
- const { email, apiToken } = this.config.jira;
448
- if (!email || !apiToken) throw new PncliError("Jira credentials not configured. Run: pncli config init");
449
- const token = Buffer.from(`${email}:${apiToken}`).toString("base64");
464
+ const { apiToken } = this.config.jira;
465
+ if (!apiToken) throw new PncliError("Jira credentials not configured. Run: pncli config init");
450
466
  return {
451
- "Authorization": `Basic ${token}`,
467
+ "Authorization": `Bearer ${apiToken}`,
452
468
  "Content-Type": "application/json",
453
469
  "Accept": "application/json"
454
470
  };
@@ -724,7 +740,7 @@ var BitbucketClient = class {
724
740
 
725
741
  // src/services/git/commands.ts
726
742
  function requireRepoRoot() {
727
- const root = getRepoRoot();
743
+ const root = getRepoRoot2();
728
744
  if (!root) throw new PncliError("Not a git repository", 1);
729
745
  return root;
730
746
  }
@@ -804,7 +820,7 @@ function registerGitCommands(program2) {
804
820
  }
805
821
 
806
822
  // src/services/jira/client.ts
807
- var API2 = "/rest/api/3";
823
+ var API2 = "/rest/api/2";
808
824
  var JiraClient = class {
809
825
  constructor(http) {
810
826
  this.http = http;
@@ -819,16 +835,11 @@ var JiraClient = class {
819
835
  project: { key: opts.project },
820
836
  issuetype: { name: opts.issueType },
821
837
  summary: opts.summary,
822
- ...opts.description ? {
823
- description: {
824
- type: "doc",
825
- version: 1,
826
- content: [{ type: "paragraph", content: [{ type: "text", text: opts.description }] }]
827
- }
828
- } : {},
838
+ ...opts.description ? { description: opts.description } : {},
829
839
  ...opts.priority ? { priority: { name: opts.priority } } : {},
830
- ...opts.assignee ? { assignee: { accountId: opts.assignee } } : {},
831
- ...opts.labels?.length ? { labels: opts.labels } : {}
840
+ ...opts.assignee ? { assignee: { name: opts.assignee } } : {},
841
+ ...opts.labels?.length ? { labels: opts.labels } : {},
842
+ ...opts.customFieldValues ?? {}
832
843
  }
833
844
  };
834
845
  const created = await this.http.jira(`${API2}/issue`, {
@@ -840,16 +851,11 @@ var JiraClient = class {
840
851
  async updateIssue(key, opts) {
841
852
  const fields = {};
842
853
  if (opts.summary) fields.summary = opts.summary;
843
- if (opts.description) {
844
- fields.description = {
845
- type: "doc",
846
- version: 1,
847
- content: [{ type: "paragraph", content: [{ type: "text", text: opts.description }] }]
848
- };
849
- }
854
+ if (opts.description) fields.description = opts.description;
850
855
  if (opts.priority) fields.priority = { name: opts.priority };
851
- if (opts.assignee) fields.assignee = { accountId: opts.assignee };
856
+ if (opts.assignee) fields.assignee = { name: opts.assignee };
852
857
  if (opts.labels) fields.labels = opts.labels;
858
+ Object.assign(fields, opts.customFieldValues ?? {});
853
859
  await this.http.jira(`${API2}/issue/${key}`, {
854
860
  method: "PUT",
855
861
  body: { fields }
@@ -870,13 +876,7 @@ var JiraClient = class {
870
876
  async addComment(key, text) {
871
877
  return this.http.jira(`${API2}/issue/${key}/comment`, {
872
878
  method: "POST",
873
- body: {
874
- body: {
875
- type: "doc",
876
- version: 1,
877
- content: [{ type: "paragraph", content: [{ type: "text", text }] }]
878
- }
879
- }
879
+ body: { body: text }
880
880
  });
881
881
  }
882
882
  async listComments(key) {
@@ -888,22 +888,27 @@ var JiraClient = class {
888
888
  return { ...result, values: result.comments };
889
889
  });
890
890
  }
891
- async search(jql, maxResults) {
891
+ async search(jql, maxResults, customFields) {
892
+ const standardFields = ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"];
893
+ const fields = customFields?.length ? [...standardFields, ...customFields.map((f) => f.id)] : standardFields;
892
894
  if (maxResults !== void 0) {
893
895
  return this.http.jira(`${API2}/search`, {
894
896
  method: "POST",
895
- body: { jql, maxResults, fields: ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"] }
897
+ body: { jql, maxResults, fields }
896
898
  });
897
899
  }
898
900
  const allIssues = await this.http.jiraPaginate(async (startAt, max) => {
899
901
  const result = await this.http.jira(`${API2}/search`, {
900
902
  method: "POST",
901
- body: { jql, startAt, maxResults: max, fields: ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"] }
903
+ body: { jql, startAt, maxResults: max, fields }
902
904
  });
903
905
  return { ...result, values: result.issues };
904
906
  });
905
907
  return { issues: allIssues, total: allIssues.length, startAt: 0, maxResults: allIssues.length };
906
908
  }
909
+ async fetchFields() {
910
+ return this.http.jira(`${API2}/field`);
911
+ }
907
912
  async assignIssue(key, accountId) {
908
913
  await this.http.jira(`${API2}/issue/${key}/assignee`, {
909
914
  method: "PUT",
@@ -933,6 +938,57 @@ var JiraClient = class {
933
938
  }
934
939
  };
935
940
 
941
+ // src/services/jira/custom-fields.ts
942
+ function buildFieldMap(fields) {
943
+ const byName = /* @__PURE__ */ new Map();
944
+ const byId = /* @__PURE__ */ new Map();
945
+ for (const f of fields) {
946
+ byName.set(f.name.toLowerCase(), f);
947
+ byId.set(f.id, f);
948
+ }
949
+ return { byName, byId };
950
+ }
951
+ function translateJql(jql, fieldMap) {
952
+ if (fieldMap.byName.size === 0) return jql;
953
+ const sortedNames = Array.from(fieldMap.byName.keys()).sort((a, b) => b.length - a.length);
954
+ const parts = jql.split(/(["'][^"']*["'])/);
955
+ return parts.map((part, i) => {
956
+ if (i % 2 === 1) return part;
957
+ let out = part;
958
+ for (const name of sortedNames) {
959
+ const def = fieldMap.byName.get(name);
960
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
961
+ out = out.replace(new RegExp(`(?<![\\w"'])${escaped}(?![\\w"'])`, "gi"), def.id);
962
+ }
963
+ return out;
964
+ }).join("");
965
+ }
966
+ function translateFieldsInOutput(fields, fieldMap) {
967
+ if (fieldMap.byId.size === 0) return fields;
968
+ const result = {};
969
+ for (const [key, value] of Object.entries(fields)) {
970
+ const def = fieldMap.byId.get(key);
971
+ result[def ? def.name : key] = value;
972
+ }
973
+ return result;
974
+ }
975
+ function formatFieldValue(value, type) {
976
+ switch (type) {
977
+ case "number":
978
+ return Number(value);
979
+ case "select":
980
+ return { value };
981
+ case "multi-select":
982
+ return value.split(",").map((v) => ({ value: v.trim() }));
983
+ case "labels":
984
+ return value.split(",").map((v) => v.trim());
985
+ case "user":
986
+ return { accountId: value };
987
+ default:
988
+ return value;
989
+ }
990
+ }
991
+
936
992
  // src/services/jira/commands.ts
937
993
  function getClient(program2) {
938
994
  const opts = program2.optsWithGlobals();
@@ -941,6 +997,16 @@ function getClient(program2) {
941
997
  const http = createHttpClient(config, Boolean(opts.dryRun));
942
998
  return new JiraClient(http);
943
999
  }
1000
+ function getClientAndFields(program2) {
1001
+ const opts = program2.optsWithGlobals();
1002
+ const config = loadConfig({ configPath: opts.config });
1003
+ if (!config.jira.baseUrl) throw new PncliError("Jira not configured. Run: pncli config init");
1004
+ const http = createHttpClient(config, Boolean(opts.dryRun));
1005
+ const client = new JiraClient(http);
1006
+ const customFields = config.jira.customFields;
1007
+ const fieldMap = buildFieldMap(customFields);
1008
+ return { client, fieldMap, customFields };
1009
+ }
944
1010
  function getDefaults(program2) {
945
1011
  const opts = program2.optsWithGlobals();
946
1012
  const config = loadConfig({ configPath: opts.config });
@@ -955,35 +1021,38 @@ function registerJiraCommands(program2) {
955
1021
  jira.command("get-issue").description("Get a Jira issue by key").requiredOption("--key <issue-key>", "Issue key (e.g. PROJ-123)").action(async (opts) => {
956
1022
  const start = Date.now();
957
1023
  try {
958
- const client = getClient(program2);
1024
+ const { client, fieldMap } = getClientAndFields(program2);
959
1025
  const data = await client.getIssue(opts.key);
960
- success(data, "jira", "get-issue", start);
1026
+ const translated = { ...data, fields: translateFieldsInOutput(data.fields, fieldMap) };
1027
+ success(translated, "jira", "get-issue", start);
961
1028
  } catch (err) {
962
1029
  fail(err, "jira", "get-issue", start);
963
1030
  }
964
1031
  });
965
- 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").action(async (opts) => {
1032
+ 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("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
966
1033
  const start = Date.now();
967
1034
  try {
968
- const client = getClient(program2);
1035
+ const { client, fieldMap } = getClientAndFields(program2);
969
1036
  const defaults = getDefaults(program2);
970
1037
  const project = opts.project ?? defaults.project;
971
1038
  const issueType = opts.type ?? defaults.issueType ?? "Task";
972
1039
  const priority = opts.priority ?? defaults.priority;
973
1040
  if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
974
1041
  const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
975
- const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels });
1042
+ const customFieldValues = parseFieldArgs(opts.field, fieldMap);
1043
+ const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, customFieldValues });
976
1044
  success(data, "jira", "create-issue", start);
977
1045
  } catch (err) {
978
1046
  fail(err, "jira", "create-issue", start);
979
1047
  }
980
1048
  });
981
- 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").action(async (opts) => {
1049
+ 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) => {
982
1050
  const start = Date.now();
983
1051
  try {
984
- const client = getClient(program2);
1052
+ const { client, fieldMap } = getClientAndFields(program2);
985
1053
  const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
986
- await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels });
1054
+ const customFieldValues = parseFieldArgs(opts.field, fieldMap);
1055
+ await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
987
1056
  success({ updated: opts.key }, "jira", "update-issue", start);
988
1057
  } catch (err) {
989
1058
  fail(err, "jira", "update-issue", start);
@@ -1039,10 +1108,15 @@ function registerJiraCommands(program2) {
1039
1108
  jira.command("search").description("Search Jira issues with JQL").requiredOption("--jql <query>", "JQL query string").option("--max-results <n>", "Maximum number of results").action(async (opts) => {
1040
1109
  const start = Date.now();
1041
1110
  try {
1042
- const client = getClient(program2);
1111
+ const { client, fieldMap, customFields } = getClientAndFields(program2);
1043
1112
  const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : void 0;
1044
- const data = await client.search(opts.jql, maxResults);
1045
- success(data, "jira", "search", start);
1113
+ const translatedJql = translateJql(opts.jql, fieldMap);
1114
+ const data = await client.search(translatedJql, maxResults, customFields);
1115
+ const translatedIssues = data.issues.map((issue) => ({
1116
+ ...issue,
1117
+ fields: translateFieldsInOutput(issue.fields, fieldMap)
1118
+ }));
1119
+ success({ ...data, issues: translatedIssues }, "jira", "search", start);
1046
1120
  } catch (err) {
1047
1121
  fail(err, "jira", "search", start);
1048
1122
  }
@@ -1067,6 +1141,35 @@ function registerJiraCommands(program2) {
1067
1141
  fail(err, "jira", "link-issue", start);
1068
1142
  }
1069
1143
  });
1144
+ jira.command("fields").description("List custom fields (configured or discovered from Jira API)").option("--discover", "Fetch field metadata from Jira API").option("--custom-only", "Show only custom fields (requires --discover)").action(async (opts) => {
1145
+ const start = Date.now();
1146
+ try {
1147
+ if (opts.discover) {
1148
+ const client = getClient(program2);
1149
+ const fields = await client.fetchFields();
1150
+ const result = opts.customOnly ? fields.filter((f) => f.custom) : fields;
1151
+ success(result, "jira", "fields", start);
1152
+ } else {
1153
+ const { customFields } = getClientAndFields(program2);
1154
+ success(customFields, "jira", "fields", start);
1155
+ }
1156
+ } catch (err) {
1157
+ fail(err, "jira", "fields", start);
1158
+ }
1159
+ });
1160
+ }
1161
+ function parseFieldArgs(fieldArgs, fieldMap) {
1162
+ const result = {};
1163
+ for (const arg of fieldArgs) {
1164
+ const eq = arg.indexOf("=");
1165
+ if (eq === -1) throw new PncliError(`Invalid --field format (expected Name=value): ${arg}`, 1);
1166
+ const name = arg.slice(0, eq).trim();
1167
+ const value = arg.slice(eq + 1);
1168
+ const def = fieldMap.byName.get(name.toLowerCase());
1169
+ if (!def) throw new PncliError(`Unknown custom field: "${name}". Run: pncli jira fields`, 1);
1170
+ result[def.id] = formatFieldValue(value, def.type);
1171
+ }
1172
+ return result;
1070
1173
  }
1071
1174
 
1072
1175
  // src/services/bitbucket/commands.ts
@@ -1397,17 +1500,24 @@ function registerConfigCommands(program2) {
1397
1500
  }
1398
1501
  async function initGlobalConfig(start) {
1399
1502
  process.stderr.write("pncli config init \u2014 Global configuration\n\n");
1400
- const jiraBaseUrl = await input({
1401
- message: "Jira base URL (e.g. https://your-domain.atlassian.net):",
1503
+ process.stderr.write("\u2500\u2500 Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1504
+ const userEmail = await input({
1505
+ message: "Your email address (used across Jira, Bitbucket, etc.):",
1402
1506
  default: ""
1403
1507
  });
1404
- const jiraEmail = await input({
1405
- message: "Jira email address:",
1508
+ const userId = await input({
1509
+ message: "Your username / user ID:",
1510
+ default: ""
1511
+ });
1512
+ process.stderr.write("\n\u2500\u2500 Jira \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1513
+ const jiraBaseUrl = await input({
1514
+ message: "Jira base URL (e.g. https://jira.your-company.com):",
1406
1515
  default: ""
1407
1516
  });
1408
1517
  const jiraApiToken = await password({
1409
- message: "Jira API token:"
1518
+ message: "Jira personal access token:"
1410
1519
  });
1520
+ process.stderr.write("\n\u2500\u2500 Bitbucket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1411
1521
  const bitbucketBaseUrl = await input({
1412
1522
  message: "Bitbucket Server base URL (e.g. https://bitbucket.your-company.com):",
1413
1523
  default: ""
@@ -1415,10 +1525,12 @@ async function initGlobalConfig(start) {
1415
1525
  const bitbucketPat = await password({
1416
1526
  message: "Bitbucket personal access token:"
1417
1527
  });
1528
+ process.stderr.write("\n\u2500\u2500 Defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
1418
1529
  const jiraProject = await input({
1419
1530
  message: "Default Jira project key (optional):",
1420
1531
  default: ""
1421
1532
  });
1533
+ process.stderr.write("\n");
1422
1534
  const confirmed = await confirm({
1423
1535
  message: "Write config to ~/.pncli/config.json?",
1424
1536
  default: true
@@ -1428,9 +1540,12 @@ async function initGlobalConfig(start) {
1428
1540
  process.exit(0);
1429
1541
  }
1430
1542
  writeGlobalConfig({
1543
+ user: {
1544
+ email: userEmail || void 0,
1545
+ userId: userId || void 0
1546
+ },
1431
1547
  jira: {
1432
1548
  baseUrl: jiraBaseUrl || void 0,
1433
- email: jiraEmail || void 0,
1434
1549
  apiToken: jiraApiToken || void 0
1435
1550
  },
1436
1551
  bitbucket: {
@@ -1510,6 +1625,11 @@ program.hook("preAction", (thisCommand) => {
1510
1625
  pretty: Boolean(opts.pretty),
1511
1626
  verbose: Boolean(opts.verbose)
1512
1627
  });
1628
+ try {
1629
+ const config = loadConfig({ configPath: opts.config });
1630
+ setGlobalUser(config.user);
1631
+ } catch {
1632
+ }
1513
1633
  });
1514
1634
  registerGitCommands(program);
1515
1635
  registerJiraCommands(program);