@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/NOTICE +377 -50
- package/copilot-instructions.md +173 -49
- package/dist/cli.js +283 -163
- package/dist/cli.js.map +1 -1
- package/package.json +4 -2
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
|
|
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
|
|
213
|
-
function
|
|
333
|
+
import { execSync as execSync3 } from "child_process";
|
|
334
|
+
function getRepoRoot2() {
|
|
214
335
|
try {
|
|
215
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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 {
|
|
448
|
-
if (!
|
|
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": `
|
|
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 =
|
|
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/
|
|
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: {
|
|
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 = {
|
|
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
|
|
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
|
|
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 =
|
|
1024
|
+
const { client, fieldMap } = getClientAndFields(program2);
|
|
959
1025
|
const data = await client.getIssue(opts.key);
|
|
960
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
1052
|
+
const { client, fieldMap } = getClientAndFields(program2);
|
|
985
1053
|
const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
|
|
986
|
-
|
|
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 =
|
|
1111
|
+
const { client, fieldMap, customFields } = getClientAndFields(program2);
|
|
1043
1112
|
const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : void 0;
|
|
1044
|
-
const
|
|
1045
|
-
|
|
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
|
-
|
|
1401
|
-
|
|
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
|
|
1405
|
-
message: "
|
|
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
|
|
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);
|