@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/NOTICE +377 -50
- package/copilot-instructions.md +205 -49
- package/dist/cli.js +1724 -204
- package/dist/cli.js.map +1 -1
- package/package.json +4 -2
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
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
61
|
-
|
|
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
|
|
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
|
|
213
|
-
function
|
|
367
|
+
import { execSync as execSync3 } from "child_process";
|
|
368
|
+
function getRepoRoot2() {
|
|
214
369
|
try {
|
|
215
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
381
|
-
const url = new URL(
|
|
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
|
-
|
|
427
|
-
|
|
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 {
|
|
448
|
-
if (!
|
|
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": `
|
|
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(
|
|
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,
|
|
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
|
-
|
|
545
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
478
546
|
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
479
|
-
`)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
567
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
499
568
|
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
500
|
-
`)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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 =
|
|
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/
|
|
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: {
|
|
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 = {
|
|
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
|
|
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
|
|
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 =
|
|
1078
|
+
const { client, fieldMap } = getClientAndFields(program2);
|
|
959
1079
|
const data = await client.getIssue(opts.key);
|
|
960
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
1106
|
+
const { client, fieldMap } = getClientAndFields(program2);
|
|
985
1107
|
const labels = opts.labels ? opts.labels.split(",").map((s) => s.trim()) : void 0;
|
|
986
|
-
|
|
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 =
|
|
1165
|
+
const { client, fieldMap, customFields } = getClientAndFields(program2);
|
|
1043
1166
|
const maxResults = opts.maxResults ? parseInt(opts.maxResults, 10) : void 0;
|
|
1044
|
-
const
|
|
1045
|
-
|
|
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/
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
1401
|
-
|
|
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
|
|
1405
|
-
message: "
|
|
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
|
|
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(
|
|
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(
|
|
2989
|
+
process.exit(ExitCode.SUCCESS);
|
|
1475
2990
|
}
|
|
1476
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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(
|
|
3054
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
1535
3055
|
});
|
|
1536
3056
|
//# sourceMappingURL=cli.js.map
|