@kolatts/pncli 1.0.1 → 1.2.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,17 +19,40 @@ var PncliError = class extends Error {
19
19
  }
20
20
  };
21
21
 
22
+ // src/lib/exitCodes.ts
23
+ var ExitCode = {
24
+ SUCCESS: 0,
25
+ GENERAL_ERROR: 1,
26
+ USAGE_ERROR: 2,
27
+ NETWORK_ERROR: 69,
28
+ // EX_UNAVAILABLE from sysexits.h
29
+ AUTH_ERROR: 77,
30
+ // EX_NOPERM from sysexits.h
31
+ CONFIG_ERROR: 78
32
+ // EX_CONFIG from sysexits.h
33
+ };
34
+ function exitCodeFromStatus(httpStatus) {
35
+ if (httpStatus === 401 || httpStatus === 403) return ExitCode.AUTH_ERROR;
36
+ if (httpStatus === 0) return ExitCode.NETWORK_ERROR;
37
+ return ExitCode.GENERAL_ERROR;
38
+ }
39
+
22
40
  // src/lib/output.ts
23
41
  var globalOptions = { pretty: false, verbose: false };
42
+ var globalUser = { email: void 0, userId: void 0 };
24
43
  function setGlobalOptions(opts) {
25
44
  globalOptions = opts;
26
45
  }
46
+ function setGlobalUser(user) {
47
+ globalUser = user;
48
+ }
27
49
  function buildMeta(service, action, startTime) {
28
50
  return {
29
51
  service,
30
52
  action,
31
53
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
32
- duration_ms: Date.now() - startTime
54
+ duration_ms: Date.now() - startTime,
55
+ user: globalUser.email ?? globalUser.userId ? globalUser : void 0
33
56
  };
34
57
  }
35
58
  function success(data, service, action, startTime) {
@@ -55,10 +78,10 @@ function fail(err, service, action, startTime) {
55
78
  };
56
79
  const msg = globalOptions.pretty ? chalk.red("\u2717 Error: ") + errorDetail.message : null;
57
80
  if (msg) process.stderr.write(msg + "\n");
58
- process.stdout.write(
59
- (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n"
60
- );
61
- process.exit(1);
81
+ const output = (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n";
82
+ const exitCode = err instanceof PncliError ? exitCodeFromStatus(err.status) : ExitCode.GENERAL_ERROR;
83
+ process.stdout.write(output, () => process.exit(exitCode));
84
+ throw new PncliError(errorDetail.message, errorDetail.status);
62
85
  }
63
86
  function log(message) {
64
87
  if (globalOptions.verbose) {
@@ -73,11 +96,143 @@ function warn(message) {
73
96
  );
74
97
  }
75
98
 
99
+ // src/lib/config.ts
100
+ import fs from "fs";
101
+ import os from "os";
102
+ import path from "path";
103
+ import { execSync } from "child_process";
104
+ var ENV_KEYS = {
105
+ EMAIL: "PNCLI_EMAIL",
106
+ USERID: "PNCLI_USERID",
107
+ JIRA_BASE_URL: "PNCLI_JIRA_BASE_URL",
108
+ JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
109
+ BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
110
+ BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
111
+ ARTIFACTORY_BASE_URL: "PNCLI_ARTIFACTORY_BASE_URL",
112
+ ARTIFACTORY_TOKEN: "PNCLI_ARTIFACTORY_TOKEN",
113
+ ARTIFACTORY_REPO_NPM: "PNCLI_ARTIFACTORY_REPO_NPM",
114
+ ARTIFACTORY_REPO_NUGET: "PNCLI_ARTIFACTORY_REPO_NUGET",
115
+ ARTIFACTORY_REPO_MAVEN: "PNCLI_ARTIFACTORY_REPO_MAVEN",
116
+ CONFIG_PATH: "PNCLI_CONFIG_PATH"
117
+ };
118
+ function getGlobalConfigPath(overridePath) {
119
+ if (overridePath) return overridePath;
120
+ const envPath = process.env[ENV_KEYS.CONFIG_PATH];
121
+ if (envPath) return envPath;
122
+ return path.join(os.homedir(), ".pncli", "config.json");
123
+ }
124
+ function loadJsonFile(filePath) {
125
+ try {
126
+ const content = fs.readFileSync(filePath, "utf8");
127
+ return JSON.parse(content);
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ function getRepoRoot() {
133
+ try {
134
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ function mergeCustomFields(global, repo) {
140
+ const map = /* @__PURE__ */ new Map();
141
+ for (const f of global ?? []) map.set(f.id, f);
142
+ for (const f of repo ?? []) map.set(f.id, f);
143
+ return Array.from(map.values());
144
+ }
145
+ function mergeDefaults(global, repo) {
146
+ return {
147
+ jira: { ...global?.jira, ...repo?.jira },
148
+ bitbucket: { ...global?.bitbucket, ...repo?.bitbucket }
149
+ };
150
+ }
151
+ function loadConfig(opts = {}) {
152
+ const globalConfigPath = getGlobalConfigPath(opts.configPath);
153
+ const globalConfig = loadJsonFile(globalConfigPath) ?? {};
154
+ const repoRoot = getRepoRoot();
155
+ let repoConfig = {};
156
+ if (repoRoot) {
157
+ repoConfig = loadJsonFile(path.join(repoRoot, ".pncli.json")) ?? {};
158
+ }
159
+ const mergedDefaults = mergeDefaults(globalConfig.defaults, repoConfig.defaults);
160
+ return {
161
+ user: {
162
+ email: process.env[ENV_KEYS.EMAIL] ?? globalConfig.user?.email,
163
+ userId: process.env[ENV_KEYS.USERID] ?? globalConfig.user?.userId
164
+ },
165
+ jira: {
166
+ baseUrl: process.env[ENV_KEYS.JIRA_BASE_URL] ?? globalConfig.jira?.baseUrl,
167
+ apiToken: process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken,
168
+ customFields: mergeCustomFields(globalConfig.jira?.customFields, repoConfig.jira?.customFields)
169
+ },
170
+ bitbucket: {
171
+ baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
172
+ pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
173
+ },
174
+ artifactory: {
175
+ baseUrl: process.env[ENV_KEYS.ARTIFACTORY_BASE_URL] ?? globalConfig.artifactory?.baseUrl,
176
+ token: process.env[ENV_KEYS.ARTIFACTORY_TOKEN] ?? globalConfig.artifactory?.token,
177
+ npmRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NPM] ?? globalConfig.artifactory?.npmRepo,
178
+ nugetRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NUGET] ?? globalConfig.artifactory?.nugetRepo,
179
+ mavenRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_MAVEN] ?? globalConfig.artifactory?.mavenRepo
180
+ },
181
+ defaults: mergedDefaults
182
+ };
183
+ }
184
+ function writeGlobalConfig(config, configPath) {
185
+ const filePath = getGlobalConfigPath(configPath);
186
+ const dir = path.dirname(filePath);
187
+ if (!fs.existsSync(dir)) {
188
+ fs.mkdirSync(dir, { recursive: true });
189
+ }
190
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
191
+ }
192
+ function writeRepoConfig(config) {
193
+ const repoRoot = getRepoRoot();
194
+ const targetDir = repoRoot ?? process.cwd();
195
+ fs.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
196
+ }
197
+ function setConfigValue(key, value, configPath) {
198
+ const filePath = getGlobalConfigPath(configPath);
199
+ const existing = loadJsonFile(filePath) ?? {};
200
+ const parts = key.split(".");
201
+ let current = existing;
202
+ for (let i = 0; i < parts.length - 1; i++) {
203
+ const part = parts[i];
204
+ if (typeof current[part] !== "object" || current[part] === null) {
205
+ current[part] = {};
206
+ }
207
+ current = current[part];
208
+ }
209
+ current[parts[parts.length - 1]] = value;
210
+ writeGlobalConfig(existing, configPath);
211
+ }
212
+ function maskConfig(config) {
213
+ return {
214
+ ...config,
215
+ jira: {
216
+ ...config.jira,
217
+ apiToken: config.jira.apiToken ? "***" : void 0,
218
+ customFields: config.jira.customFields
219
+ },
220
+ bitbucket: {
221
+ ...config.bitbucket,
222
+ pat: config.bitbucket.pat ? "***" : void 0
223
+ },
224
+ artifactory: {
225
+ ...config.artifactory,
226
+ token: config.artifactory.token ? "***" : void 0
227
+ }
228
+ };
229
+ }
230
+
76
231
  // src/services/git/client.ts
77
- import { execSync, execFileSync } from "child_process";
232
+ import { execSync as execSync2, execFileSync } from "child_process";
78
233
  var DIFF_LINE_LIMIT = 5e3;
79
234
  function exec(cmd, cwd) {
80
- return execSync(cmd, { encoding: "utf8", cwd }).trim();
235
+ return execSync2(cmd, { encoding: "utf8", cwd }).trim();
81
236
  }
82
237
  function getStatus(root) {
83
238
  const output = exec("git status --porcelain", root);
@@ -209,17 +364,17 @@ function getBranches(root) {
209
364
  }
210
365
 
211
366
  // src/lib/git-context.ts
212
- import { execSync as execSync2 } from "child_process";
213
- function getRepoRoot() {
367
+ import { execSync as execSync3 } from "child_process";
368
+ function getRepoRoot2() {
214
369
  try {
215
- return execSync2("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
370
+ return execSync3("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
216
371
  } catch {
217
372
  return null;
218
373
  }
219
374
  }
220
375
  function getCurrentBranch(repoRoot) {
221
376
  try {
222
- return execSync2("git rev-parse --abbrev-ref HEAD", {
377
+ return execSync3("git rev-parse --abbrev-ref HEAD", {
223
378
  encoding: "utf8",
224
379
  cwd: repoRoot
225
380
  }).trim();
@@ -248,14 +403,14 @@ function parseRemote(remoteUrl, bitbucketBaseUrl) {
248
403
  }
249
404
  function getRemoteUrls(repoRoot) {
250
405
  try {
251
- const output = execSync2("git remote -v", { encoding: "utf8", cwd: repoRoot });
406
+ const output = execSync3("git remote -v", { encoding: "utf8", cwd: repoRoot });
252
407
  return output.split("\n").filter((line) => line.includes("(fetch)")).map((line) => line.split(" ")[1]?.split(" ")[0] ?? "").filter(Boolean);
253
408
  } catch {
254
409
  return [];
255
410
  }
256
411
  }
257
412
  function getGitContext(config) {
258
- const root = getRepoRoot();
413
+ const root = getRepoRoot2();
259
414
  if (!root) return null;
260
415
  const branch = getCurrentBranch(root) ?? "unknown";
261
416
  const remoteUrls = getRemoteUrls(root);
@@ -272,113 +427,9 @@ function getGitContext(config) {
272
427
  return { root, branch, project, repo };
273
428
  }
274
429
 
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
430
  // src/lib/http.ts
380
- function buildUrl(base, path2, params) {
381
- const url = new URL(path2, base.endsWith("/") ? base : base + "/");
431
+ function buildUrl(base, path6, params) {
432
+ const url = new URL(path6, base.endsWith("/") ? base : base + "/");
382
433
  if (params) {
383
434
  for (const [key, val] of Object.entries(params)) {
384
435
  if (val !== void 0) {
@@ -423,8 +474,24 @@ async function request(url, init, timeoutMs) {
423
474
  try {
424
475
  const body = await response.text();
425
476
  const parsed = JSON.parse(body);
426
- if (parsed.message) message = parsed.message;
427
- else if (parsed.errors?.[0]?.message) message = parsed.errors[0].message;
477
+ const parts = [];
478
+ if (Array.isArray(parsed.errorMessages)) {
479
+ parts.push(...parsed.errorMessages.filter(Boolean));
480
+ }
481
+ if (parsed.errors && typeof parsed.errors === "object" && !Array.isArray(parsed.errors)) {
482
+ for (const [field, msg] of Object.entries(parsed.errors)) {
483
+ parts.push(`${field}: ${msg}`);
484
+ }
485
+ }
486
+ if (Array.isArray(parsed.errors)) {
487
+ for (const e of parsed.errors) {
488
+ if (e?.message) parts.push(e.message);
489
+ }
490
+ }
491
+ if (parts.length === 0 && parsed.message) {
492
+ parts.push(String(parsed.message));
493
+ }
494
+ if (parts.length > 0) message = parts.join("; ");
428
495
  } catch {
429
496
  }
430
497
  throw new PncliError(message, response.status, url);
@@ -444,13 +511,13 @@ var HttpClient = class {
444
511
  this.dryRun = dryRun;
445
512
  }
446
513
  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");
514
+ const { apiToken } = this.config.jira;
515
+ if (!apiToken) throw new PncliError("Jira credentials not configured. Run: pncli config init");
450
516
  return {
451
- "Authorization": `Basic ${token}`,
517
+ "Authorization": `Bearer ${apiToken}`,
452
518
  "Content-Type": "application/json",
453
- "Accept": "application/json"
519
+ "Accept": "application/json",
520
+ "Connection": "close"
454
521
  };
455
522
  }
456
523
  bitbucketHeaders() {
@@ -459,13 +526,14 @@ var HttpClient = class {
459
526
  return {
460
527
  "Authorization": `Bearer ${pat}`,
461
528
  "Content-Type": "application/json",
462
- "Accept": "application/json"
529
+ "Accept": "application/json",
530
+ "Connection": "close"
463
531
  };
464
532
  }
465
- async jira(path2, opts = {}) {
533
+ async jira(path6, opts = {}) {
466
534
  const baseUrl = this.config.jira.baseUrl;
467
535
  if (!baseUrl) throw new PncliError("Jira baseUrl not configured. Run: pncli config init");
468
- const url = buildUrl(baseUrl, path2, opts.params);
536
+ const url = buildUrl(baseUrl, path6, opts.params);
469
537
  const headers = this.jiraHeaders();
470
538
  const init = {
471
539
  method: opts.method ?? "GET",
@@ -474,19 +542,20 @@ var HttpClient = class {
474
542
  };
475
543
  if (this.dryRun) {
476
544
  const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
477
- process.stderr.write(`DRY RUN: ${init.method} ${url}
545
+ const msg = `DRY RUN: ${init.method} ${url}
478
546
  Headers: ${JSON.stringify(safeHeaders, null, 2)}
479
- `);
480
- if (opts.body) process.stderr.write(`Body: ${JSON.stringify(opts.body, null, 2)}
481
- `);
482
- process.exit(0);
547
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
548
+ ` : "");
549
+ process.stderr.write(msg, () => process.exit(ExitCode.SUCCESS));
550
+ return new Promise(() => {
551
+ });
483
552
  }
484
553
  return request(url, init, opts.timeoutMs ?? 3e4);
485
554
  }
486
- async bitbucket(path2, opts = {}) {
555
+ async bitbucket(path6, opts = {}) {
487
556
  const baseUrl = this.config.bitbucket.baseUrl;
488
557
  if (!baseUrl) throw new PncliError("Bitbucket baseUrl not configured. Run: pncli config init");
489
- const url = buildUrl(baseUrl, path2, opts.params);
558
+ const url = buildUrl(baseUrl, path6, opts.params);
490
559
  const headers = this.bitbucketHeaders();
491
560
  const init = {
492
561
  method: opts.method ?? "GET",
@@ -495,12 +564,13 @@ Headers: ${JSON.stringify(safeHeaders, null, 2)}
495
564
  };
496
565
  if (this.dryRun) {
497
566
  const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
498
- process.stderr.write(`DRY RUN: ${init.method} ${url}
567
+ const msg = `DRY RUN: ${init.method} ${url}
499
568
  Headers: ${JSON.stringify(safeHeaders, null, 2)}
500
- `);
501
- if (opts.body) process.stderr.write(`Body: ${JSON.stringify(opts.body, null, 2)}
502
- `);
503
- process.exit(0);
569
+ ` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
570
+ ` : "");
571
+ process.stderr.write(msg, () => process.exit(ExitCode.SUCCESS));
572
+ return new Promise(() => {
573
+ });
504
574
  }
505
575
  return request(url, init, opts.timeoutMs ?? 3e4);
506
576
  }
@@ -724,7 +794,7 @@ var BitbucketClient = class {
724
794
 
725
795
  // src/services/git/commands.ts
726
796
  function requireRepoRoot() {
727
- const root = getRepoRoot();
797
+ const root = getRepoRoot2();
728
798
  if (!root) throw new PncliError("Not a git repository", 1);
729
799
  return root;
730
800
  }
@@ -804,7 +874,7 @@ function registerGitCommands(program2) {
804
874
  }
805
875
 
806
876
  // src/services/jira/client.ts
807
- var API2 = "/rest/api/3";
877
+ var API2 = "/rest/api/2";
808
878
  var JiraClient = class {
809
879
  constructor(http) {
810
880
  this.http = http;
@@ -819,16 +889,11 @@ var JiraClient = class {
819
889
  project: { key: opts.project },
820
890
  issuetype: { name: opts.issueType },
821
891
  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
- } : {},
892
+ ...opts.description ? { description: opts.description } : {},
829
893
  ...opts.priority ? { priority: { name: opts.priority } } : {},
830
- ...opts.assignee ? { assignee: { accountId: opts.assignee } } : {},
831
- ...opts.labels?.length ? { labels: opts.labels } : {}
894
+ ...opts.assignee ? { assignee: { name: opts.assignee } } : {},
895
+ ...opts.labels?.length ? { labels: opts.labels } : {},
896
+ ...opts.customFieldValues ?? {}
832
897
  }
833
898
  };
834
899
  const created = await this.http.jira(`${API2}/issue`, {
@@ -840,16 +905,11 @@ var JiraClient = class {
840
905
  async updateIssue(key, opts) {
841
906
  const fields = {};
842
907
  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
- }
908
+ if (opts.description) fields.description = opts.description;
850
909
  if (opts.priority) fields.priority = { name: opts.priority };
851
- if (opts.assignee) fields.assignee = { accountId: opts.assignee };
910
+ if (opts.assignee) fields.assignee = { name: opts.assignee };
852
911
  if (opts.labels) fields.labels = opts.labels;
912
+ Object.assign(fields, opts.customFieldValues ?? {});
853
913
  await this.http.jira(`${API2}/issue/${key}`, {
854
914
  method: "PUT",
855
915
  body: { fields }
@@ -870,13 +930,7 @@ var JiraClient = class {
870
930
  async addComment(key, text) {
871
931
  return this.http.jira(`${API2}/issue/${key}/comment`, {
872
932
  method: "POST",
873
- body: {
874
- body: {
875
- type: "doc",
876
- version: 1,
877
- content: [{ type: "paragraph", content: [{ type: "text", text }] }]
878
- }
879
- }
933
+ body: { body: text }
880
934
  });
881
935
  }
882
936
  async listComments(key) {
@@ -888,22 +942,27 @@ var JiraClient = class {
888
942
  return { ...result, values: result.comments };
889
943
  });
890
944
  }
891
- async search(jql, maxResults) {
945
+ async search(jql, maxResults, customFields) {
946
+ const standardFields = ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"];
947
+ const fields = customFields?.length ? [...standardFields, ...customFields.map((f) => f.id)] : standardFields;
892
948
  if (maxResults !== void 0) {
893
949
  return this.http.jira(`${API2}/search`, {
894
950
  method: "POST",
895
- body: { jql, maxResults, fields: ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"] }
951
+ body: { jql, maxResults, fields }
896
952
  });
897
953
  }
898
954
  const allIssues = await this.http.jiraPaginate(async (startAt, max) => {
899
955
  const result = await this.http.jira(`${API2}/search`, {
900
956
  method: "POST",
901
- body: { jql, startAt, maxResults: max, fields: ["summary", "status", "priority", "assignee", "issuetype", "project", "created", "updated", "labels", "reporter"] }
957
+ body: { jql, startAt, maxResults: max, fields }
902
958
  });
903
959
  return { ...result, values: result.issues };
904
960
  });
905
961
  return { issues: allIssues, total: allIssues.length, startAt: 0, maxResults: allIssues.length };
906
962
  }
963
+ async fetchFields() {
964
+ return this.http.jira(`${API2}/field`);
965
+ }
907
966
  async assignIssue(key, accountId) {
908
967
  await this.http.jira(`${API2}/issue/${key}/assignee`, {
909
968
  method: "PUT",
@@ -933,6 +992,57 @@ var JiraClient = class {
933
992
  }
934
993
  };
935
994
 
995
+ // src/services/jira/custom-fields.ts
996
+ function buildFieldMap(fields) {
997
+ const byName = /* @__PURE__ */ new Map();
998
+ const byId = /* @__PURE__ */ new Map();
999
+ for (const f of fields) {
1000
+ byName.set(f.name.toLowerCase(), f);
1001
+ byId.set(f.id, f);
1002
+ }
1003
+ return { byName, byId };
1004
+ }
1005
+ function translateJql(jql, fieldMap) {
1006
+ if (fieldMap.byName.size === 0) return jql;
1007
+ const sortedNames = Array.from(fieldMap.byName.keys()).sort((a, b) => b.length - a.length);
1008
+ const parts = jql.split(/(["'][^"']*["'])/);
1009
+ return parts.map((part, i) => {
1010
+ if (i % 2 === 1) return part;
1011
+ let out = part;
1012
+ for (const name of sortedNames) {
1013
+ const def = fieldMap.byName.get(name);
1014
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1015
+ out = out.replace(new RegExp(`(?<![\\w"'])${escaped}(?![\\w"'])`, "gi"), def.id);
1016
+ }
1017
+ return out;
1018
+ }).join("");
1019
+ }
1020
+ function translateFieldsInOutput(fields, fieldMap) {
1021
+ if (fieldMap.byId.size === 0) return fields;
1022
+ const result = {};
1023
+ for (const [key, value] of Object.entries(fields)) {
1024
+ const def = fieldMap.byId.get(key);
1025
+ result[def ? def.name : key] = value;
1026
+ }
1027
+ return result;
1028
+ }
1029
+ function formatFieldValue(value, type) {
1030
+ switch (type) {
1031
+ case "number":
1032
+ return Number(value);
1033
+ case "select":
1034
+ return { value };
1035
+ case "multi-select":
1036
+ return value.split(",").map((v) => ({ value: v.trim() }));
1037
+ case "labels":
1038
+ return value.split(",").map((v) => v.trim());
1039
+ case "user":
1040
+ return { accountId: value };
1041
+ default:
1042
+ return value;
1043
+ }
1044
+ }
1045
+
936
1046
  // src/services/jira/commands.ts
937
1047
  function getClient(program2) {
938
1048
  const opts = program2.optsWithGlobals();
@@ -941,6 +1051,16 @@ function getClient(program2) {
941
1051
  const http = createHttpClient(config, Boolean(opts.dryRun));
942
1052
  return new JiraClient(http);
943
1053
  }
1054
+ function getClientAndFields(program2) {
1055
+ const opts = program2.optsWithGlobals();
1056
+ const config = loadConfig({ configPath: opts.config });
1057
+ if (!config.jira.baseUrl) throw new PncliError("Jira not configured. Run: pncli config init");
1058
+ const http = createHttpClient(config, Boolean(opts.dryRun));
1059
+ const client = new JiraClient(http);
1060
+ const customFields = config.jira.customFields;
1061
+ const fieldMap = buildFieldMap(customFields);
1062
+ return { client, fieldMap, customFields };
1063
+ }
944
1064
  function getDefaults(program2) {
945
1065
  const opts = program2.optsWithGlobals();
946
1066
  const config = loadConfig({ configPath: opts.config });
@@ -955,35 +1075,38 @@ function registerJiraCommands(program2) {
955
1075
  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
1076
  const start = Date.now();
957
1077
  try {
958
- const client = getClient(program2);
1078
+ const { client, fieldMap } = getClientAndFields(program2);
959
1079
  const data = await client.getIssue(opts.key);
960
- success(data, "jira", "get-issue", start);
1080
+ const translated = { ...data, fields: translateFieldsInOutput(data.fields, fieldMap) };
1081
+ success(translated, "jira", "get-issue", start);
961
1082
  } catch (err) {
962
1083
  fail(err, "jira", "get-issue", start);
963
1084
  }
964
1085
  });
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) => {
1086
+ 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
1087
  const start = Date.now();
967
1088
  try {
968
- const client = getClient(program2);
1089
+ const { client, fieldMap } = getClientAndFields(program2);
969
1090
  const defaults = getDefaults(program2);
970
1091
  const project = opts.project ?? defaults.project;
971
1092
  const issueType = opts.type ?? defaults.issueType ?? "Task";
972
1093
  const priority = opts.priority ?? defaults.priority;
973
1094
  if (!project) throw new PncliError("--project required (or set defaults.jira.project in config)", 1);
974
1095
  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 });
1096
+ const customFieldValues = parseFieldArgs(opts.field, fieldMap);
1097
+ const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, customFieldValues });
976
1098
  success(data, "jira", "create-issue", start);
977
1099
  } catch (err) {
978
1100
  fail(err, "jira", "create-issue", start);
979
1101
  }
980
1102
  });
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) => {
1103
+ 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
1104
  const start = Date.now();
983
1105
  try {
984
- const client = getClient(program2);
1106
+ const { client, fieldMap } = getClientAndFields(program2);
985
1107
  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 });
1108
+ const customFieldValues = parseFieldArgs(opts.field, fieldMap);
1109
+ await client.updateIssue(opts.key, { summary: opts.summary, description: opts.description, priority: opts.priority, assignee: opts.assignee, labels, customFieldValues });
987
1110
  success({ updated: opts.key }, "jira", "update-issue", start);
988
1111
  } catch (err) {
989
1112
  fail(err, "jira", "update-issue", start);
@@ -1039,10 +1162,15 @@ function registerJiraCommands(program2) {
1039
1162
  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
1163
  const start = Date.now();
1041
1164
  try {
1042
- const client = getClient(program2);
1165
+ const { client, fieldMap, customFields } = getClientAndFields(program2);
1043
1166
  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);
1167
+ const translatedJql = translateJql(opts.jql, fieldMap);
1168
+ const data = await client.search(translatedJql, maxResults, customFields);
1169
+ const translatedIssues = data.issues.map((issue) => ({
1170
+ ...issue,
1171
+ fields: translateFieldsInOutput(issue.fields, fieldMap)
1172
+ }));
1173
+ success({ ...data, issues: translatedIssues }, "jira", "search", start);
1046
1174
  } catch (err) {
1047
1175
  fail(err, "jira", "search", start);
1048
1176
  }
@@ -1067,6 +1195,35 @@ function registerJiraCommands(program2) {
1067
1195
  fail(err, "jira", "link-issue", start);
1068
1196
  }
1069
1197
  });
1198
+ 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) => {
1199
+ const start = Date.now();
1200
+ try {
1201
+ if (opts.discover) {
1202
+ const client = getClient(program2);
1203
+ const fields = await client.fetchFields();
1204
+ const result = opts.customOnly ? fields.filter((f) => f.custom) : fields;
1205
+ success(result, "jira", "fields", start);
1206
+ } else {
1207
+ const { customFields } = getClientAndFields(program2);
1208
+ success(customFields, "jira", "fields", start);
1209
+ }
1210
+ } catch (err) {
1211
+ fail(err, "jira", "fields", start);
1212
+ }
1213
+ });
1214
+ }
1215
+ function parseFieldArgs(fieldArgs, fieldMap) {
1216
+ const result = {};
1217
+ for (const arg of fieldArgs) {
1218
+ const eq = arg.indexOf("=");
1219
+ if (eq === -1) throw new PncliError(`Invalid --field format (expected Name=value): ${arg}`, 1);
1220
+ const name = arg.slice(0, eq).trim();
1221
+ const value = arg.slice(eq + 1);
1222
+ const def = fieldMap.byName.get(name.toLowerCase());
1223
+ if (!def) throw new PncliError(`Unknown custom field: "${name}". Run: pncli jira fields`, 1);
1224
+ result[def.id] = formatFieldValue(value, def.type);
1225
+ }
1226
+ return result;
1070
1227
  }
1071
1228
 
1072
1229
  // src/services/bitbucket/commands.ts
@@ -1333,21 +1490,1326 @@ function registerSonarCommands(program2) {
1333
1490
  });
1334
1491
  }
1335
1492
 
1336
- // src/services/artifactory/commands.ts
1337
- function registerArtifactoryCommands(program2) {
1338
- program2.command("artifactory").description("Artifactory operations").action(() => {
1339
- success(
1340
- { message: "Coming soon \u2014 the nightmare never ends." },
1341
- "artifactory",
1342
- "stub",
1343
- Date.now()
1493
+ // src/services/deps/parsers/index.ts
1494
+ import fs2 from "fs";
1495
+ import path5 from "path";
1496
+ import { execSync as execSync4, execFileSync as execFileSync2 } from "child_process";
1497
+
1498
+ // src/services/deps/parsers/npm.ts
1499
+ import path2 from "path";
1500
+ function findNpmManifests(files) {
1501
+ const lockFiles = new Set(
1502
+ files.filter((f) => {
1503
+ const base = path2.basename(f);
1504
+ return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
1505
+ })
1506
+ );
1507
+ const manifests = [];
1508
+ for (const file of files) {
1509
+ if (path2.basename(file) !== "package.json") continue;
1510
+ if (file.split("/").includes("node_modules")) continue;
1511
+ const dir = path2.dirname(file);
1512
+ let lockFile;
1513
+ for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
1514
+ const candidate = dir === "." ? lf : `${dir}/${lf}`;
1515
+ if (lockFiles.has(candidate)) {
1516
+ lockFile = candidate;
1517
+ break;
1518
+ }
1519
+ }
1520
+ manifests.push({ file, ecosystem: "npm", lockFile });
1521
+ }
1522
+ return manifests;
1523
+ }
1524
+ function parseNpmPackages(content, manifest, opts, lockContent) {
1525
+ if (lockContent && manifest.lockFile) {
1526
+ const base = path2.basename(manifest.lockFile);
1527
+ if (base === "package-lock.json") {
1528
+ const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
1529
+ if (pkgs.length > 0) return pkgs;
1530
+ } else if (base === "yarn.lock") {
1531
+ return parseYarnLock(lockContent, manifest.lockFile, opts, content);
1532
+ } else if (base === "pnpm-lock.yaml") {
1533
+ return parsePnpmLock(lockContent, manifest.lockFile, opts, content);
1534
+ }
1535
+ }
1536
+ return parsePackageJson(content, manifest.file, opts);
1537
+ }
1538
+ function stripVersionPrefix(v) {
1539
+ return v.replace(/^[\^~>=<* ]+/, "").split(/\s/)[0] ?? v;
1540
+ }
1541
+ function parsePackageJson(content, filePath, opts) {
1542
+ let pkg2;
1543
+ try {
1544
+ pkg2 = JSON.parse(content);
1545
+ } catch {
1546
+ return [];
1547
+ }
1548
+ const packages = [];
1549
+ for (const [name, version] of Object.entries(pkg2.dependencies ?? {})) {
1550
+ packages.push({
1551
+ name,
1552
+ version: stripVersionPrefix(version),
1553
+ ecosystem: "npm",
1554
+ source: filePath,
1555
+ type: "direct",
1556
+ scope: "production"
1557
+ });
1558
+ }
1559
+ if (opts.includeDev) {
1560
+ for (const [name, version] of Object.entries(pkg2.devDependencies ?? {})) {
1561
+ packages.push({
1562
+ name,
1563
+ version: stripVersionPrefix(version),
1564
+ ecosystem: "npm",
1565
+ source: filePath,
1566
+ type: "direct",
1567
+ scope: "dev"
1568
+ });
1569
+ }
1570
+ }
1571
+ return packages;
1572
+ }
1573
+ function parsePackageLock(content, lockFilePath, opts) {
1574
+ let lock;
1575
+ try {
1576
+ lock = JSON.parse(content);
1577
+ } catch {
1578
+ return [];
1579
+ }
1580
+ if ((lock.lockfileVersion ?? 1) < 2 || !lock.packages) return [];
1581
+ const packages = [];
1582
+ for (const [pkgPath, meta] of Object.entries(lock.packages)) {
1583
+ if (pkgPath === "") continue;
1584
+ if (!meta.version) continue;
1585
+ const isDev = meta.dev === true || meta.devOptional === true;
1586
+ if (isDev && !opts.includeDev) continue;
1587
+ const withoutRoot = pkgPath.startsWith("node_modules/") ? pkgPath.slice("node_modules/".length) : pkgPath;
1588
+ const segments = withoutRoot.split("/node_modules/");
1589
+ const isTransitive = segments.length > 1;
1590
+ if (isTransitive && !opts.includeTransitive) continue;
1591
+ const name = segments[segments.length - 1] ?? pkgPath;
1592
+ packages.push({
1593
+ name,
1594
+ version: meta.version,
1595
+ ecosystem: "npm",
1596
+ source: lockFilePath,
1597
+ type: isTransitive ? "transitive" : "direct",
1598
+ scope: isDev ? "dev" : "production"
1599
+ });
1600
+ }
1601
+ return packages;
1602
+ }
1603
+ function parseYarnLock(content, lockFilePath, opts, packageJsonContent) {
1604
+ let pkgJson;
1605
+ try {
1606
+ pkgJson = JSON.parse(packageJsonContent);
1607
+ } catch {
1608
+ pkgJson = {};
1609
+ }
1610
+ const directDeps = new Set(Object.keys(pkgJson.dependencies ?? {}));
1611
+ const devDeps = new Set(Object.keys(pkgJson.devDependencies ?? {}));
1612
+ const allDirectDeps = /* @__PURE__ */ new Set([...directDeps, ...devDeps]);
1613
+ const packages = [];
1614
+ const seen = /* @__PURE__ */ new Set();
1615
+ const blockRegex = /^"?(@?[^@"\n][^"\n]*)@[^:]+:?\n\s+version "([^"]+)"/gm;
1616
+ for (const match of content.matchAll(blockRegex)) {
1617
+ const name = match[1].trim().replace(/^"|"$/g, "");
1618
+ const version = match[2];
1619
+ const key = `${name}@${version}`;
1620
+ if (seen.has(key)) continue;
1621
+ seen.add(key);
1622
+ const isDev = devDeps.has(name) && !directDeps.has(name);
1623
+ if (isDev && !opts.includeDev) continue;
1624
+ const isTransitive = !allDirectDeps.has(name);
1625
+ if (isTransitive && !opts.includeTransitive) continue;
1626
+ packages.push({
1627
+ name,
1628
+ version,
1629
+ ecosystem: "npm",
1630
+ source: lockFilePath,
1631
+ type: isTransitive ? "transitive" : "direct",
1632
+ scope: isDev ? "dev" : "production"
1633
+ });
1634
+ }
1635
+ return packages;
1636
+ }
1637
+ function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
1638
+ let pkgJson;
1639
+ try {
1640
+ pkgJson = JSON.parse(packageJsonContent);
1641
+ } catch {
1642
+ pkgJson = {};
1643
+ }
1644
+ const directDeps = new Set(Object.keys(pkgJson.dependencies ?? {}));
1645
+ const devDeps = new Set(Object.keys(pkgJson.devDependencies ?? {}));
1646
+ const allDirectDeps = /* @__PURE__ */ new Set([...directDeps, ...devDeps]);
1647
+ const packages = [];
1648
+ const seen = /* @__PURE__ */ new Set();
1649
+ const blockRegex = /^\s{2}(\/?@?[^@\s/][^@\s]*)@(\d[^:\s]*):\s*\n((?:\s{4}[^\n]+\n)*)/gm;
1650
+ for (const match of content.matchAll(blockRegex)) {
1651
+ const name = match[1].replace(/^\//, "");
1652
+ const version = match[2];
1653
+ const attrs = match[3] ?? "";
1654
+ const key = `${name}@${version}`;
1655
+ if (seen.has(key)) continue;
1656
+ seen.add(key);
1657
+ const isDev = devDeps.has(name) && !directDeps.has(name) || /^\s+dev:\s*true/m.test(attrs);
1658
+ if (isDev && !opts.includeDev) continue;
1659
+ const isTransitive = !allDirectDeps.has(name);
1660
+ if (isTransitive && !opts.includeTransitive) continue;
1661
+ packages.push({
1662
+ name,
1663
+ version,
1664
+ ecosystem: "npm",
1665
+ source: lockFilePath,
1666
+ type: isTransitive ? "transitive" : "direct",
1667
+ scope: isDev ? "dev" : "production"
1668
+ });
1669
+ }
1670
+ return packages;
1671
+ }
1672
+
1673
+ // src/services/deps/parsers/nuget.ts
1674
+ import path3 from "path";
1675
+ var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
1676
+ function findNugetManifests(files) {
1677
+ const lockFiles = /* @__PURE__ */ new Map();
1678
+ for (const f of files) {
1679
+ if (path3.basename(f) === "packages.lock.json") {
1680
+ lockFiles.set(path3.dirname(f), f);
1681
+ }
1682
+ }
1683
+ const manifests = [];
1684
+ for (const file of files) {
1685
+ const base = path3.basename(file);
1686
+ const ext = path3.extname(file);
1687
+ if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
1688
+ const dir = path3.dirname(file);
1689
+ const lockFile = lockFiles.get(dir);
1690
+ manifests.push({ file, ecosystem: "nuget", lockFile });
1691
+ }
1692
+ if (base === "Directory.Packages.props") {
1693
+ const dir = path3.dirname(file);
1694
+ manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
1695
+ }
1696
+ }
1697
+ const deduped = [];
1698
+ const seenLocks = /* @__PURE__ */ new Set();
1699
+ for (const manifest of manifests) {
1700
+ if (!manifest.lockFile) {
1701
+ deduped.push(manifest);
1702
+ continue;
1703
+ }
1704
+ if (!seenLocks.has(manifest.lockFile)) {
1705
+ seenLocks.add(manifest.lockFile);
1706
+ deduped.push(manifest);
1707
+ }
1708
+ }
1709
+ return deduped;
1710
+ }
1711
+ function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
1712
+ const base = path3.basename(manifest.file);
1713
+ if (lockContent) {
1714
+ return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
1715
+ }
1716
+ if (base === "packages.config") {
1717
+ return parsePackagesConfig(content, manifest.file);
1718
+ }
1719
+ if (base === "Directory.Packages.props") {
1720
+ return parseDirectoryPackagesProps(content, manifest.file);
1721
+ }
1722
+ return parseCsproj(content, manifest.file, propsContent);
1723
+ }
1724
+ function parseCsproj(content, filePath, propsContent) {
1725
+ const centralVersions = propsContent ? extractCentralVersions(propsContent) : /* @__PURE__ */ new Map();
1726
+ const packages = [];
1727
+ const singleLine = /<PackageReference\s+([^>]+?)\/>/gi;
1728
+ const multiLine = /<PackageReference\s+([^>]+?)>([\s\S]*?)<\/PackageReference>/gi;
1729
+ const extractFromAttrsAndInner = (attrs, inner) => {
1730
+ const nameMatch = /\bInclude="([^"]+)"/i.exec(attrs);
1731
+ if (!nameMatch) return;
1732
+ const name = nameMatch[1];
1733
+ let version;
1734
+ const attrVersion = /\bVersion="([^"]+)"/i.exec(attrs);
1735
+ if (attrVersion) {
1736
+ version = attrVersion[1];
1737
+ } else if (inner) {
1738
+ const innerVersion = /<Version>([^<]+)<\/Version>/i.exec(inner);
1739
+ if (innerVersion) version = innerVersion[1];
1740
+ }
1741
+ if (!version || version.startsWith("$(")) {
1742
+ version = centralVersions.get(name) ?? centralVersions.get(name.toLowerCase());
1743
+ }
1744
+ if (!version) return;
1745
+ packages.push({
1746
+ name,
1747
+ version,
1748
+ ecosystem: "nuget",
1749
+ source: filePath,
1750
+ type: "direct",
1751
+ scope: "production"
1752
+ });
1753
+ };
1754
+ for (const m of content.matchAll(singleLine)) {
1755
+ extractFromAttrsAndInner(m[1], "");
1756
+ }
1757
+ for (const m of content.matchAll(multiLine)) {
1758
+ extractFromAttrsAndInner(m[1], m[2] ?? "");
1759
+ }
1760
+ return packages;
1761
+ }
1762
+ function extractCentralVersions(propsContent) {
1763
+ const map = /* @__PURE__ */ new Map();
1764
+ const regex = /<PackageVersion\s+Include="([^"]+)"\s+Version="([^"]+)"/gi;
1765
+ for (const m of propsContent.matchAll(regex)) {
1766
+ map.set(m[1], m[2]);
1767
+ map.set(m[1].toLowerCase(), m[2]);
1768
+ }
1769
+ return map;
1770
+ }
1771
+ function parseDirectoryPackagesProps(content, filePath) {
1772
+ const packages = [];
1773
+ const regex = /<PackageVersion\s+Include="([^"]+)"\s+Version="([^"]+)"/gi;
1774
+ for (const m of content.matchAll(regex)) {
1775
+ packages.push({
1776
+ name: m[1],
1777
+ version: m[2],
1778
+ ecosystem: "nuget",
1779
+ source: filePath,
1780
+ type: "direct",
1781
+ scope: "production"
1782
+ });
1783
+ }
1784
+ return packages;
1785
+ }
1786
+ function parsePackagesConfig(content, filePath) {
1787
+ const packages = [];
1788
+ const regex = /<package\s+id="([^"]+)"\s+version="([^"]+)"/gi;
1789
+ for (const m of content.matchAll(regex)) {
1790
+ packages.push({
1791
+ name: m[1],
1792
+ version: m[2],
1793
+ ecosystem: "nuget",
1794
+ source: filePath,
1795
+ type: "direct",
1796
+ scope: "production"
1797
+ });
1798
+ }
1799
+ return packages;
1800
+ }
1801
+ function parsePackagesLock(content, filePath, opts) {
1802
+ let lock;
1803
+ try {
1804
+ lock = JSON.parse(content);
1805
+ } catch {
1806
+ return [];
1807
+ }
1808
+ const packages = [];
1809
+ const seen = /* @__PURE__ */ new Set();
1810
+ for (const frameworkDeps of Object.values(lock.dependencies ?? {})) {
1811
+ for (const [name, meta] of Object.entries(frameworkDeps)) {
1812
+ const version = meta.resolved ?? "";
1813
+ if (!version) continue;
1814
+ const isTransitive = meta.type === "Transitive";
1815
+ if (isTransitive && !opts.includeTransitive) continue;
1816
+ const key = `${name}@${version}`;
1817
+ if (seen.has(key)) continue;
1818
+ seen.add(key);
1819
+ packages.push({
1820
+ name,
1821
+ version,
1822
+ ecosystem: "nuget",
1823
+ source: filePath,
1824
+ type: isTransitive ? "transitive" : "direct",
1825
+ scope: "production"
1826
+ });
1827
+ }
1828
+ }
1829
+ return packages;
1830
+ }
1831
+
1832
+ // src/services/deps/parsers/maven.ts
1833
+ import path4 from "path";
1834
+ function findMavenManifests(files) {
1835
+ const lockFiles = new Set(files.filter((f) => path4.basename(f) === "gradle.lockfile"));
1836
+ const manifests = [];
1837
+ for (const file of files) {
1838
+ const base = path4.basename(file);
1839
+ if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
1840
+ const dir = path4.dirname(file);
1841
+ const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
1842
+ const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
1843
+ manifests.push({ file, ecosystem: "maven", lockFile });
1844
+ }
1845
+ }
1846
+ return manifests;
1847
+ }
1848
+ function parseMavenPackages(content, manifest, opts, lockContent) {
1849
+ const base = path4.basename(manifest.file);
1850
+ if (lockContent && manifest.lockFile) {
1851
+ return parseGradleLock(lockContent, manifest.lockFile, opts);
1852
+ }
1853
+ if (base === "pom.xml") {
1854
+ return parsePomXml(content, manifest.file, opts);
1855
+ }
1856
+ if (base === "build.gradle" || base === "build.gradle.kts") {
1857
+ return parseBuildGradle(content, manifest.file, opts);
1858
+ }
1859
+ return [];
1860
+ }
1861
+ function parsePomXml(content, filePath, opts) {
1862
+ const props = extractPomProperties(content);
1863
+ const managedVersions = extractDependencyManagement(content, props);
1864
+ const withoutMgmt = content.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/gi, "");
1865
+ const packages = [];
1866
+ const depRegex = /<dependency>([\s\S]*?)<\/dependency>/gi;
1867
+ for (const match of withoutMgmt.matchAll(depRegex)) {
1868
+ const inner = match[1];
1869
+ const groupId = /<groupId>\s*([^<\s]+)\s*<\/groupId>/i.exec(inner)?.[1];
1870
+ const artifactId = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/i.exec(inner)?.[1];
1871
+ if (!groupId || !artifactId) continue;
1872
+ const rawVersion = /<version>\s*([^<\s]+)\s*<\/version>/i.exec(inner)?.[1];
1873
+ const resolvedVersion = resolveProperty(rawVersion ?? null, props) ?? managedVersions.get(`${groupId}:${artifactId}`);
1874
+ if (!resolvedVersion) continue;
1875
+ const scope = /<scope>\s*([^<\s]+)\s*<\/scope>/i.exec(inner)?.[1] ?? "compile";
1876
+ const isDev = scope === "test" || scope === "provided";
1877
+ if (isDev && !opts.includeDev) continue;
1878
+ packages.push({
1879
+ name: `${groupId}:${artifactId}`,
1880
+ version: resolvedVersion,
1881
+ ecosystem: "maven",
1882
+ source: filePath,
1883
+ type: "direct",
1884
+ scope: isDev ? "dev" : "production"
1885
+ });
1886
+ }
1887
+ return packages;
1888
+ }
1889
+ function extractPomProperties(content) {
1890
+ const props = /* @__PURE__ */ new Map();
1891
+ const propsMatch = /<properties>([\s\S]*?)<\/properties>/i.exec(content);
1892
+ if (!propsMatch) return props;
1893
+ const propRegex = /<([a-zA-Z0-9._-]+)>\s*([^<]+)\s*<\/\1>/g;
1894
+ for (const m of propsMatch[1].matchAll(propRegex)) {
1895
+ props.set(m[1], m[2].trim());
1896
+ }
1897
+ return props;
1898
+ }
1899
+ function extractDependencyManagement(content, props) {
1900
+ const versions = /* @__PURE__ */ new Map();
1901
+ const mgmtMatch = /<dependencyManagement>([\s\S]*?)<\/dependencyManagement>/i.exec(content);
1902
+ if (!mgmtMatch) return versions;
1903
+ const depRegex = /<dependency>([\s\S]*?)<\/dependency>/gi;
1904
+ for (const m of mgmtMatch[1].matchAll(depRegex)) {
1905
+ const inner = m[1];
1906
+ const groupId = /<groupId>\s*([^<\s]+)\s*<\/groupId>/i.exec(inner)?.[1];
1907
+ const artifactId = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/i.exec(inner)?.[1];
1908
+ const rawVersion = /<version>\s*([^<\s]+)\s*<\/version>/i.exec(inner)?.[1];
1909
+ if (!groupId || !artifactId || !rawVersion) continue;
1910
+ const resolved = resolveProperty(rawVersion, props);
1911
+ if (resolved) versions.set(`${groupId}:${artifactId}`, resolved);
1912
+ }
1913
+ return versions;
1914
+ }
1915
+ function resolveProperty(value, props) {
1916
+ if (!value) return null;
1917
+ const resolved = value.replace(/\$\{([^}]+)\}/g, (_, key) => props.get(key) ?? `\${${key}}`);
1918
+ return /\$\{[^}]+\}/.test(resolved) ? null : resolved;
1919
+ }
1920
+ var GRADLE_DEV_CONFIGS = /* @__PURE__ */ new Set(["testImplementation", "testCompileOnly", "testRuntimeOnly", "testApi"]);
1921
+ function parseBuildGradle(content, filePath, opts) {
1922
+ const packages = [];
1923
+ const seen = /* @__PURE__ */ new Set();
1924
+ const stringNotation = /\b(\w+)\s*\(?\s*['"]([^:'"]+):([^:'"]+):([^'"]+)['"]\s*\)?/g;
1925
+ for (const m of content.matchAll(stringNotation)) {
1926
+ const config = m[1];
1927
+ const groupId = m[2];
1928
+ const artifactId = m[3];
1929
+ const version = m[4].split("@")[0];
1930
+ const isDev = GRADLE_DEV_CONFIGS.has(config);
1931
+ if (isDev && !opts.includeDev) continue;
1932
+ const name = `${groupId}:${artifactId}`;
1933
+ const key = `${name}@${version}`;
1934
+ if (seen.has(key)) continue;
1935
+ seen.add(key);
1936
+ packages.push({
1937
+ name,
1938
+ version,
1939
+ ecosystem: "maven",
1940
+ source: filePath,
1941
+ type: "direct",
1942
+ scope: isDev ? "dev" : "production"
1943
+ });
1944
+ }
1945
+ const mapNotation = /\b(\w+)\s+group:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]+)['"]\s*,\s*version:\s*['"]([^'"]+)['"]/g;
1946
+ for (const m of content.matchAll(mapNotation)) {
1947
+ const config = m[1];
1948
+ const groupId = m[2];
1949
+ const artifactId = m[3];
1950
+ const version = m[4];
1951
+ const isDev = GRADLE_DEV_CONFIGS.has(config);
1952
+ if (isDev && !opts.includeDev) continue;
1953
+ const name = `${groupId}:${artifactId}`;
1954
+ const key = `${name}@${version}`;
1955
+ if (seen.has(key)) continue;
1956
+ seen.add(key);
1957
+ packages.push({
1958
+ name,
1959
+ version,
1960
+ ecosystem: "maven",
1961
+ source: filePath,
1962
+ type: "direct",
1963
+ scope: isDev ? "dev" : "production"
1964
+ });
1965
+ }
1966
+ return packages;
1967
+ }
1968
+ function parseGradleLock(content, filePath, opts) {
1969
+ const packages = [];
1970
+ const seen = /* @__PURE__ */ new Set();
1971
+ const lineRegex = /^([^:#\s]+):([^:#\s]+):([^=\s]+)=(.+)$/gm;
1972
+ for (const m of content.matchAll(lineRegex)) {
1973
+ const groupId = m[1];
1974
+ const artifactId = m[2];
1975
+ const version = m[3];
1976
+ const configs = m[4].split(",").map((s) => s.trim());
1977
+ const isDev = configs.every((c) => GRADLE_DEV_CONFIGS.has(c));
1978
+ if (isDev && !opts.includeDev) continue;
1979
+ const name = `${groupId}:${artifactId}`;
1980
+ const key = `${name}@${version}`;
1981
+ if (seen.has(key)) continue;
1982
+ seen.add(key);
1983
+ packages.push({
1984
+ name,
1985
+ version,
1986
+ ecosystem: "maven",
1987
+ source: filePath,
1988
+ type: "direct",
1989
+ scope: isDev ? "dev" : "production"
1990
+ });
1991
+ }
1992
+ return packages;
1993
+ }
1994
+
1995
+ // src/services/deps/parsers/index.ts
1996
+ function getRepoFiles(repoRoot) {
1997
+ try {
1998
+ const out = execSync4("git ls-files --cached --others --exclude-standard", {
1999
+ encoding: "utf8",
2000
+ cwd: repoRoot
2001
+ });
2002
+ return out.trim().split("\n").filter(Boolean);
2003
+ } catch {
2004
+ return [];
2005
+ }
2006
+ }
2007
+ function readFile(repoRoot, relPath) {
2008
+ try {
2009
+ return fs2.readFileSync(path5.join(repoRoot, relPath), "utf8");
2010
+ } catch {
2011
+ return null;
2012
+ }
2013
+ }
2014
+ function readFileAtRef(repoRoot, ref, relPath) {
2015
+ try {
2016
+ return execFileSync2("git", ["show", `${ref}:${relPath}`], {
2017
+ encoding: "utf8",
2018
+ cwd: repoRoot,
2019
+ stdio: ["pipe", "pipe", "pipe"]
2020
+ });
2021
+ } catch {
2022
+ return null;
2023
+ }
2024
+ }
2025
+ function findPropsContent(manifestFile, readFn) {
2026
+ let dir = path5.dirname(manifestFile);
2027
+ for (let depth = 0; depth < 10; depth++) {
2028
+ const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
2029
+ const content = readFn(candidate);
2030
+ if (content) return content;
2031
+ if (dir === ".") break;
2032
+ dir = path5.dirname(dir);
2033
+ }
2034
+ return null;
2035
+ }
2036
+ function parseManifests(manifests, opts, readFn) {
2037
+ const allPackages = [];
2038
+ const seen = /* @__PURE__ */ new Set();
2039
+ for (const manifest of manifests) {
2040
+ const content = readFn(manifest.file);
2041
+ if (!content) continue;
2042
+ const lockContent = manifest.lockFile ? readFn(manifest.lockFile) ?? void 0 : void 0;
2043
+ let pkgs = [];
2044
+ if (manifest.ecosystem === "npm") {
2045
+ pkgs = parseNpmPackages(content, manifest, opts, lockContent);
2046
+ } else if (manifest.ecosystem === "nuget") {
2047
+ const propsContent = findPropsContent(manifest.file, readFn) ?? void 0;
2048
+ pkgs = parseNugetPackages(content, manifest, opts, lockContent, propsContent);
2049
+ } else if (manifest.ecosystem === "maven") {
2050
+ pkgs = parseMavenPackages(content, manifest, opts, lockContent);
2051
+ }
2052
+ for (const pkg2 of pkgs) {
2053
+ const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}:${pkg2.source}`;
2054
+ if (!seen.has(key)) {
2055
+ seen.add(key);
2056
+ allPackages.push(pkg2);
2057
+ }
2058
+ }
2059
+ }
2060
+ return allPackages;
2061
+ }
2062
+ function scanRepo(repoRoot, opts = {}) {
2063
+ const files = getRepoFiles(repoRoot);
2064
+ const eco = opts.ecosystem ?? "all";
2065
+ const npmManifests = eco === "all" || eco === "npm" ? findNpmManifests(files) : [];
2066
+ const nugetManifests = eco === "all" || eco === "nuget" ? findNugetManifests(files) : [];
2067
+ const mavenManifests = eco === "all" || eco === "maven" ? findMavenManifests(files) : [];
2068
+ const manifests = [...npmManifests, ...nugetManifests, ...mavenManifests];
2069
+ const packages = parseManifests(manifests, opts, (rel) => readFile(repoRoot, rel));
2070
+ return buildScanData(manifests, packages);
2071
+ }
2072
+ function scanRepoAtRef(repoRoot, ref, opts = {}) {
2073
+ const eco = opts.ecosystem ?? "all";
2074
+ let files = [];
2075
+ try {
2076
+ const out = execFileSync2("git", ["ls-tree", "-r", "--name-only", ref], {
2077
+ encoding: "utf8",
2078
+ cwd: repoRoot,
2079
+ stdio: ["pipe", "pipe", "pipe"]
2080
+ });
2081
+ files = out.trim().split("\n").filter(Boolean);
2082
+ } catch {
2083
+ return buildScanData([], []);
2084
+ }
2085
+ const npmManifests = eco === "all" || eco === "npm" ? findNpmManifests(files) : [];
2086
+ const nugetManifests = eco === "all" || eco === "nuget" ? findNugetManifests(files) : [];
2087
+ const mavenManifests = eco === "all" || eco === "maven" ? findMavenManifests(files) : [];
2088
+ const manifests = [...npmManifests, ...nugetManifests, ...mavenManifests];
2089
+ const packages = parseManifests(manifests, opts, (rel) => readFileAtRef(repoRoot, ref, rel));
2090
+ return buildScanData(manifests, packages);
2091
+ }
2092
+ function buildScanData(manifests, packages) {
2093
+ const ecosystemSet = new Set(manifests.map((m) => m.ecosystem));
2094
+ const byEcosystem = {};
2095
+ const byType = { direct: 0, transitive: 0 };
2096
+ const byScope = { production: 0, dev: 0 };
2097
+ for (const pkg2 of packages) {
2098
+ byEcosystem[pkg2.ecosystem] = (byEcosystem[pkg2.ecosystem] ?? 0) + 1;
2099
+ byType[pkg2.type] = (byType[pkg2.type] ?? 0) + 1;
2100
+ byScope[pkg2.scope] = (byScope[pkg2.scope] ?? 0) + 1;
2101
+ }
2102
+ return {
2103
+ ecosystems: Array.from(ecosystemSet),
2104
+ manifests,
2105
+ packages,
2106
+ summary: {
2107
+ totalPackages: packages.length,
2108
+ byEcosystem,
2109
+ byType,
2110
+ byScope
2111
+ }
2112
+ };
2113
+ }
2114
+
2115
+ // src/services/deps/scan.ts
2116
+ function runScan(config, opts) {
2117
+ void config;
2118
+ const repoRoot = getRepoRoot2();
2119
+ if (!repoRoot) {
2120
+ throw new PncliError("Not inside a git repository. deps commands require a git repo root.", 1);
2121
+ }
2122
+ return scanRepo(repoRoot, opts);
2123
+ }
2124
+
2125
+ // src/services/deps/semver.ts
2126
+ function parse(v) {
2127
+ const clean = v.replace(/^[^0-9]*/, "");
2128
+ const dashIdx = clean.indexOf("-");
2129
+ const plusIdx = clean.indexOf("+");
2130
+ const metaStart = plusIdx > -1 ? plusIdx : Infinity;
2131
+ const preStart = dashIdx > -1 && dashIdx < metaStart ? dashIdx : Infinity;
2132
+ const numeric = clean.slice(0, Math.min(preStart, metaStart));
2133
+ const preRelease = preStart < Infinity ? clean.slice(preStart + 1, metaStart < Infinity ? metaStart : void 0) : "";
2134
+ const parts = numeric.split(".").map((n) => parseInt(n, 10) || 0);
2135
+ return { major: parts[0] ?? 0, minor: parts[1] ?? 0, patch: parts[2] ?? 0, preRelease };
2136
+ }
2137
+ function compareSemver(a, b) {
2138
+ const av = parse(a);
2139
+ const bv = parse(b);
2140
+ if (av.major !== bv.major) return av.major - bv.major;
2141
+ if (av.minor !== bv.minor) return av.minor - bv.minor;
2142
+ if (av.patch !== bv.patch) return av.patch - bv.patch;
2143
+ if (!av.preRelease && bv.preRelease) return 1;
2144
+ if (av.preRelease && !bv.preRelease) return -1;
2145
+ if (av.preRelease && bv.preRelease) return av.preRelease.localeCompare(bv.preRelease);
2146
+ return 0;
2147
+ }
2148
+ function isNewer(current, latest) {
2149
+ return compareSemver(latest, current) > 0;
2150
+ }
2151
+ function updateType(current, latest) {
2152
+ const cv = parse(current);
2153
+ const lv = parse(latest);
2154
+ if (lv.major > cv.major) return "major";
2155
+ if (lv.minor > cv.minor) return "minor";
2156
+ return "patch";
2157
+ }
2158
+ function isDowngrade(from, to) {
2159
+ return compareSemver(to, from) < 0;
2160
+ }
2161
+
2162
+ // src/services/deps/diff.ts
2163
+ function runDiff(config, from, to, opts) {
2164
+ void config;
2165
+ const repoRoot = getRepoRoot2();
2166
+ if (!repoRoot) {
2167
+ throw new PncliError("Not inside a git repository.", 1);
2168
+ }
2169
+ const fromScan = scanRepoAtRef(repoRoot, from, opts);
2170
+ const toScan = to ? scanRepoAtRef(repoRoot, to, opts) : scanRepo(repoRoot, opts);
2171
+ const fromMap = /* @__PURE__ */ new Map();
2172
+ const toMap = /* @__PURE__ */ new Map();
2173
+ for (const pkg2 of fromScan.packages) {
2174
+ const key = `${pkg2.ecosystem}:${pkg2.name}:${pkg2.source}`;
2175
+ fromMap.set(key, pkg2.version);
2176
+ }
2177
+ for (const pkg2 of toScan.packages) {
2178
+ const key = `${pkg2.ecosystem}:${pkg2.name}:${pkg2.source}`;
2179
+ toMap.set(key, pkg2.version);
2180
+ }
2181
+ const changes = [];
2182
+ const allKeys = /* @__PURE__ */ new Set([...fromMap.keys(), ...toMap.keys()]);
2183
+ for (const key of allKeys) {
2184
+ const parts = key.split(":");
2185
+ const eco = parts[0];
2186
+ const source = parts[parts.length - 1] ?? "";
2187
+ const name = parts.slice(1, -1).join(":");
2188
+ const fromVer = fromMap.get(key) ?? null;
2189
+ const toVer = toMap.get(key) ?? null;
2190
+ let change;
2191
+ if (!fromVer) {
2192
+ change = "added";
2193
+ } else if (!toVer) {
2194
+ change = "removed";
2195
+ } else if (fromVer === toVer) {
2196
+ continue;
2197
+ } else {
2198
+ change = isDowngrade(fromVer, toVer) ? "downgraded" : "upgraded";
2199
+ }
2200
+ changes.push({ name, ecosystem: eco, change, from: fromVer, to: toVer, source });
2201
+ }
2202
+ const summary = { added: 0, removed: 0, upgraded: 0, downgraded: 0, unchanged: 0 };
2203
+ for (const c of changes) summary[c.change]++;
2204
+ summary.unchanged = [...allKeys].filter((k) => {
2205
+ const fv = fromMap.get(k);
2206
+ const tv = toMap.get(k);
2207
+ return fv !== void 0 && tv !== void 0 && fv === tv;
2208
+ }).length;
2209
+ return { from, to: to ?? "working tree", changes, summary };
2210
+ }
2211
+
2212
+ // src/services/deps/clients/osv.ts
2213
+ var OSV_URL = "https://api.osv.dev";
2214
+ var BATCH_SIZE = 500;
2215
+ var TIMEOUT_MS = 3e4;
2216
+ var OSV_ECOSYSTEM = {
2217
+ npm: "npm",
2218
+ nuget: "NuGet",
2219
+ maven: "Maven"
2220
+ };
2221
+ async function fetchWithTimeout2(url, init) {
2222
+ const controller = new AbortController();
2223
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
2224
+ try {
2225
+ return await fetch(url, { ...init, signal: controller.signal });
2226
+ } finally {
2227
+ clearTimeout(timer);
2228
+ }
2229
+ }
2230
+ async function checkOsvConnectivity() {
2231
+ try {
2232
+ const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
2233
+ method: "POST",
2234
+ headers: { "Content-Type": "application/json" },
2235
+ body: JSON.stringify({ queries: [] })
2236
+ });
2237
+ return { reachable: res.ok || res.status === 400 };
2238
+ } catch (err) {
2239
+ return {
2240
+ reachable: false,
2241
+ error: err instanceof Error ? err.message : String(err)
2242
+ };
2243
+ }
2244
+ }
2245
+ function extractRemediation(vuln, pkg2) {
2246
+ const fixedVersions = [];
2247
+ for (const affected of vuln.affected ?? []) {
2248
+ for (const range of affected.ranges ?? []) {
2249
+ if (range.type === "SEMVER" || range.type === "ECOSYSTEM") {
2250
+ for (const event of range.events ?? []) {
2251
+ if (event.fixed) fixedVersions.push(event.fixed);
2252
+ }
2253
+ }
2254
+ }
2255
+ }
2256
+ const unique = [...new Set(fixedVersions)];
2257
+ const fixAvailable = unique.length > 0;
2258
+ const advice = fixAvailable ? `Upgrade ${pkg2.name} to ${unique.join(" or ")}` : `No fix available yet for ${pkg2.name}. Monitor ${vuln.id} for updates.`;
2259
+ return { fixAvailable, fixedVersions: unique, advice };
2260
+ }
2261
+ function extractSeverity(vuln) {
2262
+ const dbSpecific = vuln.database_specific ?? {};
2263
+ const dbSeverity = typeof dbSpecific["severity"] === "string" ? dbSpecific["severity"] : void 0;
2264
+ let cvss = null;
2265
+ const rawCvss = dbSpecific["cvss"];
2266
+ if (typeof rawCvss === "number") {
2267
+ cvss = rawCvss;
2268
+ } else if (rawCvss && typeof rawCvss === "object" && "score" in rawCvss) {
2269
+ const score = rawCvss["score"];
2270
+ if (typeof score === "number") cvss = score;
2271
+ }
2272
+ for (const s of vuln.severity ?? []) {
2273
+ const score = parseFloat(s.score);
2274
+ if (!isNaN(score) && score >= 0 && score <= 10) {
2275
+ cvss = score;
2276
+ break;
2277
+ }
2278
+ }
2279
+ return {
2280
+ severity: dbSeverity ?? inferSeverityFromCvss(cvss),
2281
+ cvss
2282
+ };
2283
+ }
2284
+ function inferSeverityFromCvss(cvss) {
2285
+ if (cvss === null) return "UNKNOWN";
2286
+ if (cvss >= 9) return "CRITICAL";
2287
+ if (cvss >= 7) return "HIGH";
2288
+ if (cvss >= 4) return "MEDIUM";
2289
+ return "LOW";
2290
+ }
2291
+ function mapVuln(vuln, pkg2) {
2292
+ const { severity, cvss } = extractSeverity(vuln);
2293
+ return {
2294
+ id: vuln.id,
2295
+ summary: vuln.summary ?? "",
2296
+ severity,
2297
+ cvss,
2298
+ aliases: vuln.aliases ?? [],
2299
+ remediation: extractRemediation(vuln, pkg2),
2300
+ references: (vuln.references ?? []).map((r) => r.url ?? "").filter(Boolean)
2301
+ };
2302
+ }
2303
+ async function queryBatch(queries, packages) {
2304
+ const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
2305
+ method: "POST",
2306
+ headers: { "Content-Type": "application/json" },
2307
+ body: JSON.stringify({ queries })
2308
+ });
2309
+ if (!res.ok) {
2310
+ throw new Error(`OSV.dev returned HTTP ${res.status}`);
2311
+ }
2312
+ const data = await res.json();
2313
+ const vulnerable = [];
2314
+ for (let i = 0; i < data.results.length; i++) {
2315
+ const result = data.results[i];
2316
+ const pkg2 = packages[i];
2317
+ if (!result || !pkg2) continue;
2318
+ const vulns = result.vulns ?? [];
2319
+ if (vulns.length === 0) continue;
2320
+ vulnerable.push({
2321
+ ...pkg2,
2322
+ vulnerabilities: vulns.map((v) => mapVuln(v, pkg2))
2323
+ });
2324
+ }
2325
+ return vulnerable;
2326
+ }
2327
+ async function checkPackagesForVulns(packages) {
2328
+ const allVulnerable = [];
2329
+ for (let i = 0; i < packages.length; i += BATCH_SIZE) {
2330
+ const chunk = packages.slice(i, i + BATCH_SIZE);
2331
+ const queries = chunk.map((pkg2) => ({
2332
+ package: {
2333
+ name: pkg2.name,
2334
+ ecosystem: OSV_ECOSYSTEM[pkg2.ecosystem] ?? pkg2.ecosystem
2335
+ },
2336
+ version: pkg2.version
2337
+ }));
2338
+ const results = await queryBatch(queries, chunk);
2339
+ allVulnerable.push(...results);
2340
+ }
2341
+ return allVulnerable;
2342
+ }
2343
+
2344
+ // src/services/deps/clients/artifactory.ts
2345
+ var TIMEOUT_MS2 = 15e3;
2346
+ async function fetchWithTimeout3(url, token) {
2347
+ const controller = new AbortController();
2348
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
2349
+ try {
2350
+ return await fetch(url, {
2351
+ headers: { Authorization: `Bearer ${token}` },
2352
+ signal: controller.signal
2353
+ });
2354
+ } finally {
2355
+ clearTimeout(timer);
2356
+ }
2357
+ }
2358
+ async function checkArtifactoryConnectivity(config) {
2359
+ if (!config.baseUrl && !config.token) {
2360
+ return {
2361
+ reachable: false,
2362
+ authenticated: false,
2363
+ configured: false,
2364
+ error: "Artifactory is not configured"
2365
+ };
2366
+ }
2367
+ if (!config.baseUrl) {
2368
+ return {
2369
+ reachable: false,
2370
+ authenticated: false,
2371
+ configured: false,
2372
+ error: "Artifactory baseUrl is not set"
2373
+ };
2374
+ }
2375
+ if (!config.token) {
2376
+ return {
2377
+ reachable: false,
2378
+ authenticated: false,
2379
+ configured: false,
2380
+ error: "Artifactory token is not set"
2381
+ };
2382
+ }
2383
+ try {
2384
+ const res = await fetchWithTimeout3(`${config.baseUrl}/api/system/ping`, config.token);
2385
+ if (res.status === 401 || res.status === 403) {
2386
+ return {
2387
+ reachable: true,
2388
+ authenticated: false,
2389
+ configured: true,
2390
+ error: "Artifactory token is invalid or lacks permissions"
2391
+ };
2392
+ }
2393
+ return { reachable: res.ok, authenticated: res.ok, configured: true };
2394
+ } catch (err) {
2395
+ return {
2396
+ reachable: false,
2397
+ authenticated: false,
2398
+ configured: true,
2399
+ error: err instanceof Error ? err.message : String(err)
2400
+ };
2401
+ }
2402
+ }
2403
+ function requireArtifactory(config, command) {
2404
+ if (!config.baseUrl && !config.token) {
2405
+ throw new PncliError(
2406
+ `${command} requires Artifactory. Add the following to ~/.pncli/config.json:
2407
+ "artifactory": { "baseUrl": "https://artifactory.company.com", "token": "your-token" }
2408
+ Or set PNCLI_ARTIFACTORY_BASE_URL and PNCLI_ARTIFACTORY_TOKEN environment variables.`,
2409
+ 503
2410
+ );
2411
+ }
2412
+ if (!config.baseUrl) {
2413
+ throw new PncliError(
2414
+ `${command} requires an Artifactory URL. Set artifactory.baseUrl in ~/.pncli/config.json or PNCLI_ARTIFACTORY_BASE_URL.`,
2415
+ 503
2416
+ );
2417
+ }
2418
+ if (!config.token) {
2419
+ throw new PncliError(
2420
+ `${command} requires an Artifactory token. Set artifactory.token in ~/.pncli/config.json or PNCLI_ARTIFACTORY_TOKEN.`,
2421
+ 503
2422
+ );
2423
+ }
2424
+ }
2425
+ function repoForEcosystem(config, eco) {
2426
+ if (eco === "npm") return config.npmRepo;
2427
+ if (eco === "nuget") return config.nugetRepo;
2428
+ if (eco === "maven") return config.mavenRepo;
2429
+ return void 0;
2430
+ }
2431
+ async function getLatestNpm(baseUrl, token, repoName, packageName) {
2432
+ try {
2433
+ const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
2434
+ const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
2435
+ if (!res.ok) return null;
2436
+ const data = await res.json();
2437
+ return data["dist-tags"]?.latest ?? null;
2438
+ } catch {
2439
+ return null;
2440
+ }
2441
+ }
2442
+ async function getLatestNuget(baseUrl, token, repoName, packageName) {
2443
+ try {
2444
+ const lower = packageName.toLowerCase();
2445
+ const res = await fetchWithTimeout3(
2446
+ `${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
2447
+ token
2448
+ );
2449
+ if (!res.ok) return null;
2450
+ const data = await res.json();
2451
+ const versions = data.versions ?? [];
2452
+ return versions[versions.length - 1] ?? null;
2453
+ } catch {
2454
+ return null;
2455
+ }
2456
+ }
2457
+ async function getLatestMaven(baseUrl, token, repoName, packageName) {
2458
+ const [groupId, artifactId] = packageName.split(":");
2459
+ if (!groupId || !artifactId) return null;
2460
+ try {
2461
+ const res = await fetchWithTimeout3(
2462
+ `${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
2463
+ token
1344
2464
  );
2465
+ if (!res.ok) return null;
2466
+ const text = await res.text();
2467
+ return text.trim() || null;
2468
+ } catch {
2469
+ return null;
2470
+ }
2471
+ }
2472
+ async function getOutdatedPackages(packages, config, filterType) {
2473
+ const { baseUrl, token } = config;
2474
+ if (!baseUrl || !token) return { outdated: [], uncheckedEcosystems: [] };
2475
+ const outdated = [];
2476
+ const unchecked = /* @__PURE__ */ new Set();
2477
+ for (const pkg2 of packages) {
2478
+ const repoName = repoForEcosystem(config, pkg2.ecosystem);
2479
+ if (!repoName) {
2480
+ unchecked.add(pkg2.ecosystem);
2481
+ continue;
2482
+ }
2483
+ let latest = null;
2484
+ if (pkg2.ecosystem === "npm") {
2485
+ latest = await getLatestNpm(baseUrl, token, repoName, pkg2.name);
2486
+ } else if (pkg2.ecosystem === "nuget") {
2487
+ latest = await getLatestNuget(baseUrl, token, repoName, pkg2.name);
2488
+ } else if (pkg2.ecosystem === "maven") {
2489
+ latest = await getLatestMaven(baseUrl, token, repoName, pkg2.name);
2490
+ }
2491
+ if (!latest || !isNewer(pkg2.version, latest)) continue;
2492
+ const type = updateType(pkg2.version, latest);
2493
+ if (filterType) {
2494
+ const order = { major: 3, minor: 2, patch: 1 };
2495
+ if (order[type] < order[filterType]) continue;
2496
+ }
2497
+ outdated.push({
2498
+ name: pkg2.name,
2499
+ ecosystem: pkg2.ecosystem,
2500
+ current: pkg2.version,
2501
+ latest,
2502
+ updateType: type,
2503
+ source: pkg2.source
2504
+ });
2505
+ }
2506
+ return { outdated, uncheckedEcosystems: [...unchecked] };
2507
+ }
2508
+ async function getLicenseNpm(baseUrl, token, repoName, packageName) {
2509
+ try {
2510
+ const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
2511
+ const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
2512
+ if (!res.ok) return null;
2513
+ const data = await res.json();
2514
+ if (typeof data.license === "string") return data.license;
2515
+ if (typeof data.license === "object" && data.license !== null) return data.license.type ?? null;
2516
+ return null;
2517
+ } catch {
2518
+ return null;
2519
+ }
2520
+ }
2521
+ async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
2522
+ try {
2523
+ const lower = packageName.toLowerCase();
2524
+ const res = await fetchWithTimeout3(
2525
+ `${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
2526
+ token
2527
+ );
2528
+ if (!res.ok) return null;
2529
+ const data = await res.json();
2530
+ return data.licenseExpression ?? (data.licenseUrl ? "See licenseUrl" : null);
2531
+ } catch {
2532
+ return null;
2533
+ }
2534
+ }
2535
+ async function getLicensedPackages(packages, config) {
2536
+ const { baseUrl, token } = config;
2537
+ if (!baseUrl || !token) return [];
2538
+ const result = [];
2539
+ for (const pkg2 of packages) {
2540
+ const repoName = repoForEcosystem(config, pkg2.ecosystem);
2541
+ let license = null;
2542
+ if (repoName) {
2543
+ if (pkg2.ecosystem === "npm") {
2544
+ license = await getLicenseNpm(baseUrl, token, repoName, pkg2.name);
2545
+ } else if (pkg2.ecosystem === "nuget") {
2546
+ license = await getLicenseNuget(baseUrl, token, repoName, pkg2.name, pkg2.version);
2547
+ }
2548
+ }
2549
+ result.push({
2550
+ name: pkg2.name,
2551
+ version: pkg2.version,
2552
+ ecosystem: pkg2.ecosystem,
2553
+ source: pkg2.source,
2554
+ license
2555
+ });
2556
+ }
2557
+ return result;
2558
+ }
2559
+
2560
+ // src/services/deps/connectivity.ts
2561
+ var cachedTier = null;
2562
+ async function detectTier(config) {
2563
+ if (cachedTier) return cachedTier;
2564
+ const [osvResult, artResult] = await Promise.all([
2565
+ checkOsvConnectivity(),
2566
+ checkArtifactoryConnectivity(config.artifactory)
2567
+ ]);
2568
+ const artifactoryReachable = artResult.reachable && artResult.authenticated;
2569
+ const osvReachable = osvResult.reachable;
2570
+ let tier = "local";
2571
+ if (artifactoryReachable) tier = "artifactory";
2572
+ if (osvReachable) tier = "full";
2573
+ cachedTier = { tier, osvReachable, artifactoryReachable };
2574
+ return cachedTier;
2575
+ }
2576
+ async function buildConnectivityData(config) {
2577
+ const [osvResult, artResult] = await Promise.all([
2578
+ checkOsvConnectivity(),
2579
+ checkArtifactoryConnectivity(config.artifactory)
2580
+ ]);
2581
+ const artCfg = config.artifactory;
2582
+ let tier = "local";
2583
+ if (artResult.reachable && artResult.authenticated) tier = "artifactory";
2584
+ if (osvResult.reachable) tier = "full";
2585
+ cachedTier = {
2586
+ tier,
2587
+ osvReachable: osvResult.reachable,
2588
+ artifactoryReachable: artResult.reachable && artResult.authenticated
2589
+ };
2590
+ return {
2591
+ artifactory: {
2592
+ reachable: artResult.reachable,
2593
+ url: artCfg.baseUrl ?? "(not configured)",
2594
+ authenticated: artResult.authenticated,
2595
+ repositories: {
2596
+ npm: artCfg.npmRepo ?? "(not configured)",
2597
+ nuget: artCfg.nugetRepo ?? "(not configured)",
2598
+ maven: artCfg.mavenRepo ?? "(not configured)"
2599
+ },
2600
+ ...artResult.error ? { error: artResult.error } : {}
2601
+ },
2602
+ osv: {
2603
+ reachable: osvResult.reachable,
2604
+ url: "https://api.osv.dev",
2605
+ ...osvResult.error ? { error: osvResult.error } : {}
2606
+ },
2607
+ tier,
2608
+ capabilities: {
2609
+ scan: true,
2610
+ diff: true,
2611
+ outdated: artResult.reachable && artResult.authenticated,
2612
+ licenseCheck: artResult.reachable && artResult.authenticated,
2613
+ cveCheck: osvResult.reachable
2614
+ }
2615
+ };
2616
+ }
2617
+
2618
+ // src/services/deps/frisk.ts
2619
+ async function runFrisk(config, opts) {
2620
+ const repoRoot = getRepoRoot2();
2621
+ if (!repoRoot) {
2622
+ throw new PncliError("Not inside a git repository.", 1);
2623
+ }
2624
+ const { tier, osvReachable } = await detectTier(config);
2625
+ if (!osvReachable) {
2626
+ throw new PncliError(
2627
+ "deps frisk requires OSV.dev access but api.osv.dev is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
2628
+ 503
2629
+ );
2630
+ }
2631
+ const scanOpts = {
2632
+ ...opts,
2633
+ includeTransitive: opts.includeTransitive ?? true
2634
+ };
2635
+ const scan = scanRepo(repoRoot, scanOpts);
2636
+ if (scan.packages.length === 0) {
2637
+ return { tier, scanned: 0, vulnerable: 0, packages: [] };
2638
+ }
2639
+ const vulnerable = await checkPackagesForVulns(scan.packages);
2640
+ return {
2641
+ tier,
2642
+ scanned: scan.packages.length,
2643
+ vulnerable: vulnerable.length,
2644
+ packages: vulnerable
2645
+ };
2646
+ }
2647
+
2648
+ // src/services/deps/outdated.ts
2649
+ async function runOutdated(config, opts, filterType) {
2650
+ const repoRoot = getRepoRoot2();
2651
+ if (!repoRoot) {
2652
+ throw new PncliError("Not inside a git repository.", 1);
2653
+ }
2654
+ requireArtifactory(config.artifactory, "deps outdated");
2655
+ const { artifactoryReachable } = await detectTier(config);
2656
+ if (!artifactoryReachable) {
2657
+ throw new PncliError(
2658
+ `Artifactory at ${config.artifactory.baseUrl} is not reachable or authentication failed. Run 'pncli deps connectivity' to diagnose.`,
2659
+ 503
2660
+ );
2661
+ }
2662
+ const scan = scanRepo(repoRoot, { ...opts, includeTransitive: false });
2663
+ const directPackages = scan.packages.filter((p) => p.type === "direct");
2664
+ const { outdated, uncheckedEcosystems } = await getOutdatedPackages(
2665
+ directPackages,
2666
+ config.artifactory,
2667
+ filterType
2668
+ );
2669
+ const summary = { total: outdated.length, major: 0, minor: 0, patch: 0 };
2670
+ for (const pkg2 of outdated) summary[pkg2.updateType]++;
2671
+ return {
2672
+ source: "artifactory",
2673
+ artifactoryUrl: config.artifactory.baseUrl ?? "",
2674
+ outdated,
2675
+ uncheckedEcosystems,
2676
+ summary
2677
+ };
2678
+ }
2679
+
2680
+ // src/services/deps/license-check.ts
2681
+ async function runLicenseCheck(config, opts) {
2682
+ const repoRoot = getRepoRoot2();
2683
+ if (!repoRoot) {
2684
+ throw new PncliError("Not inside a git repository.", 1);
2685
+ }
2686
+ requireArtifactory(config.artifactory, "deps license-check");
2687
+ const { artifactoryReachable } = await detectTier(config);
2688
+ if (!artifactoryReachable) {
2689
+ throw new PncliError(
2690
+ `Artifactory at ${config.artifactory.baseUrl} is not reachable or authentication failed. Run 'pncli deps connectivity' to diagnose.`,
2691
+ 503
2692
+ );
2693
+ }
2694
+ const scan = scanRepo(repoRoot, { ...opts, includeTransitive: false });
2695
+ const directPackages = scan.packages.filter((p) => p.type === "direct");
2696
+ const licensed = await getLicensedPackages(directPackages, config.artifactory);
2697
+ const byLicense = {};
2698
+ let unknown = 0;
2699
+ for (const pkg2 of licensed) {
2700
+ if (!pkg2.license) {
2701
+ unknown++;
2702
+ } else {
2703
+ byLicense[pkg2.license] = (byLicense[pkg2.license] ?? 0) + 1;
2704
+ }
2705
+ }
2706
+ return {
2707
+ source: "artifactory",
2708
+ artifactoryUrl: config.artifactory.baseUrl ?? "",
2709
+ packages: licensed,
2710
+ summary: {
2711
+ total: licensed.length,
2712
+ byLicense,
2713
+ unknown
2714
+ }
2715
+ };
2716
+ }
2717
+
2718
+ // src/services/deps/commands.ts
2719
+ function registerDepsCommands(program2) {
2720
+ const deps = program2.command("deps").description("Dependency scanning, CVE detection, and license auditing");
2721
+ deps.command("frisk").description("Scan all dependencies for CVEs and output remediation paths (requires OSV.dev)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--direct-only", "Only scan direct dependencies (default: include transitive)", false).option("--include-dev", "Include dev/test dependencies", false).action(async (opts, cmd) => {
2722
+ const startTime = Date.now();
2723
+ try {
2724
+ const globalOpts = cmd.optsWithGlobals();
2725
+ const config = loadConfig({ configPath: globalOpts.config });
2726
+ const scanOpts = {
2727
+ ecosystem: opts.ecosystem,
2728
+ includeTransitive: !opts.directOnly,
2729
+ includeDev: opts.includeDev
2730
+ };
2731
+ const data = await runFrisk(config, scanOpts);
2732
+ success(data, "deps", "frisk", startTime);
2733
+ } catch (err) {
2734
+ fail(err, "deps", "frisk", startTime);
2735
+ }
2736
+ });
2737
+ deps.command("scan").description("Inventory all dependencies from manifest files (local only, no network)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--include-transitive", "Include transitive dependencies", false).option("--include-dev", "Include dev/test dependencies", false).action((opts, cmd) => {
2738
+ const startTime = Date.now();
2739
+ try {
2740
+ const globalOpts = cmd.optsWithGlobals();
2741
+ const config = loadConfig({ configPath: globalOpts.config });
2742
+ const scanOpts = {
2743
+ ecosystem: opts.ecosystem,
2744
+ includeTransitive: opts.includeTransitive,
2745
+ includeDev: opts.includeDev
2746
+ };
2747
+ const data = runScan(config, scanOpts);
2748
+ success(data, "deps", "scan", startTime);
2749
+ } catch (err) {
2750
+ fail(err, "deps", "scan", startTime);
2751
+ }
2752
+ });
2753
+ deps.command("diff").description("Show dependency changes between two git refs (local only, no network)").requiredOption("--from <ref>", "Base git ref (commit, tag, or branch)").option("--to <ref>", "Target git ref (default: working tree)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--include-dev", "Include dev/test dependencies", false).action((opts, cmd) => {
2754
+ const startTime = Date.now();
2755
+ try {
2756
+ const globalOpts = cmd.optsWithGlobals();
2757
+ const config = loadConfig({ configPath: globalOpts.config });
2758
+ const scanOpts = {
2759
+ ecosystem: opts.ecosystem,
2760
+ includeTransitive: true,
2761
+ includeDev: opts.includeDev
2762
+ };
2763
+ const data = runDiff(config, opts.from, opts.to ?? null, scanOpts);
2764
+ success(data, "deps", "diff", startTime);
2765
+ } catch (err) {
2766
+ fail(err, "deps", "diff", startTime);
2767
+ }
2768
+ });
2769
+ deps.command("outdated").description("Check for newer versions available in Artifactory (requires Artifactory)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--major", "Only show major version bumps").option("--minor", "Only show minor version bumps or higher").option("--patch", "Only show patch version bumps or higher").action(async (opts, cmd) => {
2770
+ const startTime = Date.now();
2771
+ try {
2772
+ const globalOpts = cmd.optsWithGlobals();
2773
+ const config = loadConfig({ configPath: globalOpts.config });
2774
+ const scanOpts = { ecosystem: opts.ecosystem };
2775
+ const filterType = opts.major ? "major" : opts.minor ? "minor" : opts.patch ? "patch" : void 0;
2776
+ const data = await runOutdated(config, scanOpts, filterType);
2777
+ success(data, "deps", "outdated", startTime);
2778
+ } catch (err) {
2779
+ fail(err, "deps", "outdated", startTime);
2780
+ }
2781
+ });
2782
+ deps.command("license-check").description("Report licenses for all direct dependencies via Artifactory (requires Artifactory)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--include-dev", "Include dev/test dependencies", false).action(async (opts, cmd) => {
2783
+ const startTime = Date.now();
2784
+ try {
2785
+ const globalOpts = cmd.optsWithGlobals();
2786
+ const config = loadConfig({ configPath: globalOpts.config });
2787
+ const scanOpts = {
2788
+ ecosystem: opts.ecosystem,
2789
+ includeDev: opts.includeDev
2790
+ };
2791
+ const data = await runLicenseCheck(config, scanOpts);
2792
+ success(data, "deps", "license-check", startTime);
2793
+ } catch (err) {
2794
+ fail(err, "deps", "license-check", startTime);
2795
+ }
2796
+ });
2797
+ deps.command("connectivity").description("Test network access to Artifactory and OSV.dev, report available tier").action(async (_opts, cmd) => {
2798
+ const startTime = Date.now();
2799
+ try {
2800
+ const globalOpts = cmd.optsWithGlobals();
2801
+ const config = loadConfig({ configPath: globalOpts.config });
2802
+ const data = await buildConnectivityData(config);
2803
+ success(data, "deps", "connectivity", startTime);
2804
+ } catch (err) {
2805
+ fail(err, "deps", "connectivity", startTime);
2806
+ }
1345
2807
  });
1346
2808
  }
1347
2809
 
1348
2810
  // src/services/config/commands.ts
1349
2811
  import { input, password, confirm } from "@inquirer/prompts";
1350
- import fs2 from "fs";
2812
+ import fs3 from "fs";
1351
2813
  function registerConfigCommands(program2) {
1352
2814
  const config = program2.command("config").description("Manage pncli configuration");
1353
2815
  config.command("init").description("Interactive setup wizard").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
@@ -1361,7 +2823,7 @@ function registerConfigCommands(program2) {
1361
2823
  } catch (err) {
1362
2824
  if (err instanceof Error && err.message.includes("User force closed")) {
1363
2825
  process.stderr.write("\nSetup cancelled.\n");
1364
- process.exit(1);
2826
+ process.exit(ExitCode.GENERAL_ERROR);
1365
2827
  }
1366
2828
  fail(err, "config", "init", start);
1367
2829
  }
@@ -1397,17 +2859,24 @@ function registerConfigCommands(program2) {
1397
2859
  }
1398
2860
  async function initGlobalConfig(start) {
1399
2861
  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):",
2862
+ 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");
2863
+ const userEmail = await input({
2864
+ message: "Your email address (used across Jira, Bitbucket, etc.):",
1402
2865
  default: ""
1403
2866
  });
1404
- const jiraEmail = await input({
1405
- message: "Jira email address:",
2867
+ const userId = await input({
2868
+ message: "Your username / user ID:",
2869
+ default: ""
2870
+ });
2871
+ 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");
2872
+ const jiraBaseUrl = await input({
2873
+ message: "Jira base URL (e.g. https://jira.your-company.com):",
1406
2874
  default: ""
1407
2875
  });
1408
2876
  const jiraApiToken = await password({
1409
- message: "Jira API token:"
2877
+ message: "Jira personal access token:"
1410
2878
  });
2879
+ 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
2880
  const bitbucketBaseUrl = await input({
1412
2881
  message: "Bitbucket Server base URL (e.g. https://bitbucket.your-company.com):",
1413
2882
  default: ""
@@ -1415,28 +2884,74 @@ async function initGlobalConfig(start) {
1415
2884
  const bitbucketPat = await password({
1416
2885
  message: "Bitbucket personal access token:"
1417
2886
  });
2887
+ process.stderr.write("\n\u2500\u2500 Artifactory \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");
2888
+ const useArtifactory = await confirm({
2889
+ message: "Configure Artifactory for dependency commands (deps outdated, deps license-check)?",
2890
+ default: false
2891
+ });
2892
+ let artifactoryBaseUrl = "";
2893
+ let artifactoryToken = "";
2894
+ let npmRepo = "";
2895
+ let nugetRepo = "";
2896
+ let mavenRepo = "";
2897
+ if (useArtifactory) {
2898
+ artifactoryBaseUrl = await input({
2899
+ message: "Artifactory base URL (e.g. https://artifactory.company.com):",
2900
+ default: ""
2901
+ });
2902
+ artifactoryToken = await password({
2903
+ message: "Artifactory API token:"
2904
+ });
2905
+ process.stderr.write("\nConfigure which ecosystems you use (skip any that don't apply):\n");
2906
+ const useNpm = await confirm({ message: " Use npm packages from Artifactory?", default: true });
2907
+ if (useNpm) {
2908
+ npmRepo = await input({ message: " npm repository name:", default: "npm-remote" });
2909
+ }
2910
+ const useNuget = await confirm({ message: " Use NuGet packages from Artifactory?", default: false });
2911
+ if (useNuget) {
2912
+ nugetRepo = await input({ message: " NuGet repository name:", default: "nuget-remote" });
2913
+ }
2914
+ const useMaven = await confirm({ message: " Use Maven packages from Artifactory?", default: false });
2915
+ if (useMaven) {
2916
+ mavenRepo = await input({ message: " Maven repository name:", default: "libs-release" });
2917
+ }
2918
+ }
2919
+ 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
2920
  const jiraProject = await input({
1419
2921
  message: "Default Jira project key (optional):",
1420
2922
  default: ""
1421
2923
  });
2924
+ process.stderr.write("\n");
1422
2925
  const confirmed = await confirm({
1423
2926
  message: "Write config to ~/.pncli/config.json?",
1424
2927
  default: true
1425
2928
  });
1426
2929
  if (!confirmed) {
1427
2930
  process.stderr.write("Aborted.\n");
1428
- process.exit(0);
2931
+ process.exit(ExitCode.SUCCESS);
1429
2932
  }
1430
2933
  writeGlobalConfig({
2934
+ user: {
2935
+ email: userEmail || void 0,
2936
+ userId: userId || void 0
2937
+ },
1431
2938
  jira: {
1432
2939
  baseUrl: jiraBaseUrl || void 0,
1433
- email: jiraEmail || void 0,
1434
2940
  apiToken: jiraApiToken || void 0
1435
2941
  },
1436
2942
  bitbucket: {
1437
2943
  baseUrl: bitbucketBaseUrl || void 0,
1438
2944
  pat: bitbucketPat || void 0
1439
2945
  },
2946
+ ...useArtifactory ? {
2947
+ artifactory: {
2948
+ baseUrl: artifactoryBaseUrl || void 0,
2949
+ token: artifactoryToken || void 0,
2950
+ npmRepo: npmRepo || void 0,
2951
+ nugetRepo: nugetRepo || void 0,
2952
+ mavenRepo: mavenRepo || void 0
2953
+ }
2954
+ } : {},
1440
2955
  defaults: {
1441
2956
  jira: {
1442
2957
  project: jiraProject || void 0
@@ -1471,16 +2986,16 @@ async function initRepoConfig(start) {
1471
2986
  });
1472
2987
  if (!confirmed) {
1473
2988
  process.stderr.write("Aborted.\n");
1474
- process.exit(0);
2989
+ process.exit(ExitCode.SUCCESS);
1475
2990
  }
1476
- if (fs2.existsSync(".pncli.json")) {
2991
+ if (fs3.existsSync(".pncli.json")) {
1477
2992
  const overwrite = await confirm({
1478
2993
  message: ".pncli.json already exists. Overwrite?",
1479
2994
  default: false
1480
2995
  });
1481
2996
  if (!overwrite) {
1482
2997
  process.stderr.write("Aborted.\n");
1483
- process.exit(0);
2998
+ process.exit(ExitCode.SUCCESS);
1484
2999
  }
1485
3000
  }
1486
3001
  writeRepoConfig({
@@ -1510,27 +3025,32 @@ program.hook("preAction", (thisCommand) => {
1510
3025
  pretty: Boolean(opts.pretty),
1511
3026
  verbose: Boolean(opts.verbose)
1512
3027
  });
3028
+ try {
3029
+ const config = loadConfig({ configPath: opts.config });
3030
+ setGlobalUser(config.user);
3031
+ } catch {
3032
+ }
1513
3033
  });
1514
3034
  registerGitCommands(program);
1515
3035
  registerJiraCommands(program);
1516
3036
  registerBitbucketCommands(program);
1517
3037
  registerConfluenceCommands(program);
1518
3038
  registerSonarCommands(program);
1519
- registerArtifactoryCommands(program);
3039
+ registerDepsCommands(program);
1520
3040
  registerConfigCommands(program);
1521
3041
  program.addHelpText("after", `
1522
3042
  Services:
1523
3043
  git Local git operations (status, diff, log, branch)
3044
+ deps Dependency scanning, CVE detection, license auditing
1524
3045
  jira Jira Data Cloud (coming soon)
1525
3046
  bitbucket Bitbucket Server (coming soon)
1526
3047
  confluence Confluence (coming soon)
1527
3048
  sonar SonarQube (coming soon)
1528
- artifactory Artifactory (coming soon)
1529
3049
  config Manage pncli configuration
1530
3050
  `);
1531
3051
  program.parseAsync(process.argv).catch((err) => {
1532
3052
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
1533
3053
  `);
1534
- process.exit(1);
3054
+ process.exit(ExitCode.GENERAL_ERROR);
1535
3055
  });
1536
3056
  //# sourceMappingURL=cli.js.map